The OpenMC Monte Carlo Code

OpenMC is a community-developed Monte Carlo neutron and photon transport simulation code. It is capable of performing fixed source, k-eigenvalue, and subcritical multiplication calculations on models built using either a constructive solid geometry or CAD representation. OpenMC supports both continuous-energy and multigroup transport. The continuous-energy particle interaction data is based on a native HDF5 format that can be generated from ACE files produced by NJOY. Parallelism is enabled via a hybrid MPI and OpenMP programming model.

OpenMC was originally developed by members of the Computational Reactor Physics Group at the Massachusetts Institute of Technology starting in 2011. Various universities, laboratories, and other organizations now contribute to the development of OpenMC. For more information on OpenMC, feel free to post a message on the OpenMC Discourse Forum.

Recommended publication for citing

Paul K. Romano, Nicholas E. Horelik, Bryan R. Herman, Adam G. Nelson, Benoit Forget, and Kord Smith, “OpenMC: A State-of-the-Art Monte Carlo Code for Research and Development,” Ann. Nucl. Energy, 82, 90–97 (2015).

Contents

Quick Install Guide

This quick install guide outlines the basic steps needed to install OpenMC on your computer. For more detailed instructions on configuring and installing OpenMC, see Installation and Configuration in the User’s Manual.

Installing on Linux/Mac with conda-forge

Conda is an open source package management system and environment management system for installing multiple versions of software packages and their dependencies and switching easily between them. If you have conda installed on your system, OpenMC can be installed via the conda-forge channel. First, add the conda-forge channel with:

conda config --add channels conda-forge

To list the versions of OpenMC that are available on the conda-forge channel, in your terminal window or an Anaconda Prompt run:

conda search openmc

OpenMC can then be installed with:

conda create -n openmc-env openmc

This will install OpenMC in a conda environment called openmc-env. To activate the environment, run:

conda activate openmc-env

Installing on Linux/Mac/Windows with Docker

OpenMC can be easily deployed using Docker on any Windows, Mac, or Linux system. With Docker running, execute the following command in the shell to download and run a Docker image with the most recent release of OpenMC from DockerHub:

docker run openmc/openmc:latest

This will take several minutes to run depending on your internet download speed. The command will place you in an interactive shell running in a Docker container with OpenMC installed.

Note

The docker run command supports many options for spawning containers including mounting volumes from the host filesystem, which many users will find useful.

Installing from Source using Spack

Spack is a package management tool designed to support multiple versions and configurations of software on a wide variety of platforms and environments. Please follow Spack’s setup guide to configure the Spack system.

To install the latest OpenMC with the Python API, use the following command:

spack install py-openmc

For more information about customizations including MPI, see the detailed installation instructions using Spack. Once installed, environment/lmod modules can be generated or Spack’s load feature can be used to access the installed packages.

Installing from Source on Ubuntu

To build OpenMC from source, several prerequisites are needed. If you are using Ubuntu or higher, all prerequisites can be installed directly from the package manager:

sudo apt install g++ cmake libhdf5-dev

After the packages have been installed, follow the instructions below for building and installing OpenMC from source.

Installing from Source on Linux or Mac OS X

All OpenMC source code is hosted on GitHub. If you have git, the gcc compiler suite, CMake, and HDF5 installed, you can download and install OpenMC be entering the following commands in a terminal:

git clone --recurse-submodules https://github.com/openmc-dev/openmc.git
cd openmc
mkdir build && cd build
cmake ..
make
sudo make install

This will build an executable named openmc and install it (by default in /usr/local/bin). If you do not have administrator privileges, the cmake command should specify an installation directory where you have write access, e.g.

cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local ..

The openmc Python package must be installed separately. The easiest way to install it is using pip, which is included by default in Python 3.4+. From the root directory of the OpenMC distribution/repository, run:

pip install .

If you want to build a parallel version of OpenMC (using OpenMP or MPI), directions can be found in the detailed installation instructions.

Examples

The following series of Jupyter Notebooks provide examples for how to use various features of OpenMC by leveraging the Python API.

General Usage

Modeling a Pin-Cell

This notebook is intended to demonstrate the basic features of the Python API for constructing input files and running OpenMC. In it, we will show how to create a basic reflective pin-cell model that is equivalent to modeling an infinite array of fuel pins. If you have never used OpenMC, this can serve as a good starting point to learn the Python API. We highly recommend having a copy of the Python API reference documentation open in another browser tab that you can refer to.

[1]:
%matplotlib inline
import openmc
Defining Materials

Materials in OpenMC are defined as a set of nuclides with specified atom/weight fractions. To begin, we will create a material by making an instance of the Material class. In OpenMC, many objects, including materials, are identified by a “unique ID” that is simply just a positive integer. These IDs are used when exporting XML files that the solver reads in. They also appear in the output and can be used for identification. Since an integer ID is not very useful by itself, you can also give a material a name as well.

[2]:
uo2 = openmc.Material(1, "uo2")
print(uo2)
Material
        ID             =        1
        Name           =        uo2
        Temperature    =        None
        Density        =        None [sum]
        S(a,b) Tables
        Nuclides

On the XML side, you have no choice but to supply an ID. However, in the Python API, if you don’t give an ID, one will be automatically generated for you:

[3]:
mat = openmc.Material()
print(mat)
Material
        ID             =        2
        Name           =
        Temperature    =        None
        Density        =        None [sum]
        S(a,b) Tables
        Nuclides

We see that an ID of 2 was automatically assigned. Let’s now move on to adding nuclides to our uo2 material. The Material object has a method add_nuclide() whose first argument is the name of the nuclide and second argument is the atom or weight fraction.

[4]:
help(uo2.add_nuclide)
Help on method add_nuclide in module openmc.material:

add_nuclide(nuclide, percent, percent_type='ao') method of openmc.material.Material instance
    Add a nuclide to the material

    Parameters
    ----------
    nuclide : str
        Nuclide to add, e.g., 'Mo95'
    percent : float
        Atom or weight percent
    percent_type : {'ao', 'wo'}
        'ao' for atom percent and 'wo' for weight percent

We see that by default it assumes we want an atom fraction.

[5]:
# Add nuclides to uo2
uo2.add_nuclide('U235', 0.03)
uo2.add_nuclide('U238', 0.97)
uo2.add_nuclide('O16', 2.0)

Now we need to assign a total density to the material. We’ll use the set_density for this.

[6]:
uo2.set_density('g/cm3', 10.0)

You may sometimes be given a material specification where all the nuclide densities are in units of atom/b-cm. In this case, you just want the density to be the sum of the constituents. In that case, you can simply run mat.set_density('sum').

With UO2 finished, let’s now create materials for the clad and coolant. Note the use of add_element() for zirconium.

[7]:
zirconium = openmc.Material(name="zirconium")
zirconium.add_element('Zr', 1.0)
zirconium.set_density('g/cm3', 6.6)

water = openmc.Material(name="h2o")
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
water.set_density('g/cm3', 1.0)

An astute observer might now point out that this water material we just created will only use free-atom cross sections. We need to tell it to use an \(S(\alpha,\beta)\) table so that the bound atom cross section is used at thermal energies. To do this, there’s an add_s_alpha_beta() method. Note the use of the GND-style name “c_H_in_H2O”.

[8]:
water.add_s_alpha_beta('c_H_in_H2O')

When we go to run the transport solver in OpenMC, it is going to look for a materials.xml file. Thus far, we have only created objects in memory. To actually create a materials.xml file, we need to instantiate a Materials collection and export it to XML.

[9]:
materials = openmc.Materials([uo2, zirconium, water])

Note that Materials is actually a subclass of Python’s built-in list, so we can use methods like append(), insert(), pop(), etc.

[10]:
materials = openmc.Materials()
materials.append(uo2)
materials += [zirconium, water]
isinstance(materials, list)
[10]:
True

Finally, we can create the XML file with the export_to_xml() method. In a Jupyter notebook, we can run a shell command by putting ! before it, so in this case we are going to display the materials.xml file that we created.

[11]:
materials.export_to_xml()
!cat materials.xml
<?xml version='1.0' encoding='utf-8'?>
<materials>
  <material depletable="true" id="1" name="uo2">
    <density units="g/cm3" value="10.0" />
    <nuclide ao="0.03" name="U235" />
    <nuclide ao="0.97" name="U238" />
    <nuclide ao="2.0" name="O16" />
  </material>
  <material id="3" name="zirconium">
    <density units="g/cm3" value="6.6" />
    <nuclide ao="0.5145" name="Zr90" />
    <nuclide ao="0.1122" name="Zr91" />
    <nuclide ao="0.1715" name="Zr92" />
    <nuclide ao="0.1738" name="Zr94" />
    <nuclide ao="0.028" name="Zr96" />
  </material>
  <material id="4" name="h2o">
    <density units="g/cm3" value="1.0" />
    <nuclide ao="2.0" name="H1" />
    <nuclide ao="1.0" name="O16" />
    <sab name="c_H_in_H2O" />
  </material>
</materials>
Element Expansion

Did you notice something really cool that happened to our Zr element? OpenMC automatically turned it into a list of nuclides when it exported it! The way this feature works is as follows:

  • First, it checks whether Materials.cross_sections has been set, indicating the path to a cross_sections.xml file.
  • If Materials.cross_sections isn’t set, it looks for the OPENMC_CROSS_SECTIONS environment variable.
  • If either of these are found, it scans the file to see what nuclides are actually available and will expand elements accordingly.

Let’s see what happens if we change O16 in water to elemental O.

[12]:
water.remove_nuclide('O16')
water.add_element('O', 1.0)

materials.export_to_xml()
!cat materials.xml
<?xml version='1.0' encoding='utf-8'?>
<materials>
  <material depletable="true" id="1" name="uo2">
    <density units="g/cm3" value="10.0" />
    <nuclide ao="0.03" name="U235" />
    <nuclide ao="0.97" name="U238" />
    <nuclide ao="2.0" name="O16" />
  </material>
  <material id="3" name="zirconium">
    <density units="g/cm3" value="6.6" />
    <nuclide ao="0.5145" name="Zr90" />
    <nuclide ao="0.1122" name="Zr91" />
    <nuclide ao="0.1715" name="Zr92" />
    <nuclide ao="0.1738" name="Zr94" />
    <nuclide ao="0.028" name="Zr96" />
  </material>
  <material id="4" name="h2o">
    <density units="g/cm3" value="1.0" />
    <nuclide ao="2.0" name="H1" />
    <nuclide ao="0.999621" name="O16" />
    <nuclide ao="0.000379" name="O17" />
    <sab name="c_H_in_H2O" />
  </material>
</materials>

We see that now O16 and O17 were automatically added. O18 is missing because our cross sections file (which is based on ENDF/B-VII.1) doesn’t have O18. If OpenMC didn’t know about the cross sections file, it would have assumed that all isotopes exist.

The cross_sections.xml file

The cross_sections.xml tells OpenMC where it can find nuclide cross sections and \(S(\alpha,\beta)\) tables. It serves the same purpose as MCNP’s xsdir file and Serpent’s xsdata file. As we mentioned, this can be set either by the OPENMC_CROSS_SECTIONS environment variable or the Materials.cross_sections attribute.

Let’s have a look at what’s inside this file:

[13]:
!cat $OPENMC_CROSS_SECTIONS | head -n 10
print('    ...')
!cat $OPENMC_CROSS_SECTIONS | tail -n 10
<?xml version='1.0' encoding='utf-8'?>
<cross_sections>
  <library materials="H1" path="H1.h5" type="neutron" />
  <library materials="H2" path="H2.h5" type="neutron" />
  <library materials="H3" path="H3.h5" type="neutron" />
  <library materials="He3" path="He3.h5" type="neutron" />
  <library materials="He4" path="He4.h5" type="neutron" />
  <library materials="Li6" path="Li6.h5" type="neutron" />
  <library materials="Li7" path="Li7.h5" type="neutron" />
  <library materials="Be7" path="Be7.h5" type="neutron" />
    ...
  <library materials="Cf253" path="wmp/098253.h5" type="wmp" />
  <library materials="Cf254" path="wmp/098254.h5" type="wmp" />
  <library materials="Es251" path="wmp/099251.h5" type="wmp" />
  <library materials="Es252" path="wmp/099252.h5" type="wmp" />
  <library materials="Es253" path="wmp/099253.h5" type="wmp" />
  <library materials="Es254" path="wmp/099254.h5" type="wmp" />
  <library materials="Es254_m1" path="wmp/099254m1.h5" type="wmp" />
  <library materials="Es255" path="wmp/099255.h5" type="wmp" />
  <library materials="Fm255" path="wmp/100255.h5" type="wmp" />
</cross_sections>
Enrichment

Note that the add_element() method has a special argument enrichment that can be used for Uranium. For example, if we know that we want to create 3% enriched UO2, the following would work:

[14]:
uo2_three = openmc.Material()
uo2_three.add_element('U', 1.0, enrichment=3.0)
uo2_three.add_element('O', 2.0)
uo2_three.set_density('g/cc', 10.0)
Mixtures

In OpenMC it is also possible to define materials by mixing existing materials. For example, if we wanted to create MOX fuel out of a mixture of UO2 (97 wt%) and PuO2 (3 wt%) we could do the following:

[15]:
# Create PuO2 material
puo2 = openmc.Material()
puo2.add_nuclide('Pu239', 0.94)
puo2.add_nuclide('Pu240', 0.06)
puo2.add_nuclide('O16', 2.0)
puo2.set_density('g/cm3', 11.5)

# Create the mixture
mox = openmc.Material.mix_materials([uo2, puo2], [0.97, 0.03], 'wo')

The ‘wo’ argument in the mix_materials() method specifies that the fractions are weight fractions. Materials can also be mixed by atomic and volume fractions with ‘ao’ and ‘vo’, respectively. For ‘ao’ and ‘wo’ the fractions must sum to one. For ‘vo’, if fractions do not sum to one, the remaining fraction is set as void.

Defining Geometry

At this point, we have three materials defined, exported to XML, and ready to be used in our model. To finish our model, we need to define the geometric arrangement of materials. OpenMC represents physical volumes using constructive solid geometry (CSG), also known as combinatorial geometry. The object that allows us to assign a material to a region of space is called a Cell (same concept in MCNP, for those familiar). In order to define a region that we can assign to a cell, we must first define surfaces which bound the region. A surface is a locus of zeros of a function of Cartesian coordinates \(x\), \(y\), and \(z\), e.g.

  • A plane perpendicular to the x axis: \(x - x_0 = 0\)
  • A cylinder parallel to the z axis: \((x - x_0)^2 + (y - y_0)^2 - R^2 = 0\)
  • A sphere: \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\)

Between those three classes of surfaces (planes, cylinders, spheres), one can construct a wide variety of models. It is also possible to define cones and general second-order surfaces (tori are not currently supported).

Note that defining a surface is not sufficient to specify a volume – in order to define an actual volume, one must reference the half-space of a surface. A surface half-space is the region whose points satisfy a positive or negative inequality of the surface equation. For example, for a sphere of radius one centered at the origin, the surface equation is \(f(x,y,z) = x^2 + y^2 + z^2 - 1 = 0\). Thus, we say that the negative half-space of the sphere, is defined as the collection of points satisfying \(f(x,y,z) < 0\), which one can reason is the inside of the sphere. Conversely, the positive half-space of the sphere would correspond to all points outside of the sphere.

Let’s go ahead and create a sphere and confirm that what we’ve told you is true.

[16]:
sphere = openmc.Sphere(r=1.0)

Note that by default the sphere is centered at the origin so we didn’t have to supply x0, y0, or z0 arguments. Strictly speaking, we could have omitted R as well since it defaults to one. To get the negative or positive half-space, we simply need to apply the - or + unary operators, respectively.

(NOTE: Those unary operators are defined by special methods: __pos__ and __neg__ in this case).

[17]:
inside_sphere = -sphere
outside_sphere = +sphere

Now let’s see if inside_sphere actually contains points inside the sphere:

[18]:
print((0,0,0) in inside_sphere, (0,0,2) in inside_sphere)
print((0,0,0) in outside_sphere, (0,0,2) in outside_sphere)
True False
False True

Everything works as expected! Now that we understand how to create half-spaces, we can create more complex volumes by combining half-spaces using Boolean operators: & (intersection), | (union), and ~ (complement). For example, let’s say we want to define a region that is the top part of the sphere (all points inside the sphere that have \(z > 0\).

[19]:
z_plane = openmc.ZPlane(z0=0)
northern_hemisphere = -sphere & +z_plane

For many regions, OpenMC can automatically determine a bounding box. To get the bounding box, we use the bounding_box property of a region, which returns a tuple of the lower-left and upper-right Cartesian coordinates for the bounding box:

[20]:
northern_hemisphere.bounding_box
[20]:
(array([-1., -1.,  0.]), array([1., 1., 1.]))

Now that we see how to create volumes, we can use them to create a cell.

[21]:
cell = openmc.Cell()
cell.region = northern_hemisphere

# or...
cell = openmc.Cell(region=northern_hemisphere)

By default, the cell is not filled by any material (void). In order to assign a material, we set the fill property of a Cell.

[22]:
cell.fill = water
Universes and in-line plotting

A collection of cells is known as a universe (again, this will be familiar to MCNP/Serpent users) and can be used as a repeatable unit when creating a model. Although we don’t need it yet, the benefit of creating a universe is that we can visualize our geometry while we’re creating it.

[23]:
universe = openmc.Universe()
universe.add_cell(cell)

# this also works
universe = openmc.Universe(cells=[cell])

The Universe object has a plot method that will display our the universe as current constructed:

[24]:
universe.plot(width=(2.0, 2.0))
[24]:
<matplotlib.image.AxesImage at 0x7f7df827b250>
_images/examples_pincell_50_1.png

By default, the plot will appear in the \(x\)-\(y\) plane. We can change that with the basis argument.

[25]:
universe.plot(width=(2.0, 2.0), basis='xz')
[25]:
<matplotlib.image.AxesImage at 0x7f7df8112100>
_images/examples_pincell_52_1.png

If we have particular fondness for, say, fuchsia, we can tell the plot() method to make our cell that color.

[26]:
universe.plot(width=(2.0, 2.0), basis='xz',
              colors={cell: 'fuchsia'})
[26]:
<matplotlib.image.AxesImage at 0x7f7df80edd30>
_images/examples_pincell_54_1.png
Pin cell geometry

We now have enough knowledge to create our pin-cell. We need three surfaces to define the fuel and clad:

  1. The outer surface of the fuel – a cylinder parallel to the z axis
  2. The inner surface of the clad – same as above
  3. The outer surface of the clad – same as above

These three surfaces will all be instances of openmc.ZCylinder, each with a different radius according to the specification.

[27]:
fuel_outer_radius = openmc.ZCylinder(r=0.39)
clad_inner_radius = openmc.ZCylinder(r=0.40)
clad_outer_radius = openmc.ZCylinder(r=0.46)

With the surfaces created, we can now take advantage of the built-in operators on surfaces to create regions for the fuel, the gap, and the clad:

[28]:
fuel_region = -fuel_outer_radius
gap_region = +fuel_outer_radius & -clad_inner_radius
clad_region = +clad_inner_radius & -clad_outer_radius

Now we can create corresponding cells that assign materials to these regions. As with materials, cells have unique IDs that are assigned either manually or automatically. Note that the gap cell doesn’t have any material assigned (it is void by default).

[29]:
fuel = openmc.Cell(name='fuel')
fuel.fill = uo2
fuel.region = fuel_region

gap = openmc.Cell(name='air gap')
gap.region = gap_region

clad = openmc.Cell(name='clad')
clad.fill = zirconium
clad.region = clad_region

Finally, we need to handle the coolant outside of our fuel pin. To do this, we create x- and y-planes that bound the geometry.

[30]:
pitch = 1.26
left = openmc.XPlane(x0=-pitch/2, boundary_type='reflective')
right = openmc.XPlane(x0=pitch/2, boundary_type='reflective')
bottom = openmc.YPlane(y0=-pitch/2, boundary_type='reflective')
top = openmc.YPlane(y0=pitch/2, boundary_type='reflective')

The water region is going to be everything outside of the clad outer radius and within the box formed as the intersection of four half-spaces.

[31]:
water_region = +left & -right & +bottom & -top & +clad_outer_radius

moderator = openmc.Cell(name='moderator')
moderator.fill = water
moderator.region = water_region

OpenMC also includes a factory function that generates a rectangular prism that could have made our lives easier.

[32]:
box = openmc.rectangular_prism(width=pitch, height=pitch,
                               boundary_type='reflective')
type(box)
[32]:
openmc.region.Intersection

Pay attention here – the object that was returned is NOT a surface. It is actually the intersection of four surface half-spaces, just like we created manually before. Thus, we don’t need to apply the unary operator (-box). Instead, we can directly combine it with +clad_or.

[33]:
water_region = box & +clad_outer_radius

The final step is to assign the cells we created to a universe and tell OpenMC that this universe is the “root” universe in our geometry. The Geometry is the final object that is actually exported to XML.

[34]:
root_universe = openmc.Universe(cells=(fuel, gap, clad, moderator))

geometry = openmc.Geometry()
geometry.root_universe = root_universe

# or...
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()
!cat geometry.xml
<?xml version='1.0' encoding='utf-8'?>
<geometry>
  <cell id="3" material="1" name="fuel" region="-3" universe="3" />
  <cell id="4" material="void" name="air gap" region="3 -4" universe="3" />
  <cell id="5" material="3" name="clad" region="4 -5" universe="3" />
  <cell id="6" material="4" name="moderator" region="6 -7 8 -9 5" universe="3" />
  <surface coeffs="0.0 0.0 0.39" id="3" type="z-cylinder" />
  <surface coeffs="0.0 0.0 0.4" id="4" type="z-cylinder" />
  <surface coeffs="0.0 0.0 0.46" id="5" type="z-cylinder" />
  <surface boundary="reflective" coeffs="-0.63" id="6" type="x-plane" />
  <surface boundary="reflective" coeffs="0.63" id="7" type="x-plane" />
  <surface boundary="reflective" coeffs="-0.63" id="8" type="y-plane" />
  <surface boundary="reflective" coeffs="0.63" id="9" type="y-plane" />
</geometry>
Starting source and settings

The Python API has a module openmc.stats with various univariate and multivariate probability distributions. We can use these distributions to create a starting source using the openmc.Source object.

[35]:
# Create a point source
point = openmc.stats.Point((0, 0, 0))
source = openmc.Source(space=point)

Now let’s create a Settings object and give it the source we created along with specifying how many batches and particles we want to run.

[36]:
settings = openmc.Settings()
settings.source = source
settings.batches = 100
settings.inactive = 10
settings.particles = 1000
[37]:
settings.export_to_xml()
!cat settings.xml
<?xml version='1.0' encoding='utf-8'?>
<settings>
  <run_mode>eigenvalue</run_mode>
  <particles>1000</particles>
  <batches>100</batches>
  <inactive>10</inactive>
  <source strength="1.0">
    <space type="point">
      <parameters>0 0 0</parameters>
    </space>
  </source>
</settings>
User-defined tallies

We actually have all the required files needed to run a simulation. Before we do that though, let’s give a quick example of how to create tallies. We will show how one would tally the total, fission, absorption, and (n,:math:gamma) reaction rates for \(^{235}\)U in the cell containing fuel. Recall that filters allow us to specify where in phase-space we want events to be tallied and scores tell us what we want to tally:

\[X = \underbrace{\int d\mathbf{r} \int d\mathbf{\Omega} \int dE}_{\text{filters}} \; \underbrace{f(\mathbf{r},\mathbf{\Omega},E)}_{\text{scores}} \psi (\mathbf{r},\mathbf{\Omega},E) \]

In this case, the where is “the fuel cell”. So, we will create a cell filter specifying the fuel cell.

[38]:
cell_filter = openmc.CellFilter(fuel)

tally = openmc.Tally(1)
tally.filters = [cell_filter]

The what is the total, fission, absorption, and (n,:math:gamma) reaction rates in \(^{235}\)U. By default, if we only specify what reactions, it will gives us tallies over all nuclides. We can use the nuclides attribute to name specific nuclides we’re interested in.

[39]:
tally.nuclides = ['U235']
tally.scores = ['total', 'fission', 'absorption', '(n,gamma)']

Similar to the other files, we need to create a Tallies collection and export it to XML.

[40]:
tallies = openmc.Tallies([tally])
tallies.export_to_xml()
!cat tallies.xml
<?xml version='1.0' encoding='utf-8'?>
<tallies>
  <filter id="1" type="cell">
    <bins>3</bins>
  </filter>
  <tally id="1">
    <filters>1</filters>
    <nuclides>U235</nuclides>
    <scores>total fission absorption (n,gamma)</scores>
  </tally>
</tallies>
Running OpenMC

Running OpenMC from Python can be done using the openmc.run() function. This function allows you to set the number of MPI processes and OpenMP threads, if need be.

[41]:
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2020 MIT and OpenMC contributors
           License | https://docs.openmc.org/en/latest/license.html
           Version | 0.12.0
          Git SHA1 | 3d90a9f857ec72eae897e054d4225180f1fa4d93
         Date/Time | 2020-08-25 14:58:51
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /home/master/data/nuclear/endfb71_hdf5/U235.h5
 Reading U238 from /home/master/data/nuclear/endfb71_hdf5/U238.h5
 Reading O16 from /home/master/data/nuclear/endfb71_hdf5/O16.h5
 Reading Zr90 from /home/master/data/nuclear/endfb71_hdf5/Zr90.h5
 Reading Zr91 from /home/master/data/nuclear/endfb71_hdf5/Zr91.h5
 Reading Zr92 from /home/master/data/nuclear/endfb71_hdf5/Zr92.h5
 Reading Zr94 from /home/master/data/nuclear/endfb71_hdf5/Zr94.h5
 Reading Zr96 from /home/master/data/nuclear/endfb71_hdf5/Zr96.h5
 Reading H1 from /home/master/data/nuclear/endfb71_hdf5/H1.h5
 Reading O17 from /home/master/data/nuclear/endfb71_hdf5/O17.h5
 Reading c_H_in_H2O from /home/master/data/nuclear/endfb71_hdf5/c_H_in_H2O.h5
 Minimum neutron data temperature: 294.000000 K
 Maximum neutron data temperature: 294.000000 K
 Reading tallies XML file...
 Preparing distributed cell instances...
 Writing summary.h5 file...
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.42066
        2/1    1.39831
        3/1    1.46207
        4/1    1.44888
        5/1    1.42595
        6/1    1.35549
        7/1    1.36717
        8/1    1.45095
        9/1    1.36061
       10/1    1.36554
       11/1    1.36973
       12/1    1.44276    1.40625 +/- 0.03652
       13/1    1.35512    1.38920 +/- 0.02711
       14/1    1.54216    1.42744 +/- 0.04277
       15/1    1.39353    1.42066 +/- 0.03382
       16/1    1.38650    1.41497 +/- 0.02820
       17/1    1.38760    1.41106 +/- 0.02415
       18/1    1.38413    1.40769 +/- 0.02118
       19/1    1.39088    1.40582 +/- 0.01877
       20/1    1.47468    1.41271 +/- 0.01815
       21/1    1.45695    1.41673 +/- 0.01690
       22/1    1.40308    1.41559 +/- 0.01547
       23/1    1.40821    1.41503 +/- 0.01424
       24/1    1.32301    1.40845 +/- 0.01473
       25/1    1.36702    1.40569 +/- 0.01399
       26/1    1.30968    1.39969 +/- 0.01440
       27/1    1.38099    1.39859 +/- 0.01357
       28/1    1.42103    1.39984 +/- 0.01285
       29/1    1.39741    1.39971 +/- 0.01216
       30/1    1.36548    1.39800 +/- 0.01166
       31/1    1.41573    1.39884 +/- 0.01112
       32/1    1.39788    1.39880 +/- 0.01061
       33/1    1.35942    1.39709 +/- 0.01028
       34/1    1.40483    1.39741 +/- 0.00985
       35/1    1.39418    1.39728 +/- 0.00944
       36/1    1.41492    1.39796 +/- 0.00910
       37/1    1.49392    1.40151 +/- 0.00945
       38/1    1.45114    1.40329 +/- 0.00928
       39/1    1.42619    1.40408 +/- 0.00899
       40/1    1.35249    1.40236 +/- 0.00885
       41/1    1.35401    1.40080 +/- 0.00870
       42/1    1.40220    1.40084 +/- 0.00842
       43/1    1.36437    1.39974 +/- 0.00824
       44/1    1.33642    1.39787 +/- 0.00821
       45/1    1.36953    1.39706 +/- 0.00801
       46/1    1.30034    1.39438 +/- 0.00824
       47/1    1.44097    1.39564 +/- 0.00811
       48/1    1.37981    1.39522 +/- 0.00790
       49/1    1.34870    1.39403 +/- 0.00779
       50/1    1.41247    1.39449 +/- 0.00761
       51/1    1.33382    1.39301 +/- 0.00756
       52/1    1.37043    1.39247 +/- 0.00740
       53/1    1.38754    1.39236 +/- 0.00723
       54/1    1.40160    1.39257 +/- 0.00707
       55/1    1.37511    1.39218 +/- 0.00692
       56/1    1.38589    1.39204 +/- 0.00677
       57/1    1.40630    1.39234 +/- 0.00663
       58/1    1.29944    1.39041 +/- 0.00677
       59/1    1.40019    1.39061 +/- 0.00663
       60/1    1.42384    1.39127 +/- 0.00653
       61/1    1.36502    1.39076 +/- 0.00643
       62/1    1.37042    1.39037 +/- 0.00631
       63/1    1.42295    1.39098 +/- 0.00622
       64/1    1.40042    1.39116 +/- 0.00611
       65/1    1.36382    1.39066 +/- 0.00602
       66/1    1.31659    1.38934 +/- 0.00606
       67/1    1.36101    1.38884 +/- 0.00597
       68/1    1.46359    1.39013 +/- 0.00601
       69/1    1.41012    1.39047 +/- 0.00591
       70/1    1.27411    1.38853 +/- 0.00613
       71/1    1.45399    1.38960 +/- 0.00612
       72/1    1.40455    1.38984 +/- 0.00603
       73/1    1.33020    1.38890 +/- 0.00601
       74/1    1.44599    1.38979 +/- 0.00598
       75/1    1.34985    1.38917 +/- 0.00592
       76/1    1.36183    1.38876 +/- 0.00584
       77/1    1.41080    1.38909 +/- 0.00576
       78/1    1.43991    1.38984 +/- 0.00573
       79/1    1.35613    1.38935 +/- 0.00566
       80/1    1.31659    1.38831 +/- 0.00568
       81/1    1.51344    1.39007 +/- 0.00587
       82/1    1.38404    1.38999 +/- 0.00579
       83/1    1.39613    1.39007 +/- 0.00571
       84/1    1.43037    1.39061 +/- 0.00566
       85/1    1.47316    1.39172 +/- 0.00569
       86/1    1.39220    1.39172 +/- 0.00561
       87/1    1.44400    1.39240 +/- 0.00558
       88/1    1.42419    1.39281 +/- 0.00552
       89/1    1.30930    1.39175 +/- 0.00556
       90/1    1.46976    1.39273 +/- 0.00557
       91/1    1.38334    1.39261 +/- 0.00550
       92/1    1.35260    1.39212 +/- 0.00546
       93/1    1.38505    1.39204 +/- 0.00539
       94/1    1.38290    1.39193 +/- 0.00533
       95/1    1.42597    1.39233 +/- 0.00528
       96/1    1.41624    1.39261 +/- 0.00523
       97/1    1.42053    1.39293 +/- 0.00518
       98/1    1.36268    1.39258 +/- 0.00513
       99/1    1.39175    1.39258 +/- 0.00507
      100/1    1.38148    1.39245 +/- 0.00502
 Creating state point statepoint.100.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 6.9022e-01 seconds
   Reading cross sections          = 6.7913e-01 seconds
 Total time in simulation          = 1.7892e+00 seconds
   Time in transport only          = 1.7650e+00 seconds
   Time in inactive batches        = 1.5005e-01 seconds
   Time in active batches          = 1.6391e+00 seconds
   Time synchronizing fission bank = 4.2308e-03 seconds
     Sampling source sites         = 3.4593e-03 seconds
     SEND/RECV source sites        = 6.2601e-04 seconds
   Time accumulating tallies       = 9.5555e-05 seconds
 Total time for finalization       = 7.4948e-05 seconds
 Total time elapsed                = 2.4836e+00 seconds
 Calculation Rate (inactive)       = 66645.8 particles/second
 Calculation Rate (active)         = 54907.5 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.39516 +/- 0.00457
 k-effective (Track-length)  = 1.39245 +/- 0.00502
 k-effective (Absorption)    = 1.40443 +/- 0.00333
 Combined k-effective        = 1.40145 +/- 0.00319
 Leakage Fraction            = 0.00000 +/- 0.00000

Great! OpenMC already told us our k-effective. It also spit out a file called tallies.out that shows our tallies. This is a very basic method to look at tally data; for more sophisticated methods, see other example notebooks.

[42]:
!cat tallies.out
 ============================>     TALLY 1     <============================

 Cell 3
   U235
     Total Reaction Rate                  0.726151 +/- 0.00251702
     Fission Rate                         0.543836 +/- 0.00205084
     Absorption Rate                      0.652874 +/- 0.002424
     (n,gamma)                            0.10904 +/- 0.000385793
Geometry plotting

We saw before that we could call the Universe.plot() method to show a universe while we were creating our geometry. There is also a built-in plotter in the codebase that is much faster than the Python plotter and has more options. The interface looks somewhat similar to the Universe.plot() method. Instead though, we create Plot instances, assign them to a Plots collection, export it to XML, and then run OpenMC in geometry plotting mode. As an example, let’s specify that we want the plot to be colored by material (rather than by cell) and we assign yellow to fuel and blue to water.

[43]:
plot = openmc.Plot()
plot.filename = 'pinplot'
plot.width = (pitch, pitch)
plot.pixels = (200, 200)
plot.color_by = 'material'
plot.colors = {uo2: 'yellow', water: 'blue'}

With our plot created, we need to add it to a Plots collection which can be exported to XML.

[44]:
plots = openmc.Plots([plot])
plots.export_to_xml()
!cat plots.xml
<?xml version='1.0' encoding='utf-8'?>
<plots>
  <plot basis="xy" color_by="material" filename="pinplot" id="1" type="slice">
    <origin>0.0 0.0 0.0</origin>
    <width>1.26 1.26</width>
    <pixels>200 200</pixels>
    <color id="1" rgb="255 255 0" />
    <color id="4" rgb="0 0 255" />
  </plot>
</plots>

Now we can run OpenMC in plotting mode by calling the plot_geometry() function. Under the hood this is calling openmc --plot.

[45]:
openmc.plot_geometry()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2020 MIT and OpenMC contributors
           License | https://docs.openmc.org/en/latest/license.html
           Version | 0.12.0
          Git SHA1 | 3d90a9f857ec72eae897e054d4225180f1fa4d93
         Date/Time | 2020-08-25 14:58:54
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading tallies XML file...
 Preparing distributed cell instances...
 Reading plot XML file...

 =======================>     PLOTTING SUMMARY     <========================

Plot ID: 1
Plot file: pinplot.ppm
Universe depth: -1
Plot Type: Slice
Origin: 0.0 0.0 0.0
Width: 1.26 1.26
Coloring: Materials
Basis: XY
Pixels: 200 200

 Processing plot 1: pinplot.ppm...

OpenMC writes out a peculiar image with a .ppm extension. If you have ImageMagick installed, this can be converted into a more normal .png file.

[46]:
!convert pinplot.ppm pinplot.png

We can use functionality from IPython to display the image inline in our notebook:

[47]:
from IPython.display import Image
Image("pinplot.png")
[47]:
_images/examples_pincell_95_0.png

That was a little bit cumbersome. Thankfully, OpenMC provides us with a method on the Plot class that does all that “boilerplate” work.

[48]:
plot.to_ipython_image()
[48]:
_images/examples_pincell_97_0.png

Post Processing

This notebook demonstrates some basic post-processing tasks that can be performed with the Python API, such as plotting a 2D mesh tally and plotting neutron source sites from an eigenvalue calculation. The problem we will use is a simple reflected pin-cell.

[1]:
%matplotlib inline
from IPython.display import Image
import numpy as np
import matplotlib.pyplot as plt
import openmc
Generate Input Files

First we need to define materials that will be used in the problem. We’ll create three materials for the fuel, water, and cladding of the fuel pin.

[2]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)
water.add_nuclide('B10', 8.0042e-6)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our three materials, we can now create a materials file object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials collection
materials = openmc.Materials([fuel, water, zircaloy])

# Export to "materials.xml"
materials.export_to_xml()

Now let’s move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces – in this case two cylinders and six reflective planes.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.45720)

# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-0.63, boundary_type='reflective')
max_x = openmc.XPlane(x0=+0.63, boundary_type='reflective')
min_y = openmc.YPlane(y0=-0.63, boundary_type='reflective')
max_y = openmc.YPlane(y0=+0.63, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-0.63, boundary_type='reflective')
max_z = openmc.ZPlane(z0=+0.63, boundary_type='reflective')

With the surfaces defined, we can now create cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
pin_cell_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
pin_cell_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
pin_cell_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
pin_cell_universe.add_cell(moderator_cell)

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.

[6]:
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = pin_cell_universe

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(root_cell)

We now must create a geometry that is assigned a root universe, put the geometry into a geometry file, and export it to XML.

[7]:
# Create Geometry and set root Universe
geometry = openmc.Geometry(root_universe)
[8]:
# Export to "geometry.xml"
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 10 inactive batches and 90 active batches each with 5000 particles.

[9]:
# OpenMC simulation parameters
settings = openmc.Settings()
settings.batches = 100
settings.inactive = 10
settings.particles = 5000

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings.export_to_xml()

Let us also create a plot file that we can use to verify that our pin cell geometry was created successfully.

[10]:
plot = openmc.Plot.from_geometry(geometry)
plot.pixels = (250, 250)
plot.to_ipython_image()
[10]:
_images/examples_post-processing_19_0.png

As we can see from the plot, we have a nice pin cell with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a 2D mesh tally.

[11]:
# Instantiate an empty Tallies object
tallies = openmc.Tallies()
[12]:
# Create mesh which will be used for tally
mesh = openmc.RegularMesh()
mesh.dimension = [100, 100]
mesh.lower_left = [-0.63, -0.63]
mesh.upper_right = [0.63, 0.63]

# Create mesh filter for tally
mesh_filter = openmc.MeshFilter(mesh)

# Create mesh tally to score flux and fission rate
tally = openmc.Tally(name='flux')
tally.filters = [mesh_filter]
tally.scores = ['flux', 'fission']
tallies.append(tally)
[13]:
# Export to "tallies.xml"
tallies.export_to_xml()

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[14]:
# Run OpenMC!
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-19 06:22:24
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading B10 from /opt/data/hdf5/nndc_hdf5_v15/B10.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.04359
        2/1    1.04323
        3/1    1.04711
        4/1    1.03892
        5/1    1.02459
        6/1    1.03936
        7/1    1.03529
        8/1    1.01590
        9/1    1.03060
       10/1    1.02892
       11/1    1.03987
       12/1    1.04395    1.04191 +/- 0.00204
       13/1    1.04971    1.04451 +/- 0.00285
       14/1    1.03880    1.04308 +/- 0.00247
       15/1    1.03091    1.04065 +/- 0.00310
       16/1    1.03618    1.03990 +/- 0.00264
       17/1    1.04109    1.04007 +/- 0.00223
       18/1    1.02978    1.03879 +/- 0.00232
       19/1    1.06363    1.04155 +/- 0.00344
       20/1    1.06549    1.04394 +/- 0.00390
       21/1    1.03469    1.04310 +/- 0.00362
       22/1    1.01925    1.04111 +/- 0.00386
       23/1    1.03268    1.04046 +/- 0.00361
       24/1    1.03906    1.04036 +/- 0.00334
       25/1    1.02632    1.03943 +/- 0.00325
       26/1    1.03906    1.03940 +/- 0.00304
       27/1    1.05058    1.04006 +/- 0.00293
       28/1    1.03248    1.03964 +/- 0.00279
       29/1    1.04076    1.03970 +/- 0.00264
       30/1    1.00994    1.03821 +/- 0.00292
       31/1    1.04785    1.03867 +/- 0.00281
       32/1    1.03080    1.03831 +/- 0.00270
       33/1    1.01862    1.03746 +/- 0.00272
       34/1    1.05370    1.03813 +/- 0.00269
       35/1    1.02226    1.03750 +/- 0.00266
       36/1    1.02862    1.03716 +/- 0.00258
       37/1    1.04790    1.03755 +/- 0.00251
       38/1    1.03762    1.03756 +/- 0.00242
       39/1    1.02255    1.03704 +/- 0.00239
       40/1    1.06094    1.03784 +/- 0.00245
       41/1    1.03842    1.03786 +/- 0.00237
       42/1    1.00628    1.03687 +/- 0.00249
       43/1    1.04916    1.03724 +/- 0.00245
       44/1    1.06237    1.03798 +/- 0.00248
       45/1    1.08153    1.03922 +/- 0.00271
       46/1    1.05649    1.03970 +/- 0.00268
       47/1    1.06265    1.04032 +/- 0.00268
       48/1    1.05728    1.04077 +/- 0.00265
       49/1    1.07343    1.04161 +/- 0.00271
       50/1    1.04640    1.04173 +/- 0.00265
       51/1    1.05143    1.04196 +/- 0.00259
       52/1    1.03639    1.04183 +/- 0.00253
       53/1    1.04846    1.04199 +/- 0.00248
       54/1    1.02435    1.04158 +/- 0.00245
       55/1    1.04806    1.04173 +/- 0.00240
       56/1    1.04798    1.04186 +/- 0.00235
       57/1    1.06621    1.04238 +/- 0.00236
       58/1    1.05734    1.04269 +/- 0.00233
       59/1    1.04581    1.04276 +/- 0.00228
       60/1    1.02682    1.04244 +/- 0.00226
       61/1    1.05971    1.04278 +/- 0.00224
       62/1    1.02357    1.04241 +/- 0.00223
       63/1    1.02645    1.04211 +/- 0.00221
       64/1    1.00711    1.04146 +/- 0.00226
       65/1    1.06171    1.04183 +/- 0.00225
       66/1    1.03444    1.04170 +/- 0.00221
       67/1    1.05875    1.04199 +/- 0.00219
       68/1    1.04640    1.04207 +/- 0.00216
       69/1    1.04376    1.04210 +/- 0.00212
       70/1    1.07078    1.04258 +/- 0.00214
       71/1    1.03916    1.04252 +/- 0.00210
       72/1    1.01843    1.04213 +/- 0.00211
       73/1    1.03666    1.04205 +/- 0.00207
       74/1    1.04625    1.04211 +/- 0.00204
       75/1    1.05277    1.04228 +/- 0.00202
       76/1    1.04944    1.04238 +/- 0.00199
       77/1    1.01898    1.04203 +/- 0.00199
       78/1    1.03283    1.04190 +/- 0.00197
       79/1    1.02304    1.04163 +/- 0.00196
       80/1    1.01539    1.04125 +/- 0.00196
       81/1    1.03988    1.04123 +/- 0.00194
       82/1    1.02138    1.04096 +/- 0.00193
       83/1    1.02473    1.04073 +/- 0.00192
       84/1    1.03810    1.04070 +/- 0.00189
       85/1    1.07438    1.04115 +/- 0.00192
       86/1    1.03048    1.04101 +/- 0.00190
       87/1    1.06778    1.04135 +/- 0.00191
       88/1    1.07341    1.04177 +/- 0.00192
       89/1    1.06729    1.04209 +/- 0.00193
       90/1    1.05069    1.04220 +/- 0.00191
       91/1    1.07675    1.04262 +/- 0.00193
       92/1    1.06470    1.04289 +/- 0.00193
       93/1    1.02609    1.04269 +/- 0.00191
       94/1    1.04761    1.04275 +/- 0.00189
       95/1    1.08802    1.04328 +/- 0.00194
       96/1    1.04162    1.04326 +/- 0.00192
       97/1    1.04573    1.04329 +/- 0.00190
       98/1    1.03232    1.04317 +/- 0.00188
       99/1    1.03473    1.04307 +/- 0.00186
      100/1    1.04505    1.04309 +/- 0.00184
 Creating state point statepoint.100.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 6.4445e-01 seconds
   Reading cross sections          = 6.1129e-01 seconds
 Total time in simulation          = 2.0000e+02 seconds
   Time in transport only          = 1.9970e+02 seconds
   Time in inactive batches        = 2.9966e+00 seconds
   Time in active batches          = 1.9701e+02 seconds
   Time synchronizing fission bank = 4.0040e-02 seconds
     Sampling source sites         = 3.1522e-02 seconds
     SEND/RECV source sites        = 8.3459e-03 seconds
   Time accumulating tallies       = 9.3582e-03 seconds
 Total time for finalization       = 4.6582e-02 seconds
 Total time elapsed                = 2.0072e+02 seconds
 Calculation Rate (inactive)       = 16685.4 particles/second
 Calculation Rate (active)         = 2284.19 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.04342 +/- 0.00159
 k-effective (Track-length)  = 1.04309 +/- 0.00184
 k-effective (Absorption)    = 1.04107 +/- 0.00140
 Combined k-effective        = 1.04195 +/- 0.00117
 Leakage Fraction            = 0.00000 +/- 0.00000

Tally Data Processing

Our simulation ran successfully and created a statepoint file with all the tally data in it. We begin our analysis here loading the statepoint file and ‘reading’ the results. By default, data from the statepoint file is only read into memory when it is requested. This helps keep the memory use to a minimum even when a statepoint file may be huge.

[15]:
# Load the statepoint file
sp = openmc.StatePoint('statepoint.100.h5')

Next we need to get the tally, which can be done with the StatePoint.get_tally(...) method.

[16]:
tally = sp.get_tally(scores=['flux'])
print(tally)
Tally
        ID             =        1
        Name           =        flux
        Filters        =        MeshFilter
        Nuclides       =        total
        Scores         =        ['flux', 'fission']
        Estimator      =        tracklength

The statepoint file actually stores the sum and sum-of-squares for each tally bin from which the mean and variance can be calculated as described here. The sum and sum-of-squares can be accessed using the sum and sum_sq properties:

[17]:
tally.sum
[17]:
array([[[0.40767451, 0.        ]],

       [[0.40933814, 0.        ]],

       [[0.4119165 , 0.        ]],

       ...,

       [[0.40854327, 0.        ]],

       [[0.40970805, 0.        ]],

       [[0.40948065, 0.        ]]])

However, the mean and standard deviation of the mean are usually what you are more interested in. The Tally class also has properties mean and std_dev which automatically calculate these statistics on-the-fly.

[18]:
print(tally.mean.shape)
(tally.mean, tally.std_dev)
(10000, 1, 2)
[18]:
(array([[[0.00452972, 0.        ]],

        [[0.0045482 , 0.        ]],

        [[0.00457685, 0.        ]],

        ...,

        [[0.00453937, 0.        ]],

        [[0.00455231, 0.        ]],

        [[0.00454978, 0.        ]]]),
 array([[[2.03553236e-05, 0.00000000e+00]],

        [[1.83847389e-05, 0.00000000e+00]],

        [[1.68647098e-05, 0.00000000e+00]],

        ...,

        [[1.71606078e-05, 0.00000000e+00]],

        [[1.87645811e-05, 0.00000000e+00]],

        [[1.94447454e-05, 0.00000000e+00]]]))

The tally data has three dimensions: one for filter combinations, one for nuclides, and one for scores. We see that there are 10000 filter combinations (corresponding to the 100 x 100 mesh bins), a single nuclide (since none was specified), and two scores. If we only want to look at a single score, we can use the get_slice(...) method as follows.

[19]:
flux = tally.get_slice(scores=['flux'])
fission = tally.get_slice(scores=['fission'])
print(flux)
Tally
        ID             =        2
        Name           =        flux
        Filters        =        MeshFilter
        Nuclides       =        total
        Scores         =        ['flux']
        Estimator      =        tracklength

To get the bins into a form that we can plot, we can simply change the shape of the array since it is a numpy array.

[20]:
flux.std_dev.shape = (100, 100)
flux.mean.shape = (100, 100)
fission.std_dev.shape = (100, 100)
fission.mean.shape = (100, 100)
[21]:
fig = plt.subplot(121)
fig.imshow(flux.mean)
fig2 = plt.subplot(122)
fig2.imshow(fission.mean)
[21]:
<matplotlib.image.AxesImage at 0x14d12e58cb38>
_images/examples_post-processing_39_1.png

Now let’s say we want to look at the distribution of relative errors of our tally bins for flux. First we create a new variable called relative_error and set it to the ratio of the standard deviation and the mean, being careful not to divide by zero in case some bins were never scored to.

[22]:
# Determine relative error
relative_error = np.zeros_like(flux.std_dev)
nonzero = flux.mean > 0
relative_error[nonzero] = flux.std_dev[nonzero] / flux.mean[nonzero]

# distribution of relative errors
ret = plt.hist(relative_error[nonzero], bins=50)
_images/examples_post-processing_41_0.png
Source Sites

Source sites can be accessed from the source property. As shown below, the source sites are represented as a numpy array with a structured datatype.

[23]:
sp.source
[23]:
array([((-0.28690552, -0.23731283,  0.51447853), ( 0.02705364, -0.14292142,  0.98936422), 1780128.70101981, 1., 0, 0),
       ((-0.28690552, -0.23731283,  0.51447853), (-0.16786951,  0.86432444, -0.47409186), 1553436.10501094, 1., 0, 0),
       (( 0.17162994,  0.134092  ,  0.42932363), ( 0.25199134, -0.11168216,  0.96126347),  829530.02360943, 1., 0, 0),
       ...,
       ((-0.24444068, -0.01351615, -0.41772172), ( 0.10437178, -0.86754673,  0.486281  ),  807617.55637656, 1., 0, 0),
       ((-0.2146841 ,  0.14307096,  0.07419328), ( 0.89645066, -0.35557279, -0.26446968), 6036005.44157462, 1., 0, 0),
       ((-0.2146841 ,  0.14307096,  0.07419328), (-0.95287644, -0.25857878,  0.15863005), 4923751.04163063, 1., 0, 0)],
      dtype=[('r', [('x', '<f8'), ('y', '<f8'), ('z', '<f8')]), ('u', [('x', '<f8'), ('y', '<f8'), ('z', '<f8')]), ('E', '<f8'), ('wgt', '<f8'), ('delayed_group', '<i4'), ('particle', '<i4')])

If we want, say, only the energies from the source sites, we can simply index the source array with the name of the field:

[24]:
sp.source['E']
[24]:
array([1780128.70101981, 1553436.10501094,  829530.02360943, ...,
        807617.55637656, 6036005.44157462, 4923751.04163063])

Now, we can look at things like the energy distribution of source sites. Note that we don’t directly use the matplotlib.pyplot.hist method since our binning is logarithmic.

[25]:
# Create log-spaced energy bins from 1 keV to 10 MeV
energy_bins = np.logspace(3,7)

# Calculate pdf for source energies
probability, bin_edges = np.histogram(sp.source['E'], energy_bins, density=True)

# Make sure integrating the PDF gives us unity
print(sum(probability*np.diff(energy_bins)))

# Plot source energy PDF
plt.semilogx(energy_bins[:-1], probability*np.diff(energy_bins), drawstyle='steps')
plt.xlabel('Energy (eV)')
plt.ylabel('Probability/eV')
0.9999999999999999
[25]:
Text(0, 0.5, 'Probability/eV')
_images/examples_post-processing_48_2.png

Let’s also look at the spatial distribution of the sites. To make the plot a little more interesting, we can also include the direction of the particle emitted from the source and color each source by the logarithm of its energy.

[26]:
plt.quiver(sp.source['r']['x'], sp.source['r']['y'],
           sp.source['u']['x'], sp.source['u']['y'],
           np.log(sp.source['E']), cmap='jet', scale=20.0)
plt.colorbar()
plt.xlim((-0.5,0.5))
plt.ylim((-0.5,0.5))
[26]:
(-0.5, 0.5)
_images/examples_post-processing_50_1.png

Pandas Dataframes

This notebook demonstrates how systematic analysis of tally scores is possible using Pandas dataframes. A dataframe can be automatically generated using the Tally.get_pandas_dataframe(...) method. Furthermore, by linking the tally data in a statepoint file with geometry and material information from a summary file, the dataframe can be shown with user-supplied labels.

[1]:
import glob
from IPython.display import Image
import matplotlib.pyplot as plt
import scipy.stats
import numpy as np
import pandas as pd
import openmc
%matplotlib inline
Generate Input Files

First we need to define materials that will be used in the problem. We will create three materials for the fuel, water, and cladding of the fuel pin.

[2]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)
water.add_nuclide('B10', 8.0042e-6)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our three materials, we can now create a materials file object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials collection
materials = openmc.Materials([fuel, water, zircaloy])

# Export to "materials.xml"
materials.export_to_xml()

Now let’s move on to the geometry. This problem will be a square array of fuel pins for which we can use OpenMC’s lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.45720)

# Create boundary planes to surround the geometry
# Use both reflective and vacuum boundaries to make life interesting
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='vacuum')
min_y = openmc.YPlane(y0=-10.71, boundary_type='vacuum')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10.71, boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10.71, boundary_type='reflective')

With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel', fill=fuel,
                        region=-fuel_outer_radius)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad', fill=zircaloy)
clad_cell.region = +fuel_outer_radius & -clad_outer_radius

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator', fill=water,
                             region=+clad_outer_radius)

# Create a Universe to encapsulate a fuel pin
pin_cell_universe = openmc.Universe(name='1.6% Fuel Pin', cells=[
    fuel_cell, clad_cell, moderator_cell
])

Using the pin cell universe, we can construct a 17x17 rectangular lattice with a 1.26 cm pitch.

[6]:
# Create fuel assembly Lattice
assembly = openmc.RectLattice(name='1.6% Fuel - 0BA')
assembly.pitch = (1.26, 1.26)
assembly.lower_left = [-1.26 * 17. / 2.0] * 2
assembly.universes = [[pin_cell_universe] * 17] * 17

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.

[7]:
# Create root Cell
root_cell = openmc.Cell(name='root cell', fill=assembly)

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(name='root universe')
root_universe.add_cell(root_cell)

We now must create a geometry that is assigned a root universe and export it to XML.

[8]:
# Create Geometry and export to "geometry.xml"
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 5 inactive batches and 15 minimum active batches each with 2500 particles. We also tell OpenMC to turn tally triggers on, which means it will keep running until some criterion on the uncertainty of tallies is reached.

[9]:
# OpenMC simulation parameters
min_batches = 20
max_batches = 200
inactive = 5
particles = 2500

# Instantiate a Settings object
settings = openmc.Settings()
settings.batches = min_batches
settings.inactive = inactive
settings.particles = particles
settings.output = {'tallies': False}
settings.trigger_active = True
settings.trigger_max_batches = max_batches

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings.export_to_xml()

Let us also create a plot file that we can use to verify that our pin cell geometry was created successfully.

[10]:
# Instantiate a Plot
plot = openmc.Plot(plot_id=1)
plot.filename = 'materials-xy'
plot.origin = [0, 0, 0]
plot.width = [21.5, 21.5]
plot.pixels = [250, 250]
plot.color_by = 'material'

# Show plot
openmc.plot_inline(plot)
_images/examples_pandas-dataframes_20_0.png

As we can see from the plot, we have a nice array of pin cells with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a variety of tallies.

[11]:
# Instantiate an empty Tallies object
tallies = openmc.Tallies()

Instantiate a fission rate mesh Tally

[12]:
# Instantiate a tally Mesh
mesh = openmc.RegularMesh(mesh_id=1)
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.width = [1.26, 1.26]

# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)

# Instantiate energy Filter
energy_filter = openmc.EnergyFilter([0, 0.625, 20.0e6])

# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['fission', 'nu-fission']

# Add mesh and Tally to Tallies
tallies.append(tally)

Instantiate a cell Tally with nuclides

[13]:
# Instantiate tally Filter
cell_filter = openmc.CellFilter(fuel_cell)

# Instantiate the tally
tally = openmc.Tally(name='cell tally')
tally.filters = [cell_filter]
tally.scores = ['scatter']
tally.nuclides = ['U235', 'U238']

# Add mesh and tally to Tallies
tallies.append(tally)

Create a “distribcell” Tally. The distribcell filter allows us to tally multiple repeated instances of the same cell throughout the geometry.

[14]:
# Instantiate tally Filter
distribcell_filter = openmc.DistribcellFilter(moderator_cell)

# Instantiate tally Trigger for kicks
trigger = openmc.Trigger(trigger_type='std_dev', threshold=5e-5)
trigger.scores = ['absorption']

# Instantiate the Tally
tally = openmc.Tally(name='distribcell tally')
tally.filters = [distribcell_filter]
tally.scores = ['absorption', 'scatter']
tally.triggers = [trigger]

# Add mesh and tally to Tallies
tallies.append(tally)
[15]:
# Export to "tallies.xml"
tallies.export_to_xml()

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[16]:
# Remove old HDF5 (summary, statepoint) files
!rm statepoint.*

# Run OpenMC!
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2020 MIT and OpenMC contributors
           License | https://docs.openmc.org/en/latest/license.html
           Version | 0.12.0
          Git SHA1 | 3d90a9f857ec72eae897e054d4225180f1fa4d93
         Date/Time | 2020-08-15 07:10:20
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /home/master/data/nuclear/endfb71_hdf5/U235.h5
 Reading U238 from /home/master/data/nuclear/endfb71_hdf5/U238.h5
 Reading O16 from /home/master/data/nuclear/endfb71_hdf5/O16.h5
 Reading H1 from /home/master/data/nuclear/endfb71_hdf5/H1.h5
 Reading B10 from /home/master/data/nuclear/endfb71_hdf5/B10.h5
 Reading Zr90 from /home/master/data/nuclear/endfb71_hdf5/Zr90.h5
 Minimum neutron data temperature: 294.000000 K
 Maximum neutron data temperature: 294.000000 K
 Reading tallies XML file...
 Preparing distributed cell instances...
 Writing summary.h5 file...
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    0.53544
        2/1    0.62631
        3/1    0.63917
        4/1    0.67203
        5/1    0.69300
        6/1    0.64862
        7/1    0.63937    0.64399 +/- 0.00463
        8/1    0.67696    0.65498 +/- 0.01131
        9/1    0.63216    0.64928 +/- 0.00982
       10/1    0.70996    0.66141 +/- 0.01433
       11/1    0.69761    0.66745 +/- 0.01316
       12/1    0.68662    0.67019 +/- 0.01146
       13/1    0.64374    0.66688 +/- 0.01046
       14/1    0.69121    0.66958 +/- 0.00961
       15/1    0.72125    0.67475 +/- 0.01003
       16/1    0.72706    0.67950 +/- 0.01024
       17/1    0.69623    0.68090 +/- 0.00945
       18/1    0.70953    0.68310 +/- 0.00897
       19/1    0.69026    0.68361 +/- 0.00832
       20/1    0.68633    0.68379 +/- 0.00775
 Triggers unsatisfied, max unc./thresh. is 75.24758750489383 for absorption in
 tally 3
 WARNING: The estimated number of batches is 84938 --- greater than max batches
 Creating state point statepoint.020.h5...
       21/1    0.68310    0.68375 +/- 0.00725
 Triggers unsatisfied, max unc./thresh. is 71.20148627325992 for absorption in
 tally 3
 WARNING: The estimated number of batches is 81120 --- greater than max batches
       22/1    0.68679    0.68393 +/- 0.00681
 Triggers unsatisfied, max unc./thresh. is 66.94650483064697 for absorption in
 tally 3
 WARNING: The estimated number of batches is 76197 --- greater than max batches
       23/1    0.67440    0.68340 +/- 0.00644
 Triggers unsatisfied, max unc./thresh. is 63.553590826021285 for absorption in
 tally 3
 WARNING: The estimated number of batches is 72709 --- greater than max batches
       24/1    0.67483    0.68295 +/- 0.00611
 Triggers unsatisfied, max unc./thresh. is 60.37873858685279 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69272 --- greater than max batches
       25/1    0.71558    0.68458 +/- 0.00602
 Triggers unsatisfied, max unc./thresh. is 60.34535216026281 for absorption in
 tally 3
 WARNING: The estimated number of batches is 72837 --- greater than max batches
       26/1    0.71853    0.68620 +/- 0.00595
 Triggers unsatisfied, max unc./thresh. is 59.60875760463032 for absorption in
 tally 3
 WARNING: The estimated number of batches is 74623 --- greater than max batches
       27/1    0.67455    0.68567 +/- 0.00570
 Triggers unsatisfied, max unc./thresh. is 57.228951643423976 for absorption in
 tally 3
 WARNING: The estimated number of batches is 72059 --- greater than max batches
       28/1    0.69435    0.68605 +/- 0.00546
 Triggers unsatisfied, max unc./thresh. is 56.194065573871285 for absorption in
 tally 3
 WARNING: The estimated number of batches is 72634 --- greater than max batches
       29/1    0.67706    0.68567 +/- 0.00524
 Triggers unsatisfied, max unc./thresh. is 53.86022066923874 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69628 --- greater than max batches
       30/1    0.69294    0.68596 +/- 0.00504
 Triggers unsatisfied, max unc./thresh. is 51.73413206858763 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66916 --- greater than max batches
       31/1    0.69108    0.68616 +/- 0.00484
 Triggers unsatisfied, max unc./thresh. is 49.71174462484801 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64258 --- greater than max batches
       32/1    0.68089    0.68596 +/- 0.00466
 Triggers unsatisfied, max unc./thresh. is 50.80993117627794 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69710 --- greater than max batches
       33/1    0.67698    0.68564 +/- 0.00450
 Triggers unsatisfied, max unc./thresh. is 50.46659333785448 for absorption in
 tally 3
 WARNING: The estimated number of batches is 71318 --- greater than max batches
       34/1    0.68167    0.68551 +/- 0.00435
 Triggers unsatisfied, max unc./thresh. is 48.852656603250665 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69216 --- greater than max batches
       35/1    0.67760    0.68524 +/- 0.00421
 Triggers unsatisfied, max unc./thresh. is 48.5685583427197 for absorption in
 tally 3
 WARNING: The estimated number of batches is 70773 --- greater than max batches
       36/1    0.67628    0.68495 +/- 0.00408
 Triggers unsatisfied, max unc./thresh. is 47.77661998216646 for absorption in
 tally 3
 WARNING: The estimated number of batches is 70766 --- greater than max batches
       37/1    0.66736    0.68440 +/- 0.00399
 Triggers unsatisfied, max unc./thresh. is 46.57810773879176 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69430 --- greater than max batches
       38/1    0.71026    0.68519 +/- 0.00395
 Triggers unsatisfied, max unc./thresh. is 45.876107560616674 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69458 --- greater than max batches
       39/1    0.67674    0.68494 +/- 0.00384
 Triggers unsatisfied, max unc./thresh. is 45.30341918376721 for absorption in
 tally 3
 WARNING: The estimated number of batches is 69787 --- greater than max batches
       40/1    0.69360    0.68519 +/- 0.00373
 Triggers unsatisfied, max unc./thresh. is 44.018056562863386 for absorption in
 tally 3
 WARNING: The estimated number of batches is 67821 --- greater than max batches
       41/1    0.70987    0.68587 +/- 0.00369
 Triggers unsatisfied, max unc./thresh. is 42.781078099052785 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65893 --- greater than max batches
       42/1    0.68780    0.68592 +/- 0.00359
 Triggers unsatisfied, max unc./thresh. is 41.60877069209228 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64063 --- greater than max batches
       43/1    0.69223    0.68609 +/- 0.00350
 Triggers unsatisfied, max unc./thresh. is 42.44932759168626 for absorption in
 tally 3
 WARNING: The estimated number of batches is 68479 --- greater than max batches
       44/1    0.69561    0.68633 +/- 0.00342
 Triggers unsatisfied, max unc./thresh. is 41.34743776753899 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66680 --- greater than max batches
       45/1    0.67503    0.68605 +/- 0.00334
 Triggers unsatisfied, max unc./thresh. is 40.97332186124358 for absorption in
 tally 3
 WARNING: The estimated number of batches is 67158 --- greater than max batches
       46/1    0.67290    0.68573 +/- 0.00328
 Triggers unsatisfied, max unc./thresh. is 40.24678448931756 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66417 --- greater than max batches
       47/1    0.67355    0.68544 +/- 0.00321
 Triggers unsatisfied, max unc./thresh. is 40.42620640829592 for absorption in
 tally 3
 WARNING: The estimated number of batches is 68645 --- greater than max batches
       48/1    0.71383    0.68610 +/- 0.00320
 Triggers unsatisfied, max unc./thresh. is 39.90662320308606 for absorption in
 tally 3
 WARNING: The estimated number of batches is 68485 --- greater than max batches
       49/1    0.68389    0.68605 +/- 0.00313
 Triggers unsatisfied, max unc./thresh. is 39.075369568753906 for absorption in
 tally 3
 WARNING: The estimated number of batches is 67188 --- greater than max batches
       50/1    0.73148    0.68706 +/- 0.00322
 Triggers unsatisfied, max unc./thresh. is 38.57054567218653 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66951 --- greater than max batches
       51/1    0.69796    0.68730 +/- 0.00316
 Triggers unsatisfied, max unc./thresh. is 38.21572703507316 for absorption in
 tally 3
 WARNING: The estimated number of batches is 67186 --- greater than max batches
       52/1    0.70691    0.68771 +/- 0.00312
 Triggers unsatisfied, max unc./thresh. is 37.50971908717773 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66134 --- greater than max batches
       53/1    0.69104    0.68778 +/- 0.00306
 Triggers unsatisfied, max unc./thresh. is 36.824732312223716 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65096 --- greater than max batches
       54/1    0.74368    0.68892 +/- 0.00320
 Triggers unsatisfied, max unc./thresh. is 36.20814737643575 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64246 --- greater than max batches
       55/1    0.67371    0.68862 +/- 0.00315
 Triggers unsatisfied, max unc./thresh. is 35.48607231512293 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62969 --- greater than max batches
       56/1    0.67846    0.68842 +/- 0.00310
 Triggers unsatisfied, max unc./thresh. is 35.34421893287461 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63715 --- greater than max batches
       57/1    0.66351    0.68794 +/- 0.00307
 Triggers unsatisfied, max unc./thresh. is 34.67062652878957 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62512 --- greater than max batches
       58/1    0.67049    0.68761 +/- 0.00303
 Triggers unsatisfied, max unc./thresh. is 34.22135922543247 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62074 --- greater than max batches
       59/1    0.66967    0.68728 +/- 0.00299
 Triggers unsatisfied, max unc./thresh. is 33.66881484408945 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61219 --- greater than max batches
       60/1    0.70271    0.68756 +/- 0.00295
 Triggers unsatisfied, max unc./thresh. is 33.19914799505717 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60626 --- greater than max batches
       61/1    0.70035    0.68779 +/- 0.00291
 Triggers unsatisfied, max unc./thresh. is 32.65594936729897 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59725 --- greater than max batches
       62/1    0.66274    0.68735 +/- 0.00289
 Triggers unsatisfied, max unc./thresh. is 32.15622046485561 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58945 --- greater than max batches
       63/1    0.68607    0.68733 +/- 0.00284
 Triggers unsatisfied, max unc./thresh. is 31.601225649282494 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57926 --- greater than max batches
       64/1    0.66518    0.68695 +/- 0.00282
 Triggers unsatisfied, max unc./thresh. is 31.12129365572805 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57149 --- greater than max batches
       65/1    0.65999    0.68650 +/- 0.00281
 Triggers unsatisfied, max unc./thresh. is 30.641988019531464 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56341 --- greater than max batches
       66/1    0.67843    0.68637 +/- 0.00276
 Triggers unsatisfied, max unc./thresh. is 30.320463443580458 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56085 --- greater than max batches
       67/1    0.69295    0.68648 +/- 0.00272
 Triggers unsatisfied, max unc./thresh. is 30.06051080038397 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56031 --- greater than max batches
       68/1    0.69158    0.68656 +/- 0.00268
 Triggers unsatisfied, max unc./thresh. is 29.7400907913873 for absorption in
 tally 3
 WARNING: The estimated number of batches is 55727 --- greater than max batches
       69/1    0.69825    0.68674 +/- 0.00264
 Triggers unsatisfied, max unc./thresh. is 29.278619659445805 for absorption in
 tally 3
 WARNING: The estimated number of batches is 54869 --- greater than max batches
       70/1    0.73637    0.68750 +/- 0.00271
 Triggers unsatisfied, max unc./thresh. is 28.945044018568716 for absorption in
 tally 3
 WARNING: The estimated number of batches is 54464 --- greater than max batches
       71/1    0.64301    0.68683 +/- 0.00275
 Triggers unsatisfied, max unc./thresh. is 28.73677928804667 for absorption in
 tally 3
 WARNING: The estimated number of batches is 54508 --- greater than max batches
       72/1    0.71506    0.68725 +/- 0.00274
 Triggers unsatisfied, max unc./thresh. is 29.20796537704291 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57164 --- greater than max batches
       73/1    0.69203    0.68732 +/- 0.00270
 Triggers unsatisfied, max unc./thresh. is 29.56297016014581 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59435 --- greater than max batches
       74/1    0.69208    0.68739 +/- 0.00267
 Triggers unsatisfied, max unc./thresh. is 29.545241442413783 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60237 --- greater than max batches
       75/1    0.65717    0.68696 +/- 0.00266
 Triggers unsatisfied, max unc./thresh. is 29.284013224166248 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60034 --- greater than max batches
       76/1    0.70992    0.68728 +/- 0.00265
 Triggers unsatisfied, max unc./thresh. is 29.00034584995327 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59718 --- greater than max batches
       77/1    0.65590    0.68685 +/- 0.00264
 Triggers unsatisfied, max unc./thresh. is 28.867967845905174 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60007 --- greater than max batches
       78/1    0.64439    0.68626 +/- 0.00267
 Triggers unsatisfied, max unc./thresh. is 29.016012595317935 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61466 --- greater than max batches
       79/1    0.66295    0.68595 +/- 0.00265
 Triggers unsatisfied, max unc./thresh. is 28.626245464278142 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60646 --- greater than max batches
       80/1    0.66672    0.68569 +/- 0.00263
 Triggers unsatisfied, max unc./thresh. is 28.24218910063624 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59827 --- greater than max batches
       81/1    0.69110    0.68576 +/- 0.00260
 Triggers unsatisfied, max unc./thresh. is 27.917349908027763 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59238 --- greater than max batches
       82/1    0.67481    0.68562 +/- 0.00257
 Triggers unsatisfied, max unc./thresh. is 28.01946018168837 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60457 --- greater than max batches
       83/1    0.72216    0.68609 +/- 0.00258
 Triggers unsatisfied, max unc./thresh. is 27.931394620754766 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60858 --- greater than max batches
       84/1    0.68429    0.68607 +/- 0.00254
 Triggers unsatisfied, max unc./thresh. is 27.713137470531738 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60679 --- greater than max batches
       85/1    0.65458    0.68567 +/- 0.00254
 Triggers unsatisfied, max unc./thresh. is 27.364539968246927 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59911 --- greater than max batches
       86/1    0.69966    0.68585 +/- 0.00252
 Triggers unsatisfied, max unc./thresh. is 27.178113974435043 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59836 --- greater than max batches
       87/1    0.64776    0.68538 +/- 0.00253
 Triggers unsatisfied, max unc./thresh. is 26.941566345072534 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59525 --- greater than max batches
       88/1    0.62737    0.68468 +/- 0.00259
 Triggers unsatisfied, max unc./thresh. is 26.73959660667411 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59351 --- greater than max batches
       89/1    0.69779    0.68484 +/- 0.00257
 Triggers unsatisfied, max unc./thresh. is 26.490234865810894 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58951 --- greater than max batches
       90/1    0.67312    0.68470 +/- 0.00254
 Triggers unsatisfied, max unc./thresh. is 26.24036001465229 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58533 --- greater than max batches
       91/1    0.69289    0.68480 +/- 0.00251
 Triggers unsatisfied, max unc./thresh. is 25.936795778335345 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57859 --- greater than max batches
       92/1    0.69884    0.68496 +/- 0.00249
 Triggers unsatisfied, max unc./thresh. is 25.695465963215582 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57448 --- greater than max batches
       93/1    0.71351    0.68528 +/- 0.00248
 Triggers unsatisfied, max unc./thresh. is 25.49001212499821 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57183 --- greater than max batches
       94/1    0.65602    0.68495 +/- 0.00248
 Triggers unsatisfied, max unc./thresh. is 25.350859463183905 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57203 --- greater than max batches
       95/1    0.72223    0.68537 +/- 0.00248
 Triggers unsatisfied, max unc./thresh. is 25.157803279393637 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56968 --- greater than max batches
       96/1    0.67930    0.68530 +/- 0.00246
 Triggers unsatisfied, max unc./thresh. is 24.92205849077747 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56526 --- greater than max batches
       97/1    0.66201    0.68505 +/- 0.00244
 Triggers unsatisfied, max unc./thresh. is 24.653967027285237 for absorption in
 tally 3
 WARNING: The estimated number of batches is 55925 --- greater than max batches
       98/1    0.71110    0.68533 +/- 0.00243
 Triggers unsatisfied, max unc./thresh. is 24.566957281211884 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56134 --- greater than max batches
       99/1    0.69409    0.68542 +/- 0.00241
 Triggers unsatisfied, max unc./thresh. is 24.581943149247135 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56807 --- greater than max batches
      100/1    0.71197    0.68570 +/- 0.00240
 Triggers unsatisfied, max unc./thresh. is 24.444112571967217 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56769 --- greater than max batches
      101/1    0.71713    0.68603 +/- 0.00240
 Triggers unsatisfied, max unc./thresh. is 24.18957776896016 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56179 --- greater than max batches
      102/1    0.68143    0.68598 +/- 0.00237
 Triggers unsatisfied, max unc./thresh. is 23.97797098066553 for absorption in
 tally 3
 WARNING: The estimated number of batches is 55775 --- greater than max batches
      103/1    0.69936    0.68612 +/- 0.00235
 Triggers unsatisfied, max unc./thresh. is 24.253000406602812 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57650 --- greater than max batches
      104/1    0.65247    0.68578 +/- 0.00235
 Triggers unsatisfied, max unc./thresh. is 24.593482483379837 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59885 --- greater than max batches
      105/1    0.66517    0.68557 +/- 0.00234
 Triggers unsatisfied, max unc./thresh. is 24.37904760701804 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59439 --- greater than max batches
      106/1    0.67814    0.68550 +/- 0.00232
 Triggers unsatisfied, max unc./thresh. is 24.142311084988883 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58873 --- greater than max batches
      107/1    0.67788    0.68542 +/- 0.00229
 Triggers unsatisfied, max unc./thresh. is 23.935477435724106 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58442 --- greater than max batches
      108/1    0.68016    0.68537 +/- 0.00227
 Triggers unsatisfied, max unc./thresh. is 24.532504688648594 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61995 --- greater than max batches
      109/1    0.66963    0.68522 +/- 0.00226
 Triggers unsatisfied, max unc./thresh. is 24.354532539671386 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61692 --- greater than max batches
      110/1    0.67556    0.68513 +/- 0.00224
 Triggers unsatisfied, max unc./thresh. is 24.16165322902175 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61303 --- greater than max batches
      111/1    0.68273    0.68511 +/- 0.00222
 Triggers unsatisfied, max unc./thresh. is 24.00069508298176 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61065 --- greater than max batches
      112/1    0.69505    0.68520 +/- 0.00220
 Triggers unsatisfied, max unc./thresh. is 23.791656279909404 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60572 --- greater than max batches
      113/1    0.69385    0.68528 +/- 0.00218
 Triggers unsatisfied, max unc./thresh. is 23.667941020219764 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60504 --- greater than max batches
      114/1    0.65352    0.68499 +/- 0.00218
 Triggers unsatisfied, max unc./thresh. is 23.469658485546123 for absorption in
 tally 3
 WARNING: The estimated number of batches is 60045 --- greater than max batches
      115/1    0.68339    0.68497 +/- 0.00216
 Triggers unsatisfied, max unc./thresh. is 23.259123161328624 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59514 --- greater than max batches
      116/1    0.65854    0.68474 +/- 0.00215
 Triggers unsatisfied, max unc./thresh. is 23.06250977653337 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59044 --- greater than max batches
      117/1    0.66907    0.68460 +/- 0.00214
 Triggers unsatisfied, max unc./thresh. is 22.874382198219536 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58608 --- greater than max batches
      118/1    0.68165    0.68457 +/- 0.00212
 Triggers unsatisfied, max unc./thresh. is 22.709602691165983 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58283 --- greater than max batches
      119/1    0.70967    0.68479 +/- 0.00211
 Triggers unsatisfied, max unc./thresh. is 22.509869996658225 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57769 --- greater than max batches
      120/1    0.65543    0.68453 +/- 0.00211
 Triggers unsatisfied, max unc./thresh. is 22.422104322874098 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57822 --- greater than max batches
      121/1    0.67305    0.68444 +/- 0.00209
 Triggers unsatisfied, max unc./thresh. is 22.32834321567902 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57838 --- greater than max batches
      122/1    0.68206    0.68441 +/- 0.00207
 Triggers unsatisfied, max unc./thresh. is 22.155196965032374 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57435 --- greater than max batches
      123/1    0.71125    0.68464 +/- 0.00207
 Triggers unsatisfied, max unc./thresh. is 21.96683678913398 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56945 --- greater than max batches
      124/1    0.65918    0.68443 +/- 0.00206
 Triggers unsatisfied, max unc./thresh. is 21.78216358933223 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56467 --- greater than max batches
      125/1    0.68122    0.68440 +/- 0.00205
 Triggers unsatisfied, max unc./thresh. is 21.681928420505304 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56418 --- greater than max batches
      126/1    0.66900    0.68427 +/- 0.00203
 Triggers unsatisfied, max unc./thresh. is 21.60631058168512 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56492 --- greater than max batches
      127/1    0.66742    0.68414 +/- 0.00202
 Triggers unsatisfied, max unc./thresh. is 21.468291123480988 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56234 --- greater than max batches
      128/1    0.66971    0.68402 +/- 0.00201
 Triggers unsatisfied, max unc./thresh. is 21.313238544974386 for absorption in
 tally 3
 WARNING: The estimated number of batches is 55879 --- greater than max batches
      129/1    0.68183    0.68400 +/- 0.00199
 Triggers unsatisfied, max unc./thresh. is 21.314008888585132 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56337 --- greater than max batches
      130/1    0.68403    0.68400 +/- 0.00197
 Triggers unsatisfied, max unc./thresh. is 21.159444482258046 for absorption in
 tally 3
 WARNING: The estimated number of batches is 55971 --- greater than max batches
      131/1    0.69137    0.68406 +/- 0.00196
 Triggers unsatisfied, max unc./thresh. is 21.24931160989673 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56899 --- greater than max batches
      132/1    0.67481    0.68399 +/- 0.00195
 Triggers unsatisfied, max unc./thresh. is 21.164512281281944 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56893 --- greater than max batches
      133/1    0.70390    0.68414 +/- 0.00194
 Triggers unsatisfied, max unc./thresh. is 21.084176856795946 for absorption in
 tally 3
 WARNING: The estimated number of batches is 56907 --- greater than max batches
      134/1    0.67961    0.68411 +/- 0.00192
 Triggers unsatisfied, max unc./thresh. is 21.48684646255342 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59563 --- greater than max batches
      135/1    0.65362    0.68387 +/- 0.00192
 Triggers unsatisfied, max unc./thresh. is 21.41235779526348 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59609 --- greater than max batches
      136/1    0.63946    0.68353 +/- 0.00194
 Triggers unsatisfied, max unc./thresh. is 21.30299014546295 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59456 --- greater than max batches
      137/1    0.64818    0.68327 +/- 0.00194
 Triggers unsatisfied, max unc./thresh. is 21.159761745415484 for absorption in
 tally 3
 WARNING: The estimated number of batches is 59107 --- greater than max batches
      138/1    0.68975    0.68331 +/- 0.00193
 Triggers unsatisfied, max unc./thresh. is 21.000094566393475 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58659 --- greater than max batches
      139/1    0.67280    0.68324 +/- 0.00191
 Triggers unsatisfied, max unc./thresh. is 20.853656171297644 for absorption in
 tally 3
 WARNING: The estimated number of batches is 58279 --- greater than max batches
      140/1    0.66857    0.68313 +/- 0.00190
 Triggers unsatisfied, max unc./thresh. is 20.709591767033707 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57905 --- greater than max batches
      141/1    0.68175    0.68312 +/- 0.00189
 Triggers unsatisfied, max unc./thresh. is 20.560813068689416 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57499 --- greater than max batches
      142/1    0.72210    0.68340 +/- 0.00190
 Triggers unsatisfied, max unc./thresh. is 20.54814917791921 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57851 --- greater than max batches
      143/1    0.67361    0.68333 +/- 0.00188
 Triggers unsatisfied, max unc./thresh. is 20.4177880049802 for absorption in
 tally 3
 WARNING: The estimated number of batches is 57536 --- greater than max batches
      144/1    0.65862    0.68315 +/- 0.00188
 Triggers unsatisfied, max unc./thresh. is 21.229890183572195 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62654 --- greater than max batches
      145/1    0.69713    0.68325 +/- 0.00187
 Triggers unsatisfied, max unc./thresh. is 21.34513800240435 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63792 --- greater than max batches
      146/1    0.72980    0.68358 +/- 0.00188
 Triggers unsatisfied, max unc./thresh. is 21.60165210412777 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65801 --- greater than max batches
      147/1    0.70004    0.68370 +/- 0.00187
 Triggers unsatisfied, max unc./thresh. is 21.596734424310384 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66237 --- greater than max batches
      148/1    0.68882    0.68374 +/- 0.00186
 Triggers unsatisfied, max unc./thresh. is 21.447240534346236 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65783 --- greater than max batches
      149/1    0.70401    0.68388 +/- 0.00185
 Triggers unsatisfied, max unc./thresh. is 21.424993974056104 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66106 --- greater than max batches
      150/1    0.72110    0.68413 +/- 0.00186
 Triggers unsatisfied, max unc./thresh. is 21.27792348945665 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65654 --- greater than max batches
      151/1    0.65918    0.68396 +/- 0.00185
 Triggers unsatisfied, max unc./thresh. is 21.378637401006184 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66734 --- greater than max batches
      152/1    0.67751    0.68392 +/- 0.00184
 Triggers unsatisfied, max unc./thresh. is 21.25974745003047 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66446 --- greater than max batches
      153/1    0.69302    0.68398 +/- 0.00183
 Triggers unsatisfied, max unc./thresh. is 21.16055371271148 for absorption in
 tally 3
 WARNING: The estimated number of batches is 66275 --- greater than max batches
      154/1    0.67102    0.68389 +/- 0.00182
 Triggers unsatisfied, max unc./thresh. is 21.0227808264386 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65857 --- greater than max batches
      155/1    0.64427    0.68363 +/- 0.00183
 Triggers unsatisfied, max unc./thresh. is 20.882547322553506 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65418 --- greater than max batches
      156/1    0.68488    0.68364 +/- 0.00181
 Triggers unsatisfied, max unc./thresh. is 20.797424126850476 for absorption in
 tally 3
 WARNING: The estimated number of batches is 65318 --- greater than max batches
      157/1    0.67337    0.68357 +/- 0.00180
 Triggers unsatisfied, max unc./thresh. is 20.67015745584828 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64948 --- greater than max batches
      158/1    0.66662    0.68346 +/- 0.00180
 Triggers unsatisfied, max unc./thresh. is 20.568519956722266 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64734 --- greater than max batches
      159/1    0.62697    0.68309 +/- 0.00182
 Triggers unsatisfied, max unc./thresh. is 20.47085213159483 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64540 --- greater than max batches
      160/1    0.68300    0.68309 +/- 0.00181
 Triggers unsatisfied, max unc./thresh. is 20.34586209866351 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64168 --- greater than max batches
      161/1    0.68918    0.68313 +/- 0.00180
 Triggers unsatisfied, max unc./thresh. is 20.23505377614212 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63881 --- greater than max batches
      162/1    0.70939    0.68330 +/- 0.00179
 Triggers unsatisfied, max unc./thresh. is 20.21114215977674 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64138 --- greater than max batches
      163/1    0.69681    0.68338 +/- 0.00179
 Triggers unsatisfied, max unc./thresh. is 20.163170350438893 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64241 --- greater than max batches
      164/1    0.66454    0.68326 +/- 0.00178
 Triggers unsatisfied, max unc./thresh. is 20.109882525638955 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64306 --- greater than max batches
      165/1    0.68804    0.68329 +/- 0.00177
 Triggers unsatisfied, max unc./thresh. is 20.033653897136055 for absorption in
 tally 3
 WARNING: The estimated number of batches is 64221 --- greater than max batches
      166/1    0.66078    0.68315 +/- 0.00176
 Triggers unsatisfied, max unc./thresh. is 19.916131324861123 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63867 --- greater than max batches
      167/1    0.65762    0.68300 +/- 0.00176
 Triggers unsatisfied, max unc./thresh. is 19.85097950868946 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63843 --- greater than max batches
      168/1    0.69267    0.68306 +/- 0.00175
 Triggers unsatisfied, max unc./thresh. is 19.729436003984436 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63453 --- greater than max batches
      169/1    0.67859    0.68303 +/- 0.00174
 Triggers unsatisfied, max unc./thresh. is 19.61178427698242 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63084 --- greater than max batches
      170/1    0.66545    0.68292 +/- 0.00173
 Triggers unsatisfied, max unc./thresh. is 19.495197332905757 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62716 --- greater than max batches
      171/1    0.66716    0.68283 +/- 0.00172
 Triggers unsatisfied, max unc./thresh. is 19.47044861415963 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62936 --- greater than max batches
      172/1    0.70008    0.68293 +/- 0.00172
 Triggers unsatisfied, max unc./thresh. is 19.382801191970024 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62746 --- greater than max batches
      173/1    0.69417    0.68300 +/- 0.00171
 Triggers unsatisfied, max unc./thresh. is 19.270038663528165 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62390 --- greater than max batches
      174/1    0.66458    0.68289 +/- 0.00170
 Triggers unsatisfied, max unc./thresh. is 19.281533726364312 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62836 --- greater than max batches
      175/1    0.65867    0.68275 +/- 0.00170
 Triggers unsatisfied, max unc./thresh. is 19.234847030404104 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62902 --- greater than max batches
      176/1    0.69631    0.68283 +/- 0.00169
 Triggers unsatisfied, max unc./thresh. is 19.13381099709457 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62609 --- greater than max batches
      177/1    0.71142    0.68299 +/- 0.00169
 Triggers unsatisfied, max unc./thresh. is 19.022563643493143 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62245 --- greater than max batches
      178/1    0.68640    0.68301 +/- 0.00168
 Triggers unsatisfied, max unc./thresh. is 19.03176453708651 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62667 --- greater than max batches
      179/1    0.70448    0.68313 +/- 0.00167
 Triggers unsatisfied, max unc./thresh. is 19.088395456136563 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63405 --- greater than max batches
      180/1    0.70538    0.68326 +/- 0.00167
 Triggers unsatisfied, max unc./thresh. is 18.98864751831452 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63105 --- greater than max batches
      181/1    0.65591    0.68311 +/- 0.00166
 Triggers unsatisfied, max unc./thresh. is 18.911017891051518 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62948 --- greater than max batches
      182/1    0.72818    0.68336 +/- 0.00167
 Triggers unsatisfied, max unc./thresh. is 18.808510226366458 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62621 --- greater than max batches
      183/1    0.67896    0.68334 +/- 0.00167
 Triggers unsatisfied, max unc./thresh. is 18.825142861337717 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63086 --- greater than max batches
      184/1    0.65442    0.68317 +/- 0.00166
 Triggers unsatisfied, max unc./thresh. is 18.79514029258707 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63239 --- greater than max batches
      185/1    0.68885    0.68321 +/- 0.00165
 Triggers unsatisfied, max unc./thresh. is 18.76276176864163 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63373 --- greater than max batches
      186/1    0.68893    0.68324 +/- 0.00165
 Triggers unsatisfied, max unc./thresh. is 18.690155368597363 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63233 --- greater than max batches
      187/1    0.68918    0.68327 +/- 0.00164
 Triggers unsatisfied, max unc./thresh. is 18.590144288270153 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62904 --- greater than max batches
      188/1    0.69854    0.68335 +/- 0.00163
 Triggers unsatisfied, max unc./thresh. is 18.61460656150607 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63416 --- greater than max batches
      189/1    0.66324    0.68324 +/- 0.00162
 Triggers unsatisfied, max unc./thresh. is 18.518608099237504 for absorption in
 tally 3
 WARNING: The estimated number of batches is 63106 --- greater than max batches
      190/1    0.69450    0.68331 +/- 0.00162
 Triggers unsatisfied, max unc./thresh. is 18.425351661292233 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62812 --- greater than max batches
      191/1    0.68953    0.68334 +/- 0.00161
 Triggers unsatisfied, max unc./thresh. is 18.328779429843646 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62491 --- greater than max batches
      192/1    0.66621    0.68325 +/- 0.00160
 Triggers unsatisfied, max unc./thresh. is 18.28094973389564 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62500 --- greater than max batches
      193/1    0.71102    0.68339 +/- 0.00160
 Triggers unsatisfied, max unc./thresh. is 18.19949730061142 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62275 --- greater than max batches
      194/1    0.65341    0.68324 +/- 0.00160
 Triggers unsatisfied, max unc./thresh. is 18.159054737369345 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62328 --- greater than max batches
      195/1    0.70061    0.68333 +/- 0.00159
 Triggers unsatisfied, max unc./thresh. is 18.082465954324594 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62131 --- greater than max batches
      196/1    0.69339    0.68338 +/- 0.00159
 Triggers unsatisfied, max unc./thresh. is 18.043133483791827 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62186 --- greater than max batches
      197/1    0.64411    0.68318 +/- 0.00159
 Triggers unsatisfied, max unc./thresh. is 18.019303623546417 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62347 --- greater than max batches
      198/1    0.66626    0.68309 +/- 0.00159
 Triggers unsatisfied, max unc./thresh. is 17.968092739058083 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62316 --- greater than max batches
      199/1    0.67839    0.68306 +/- 0.00158
 Triggers unsatisfied, max unc./thresh. is 17.91968515142146 for absorption in
 tally 3
 WARNING: The estimated number of batches is 62302 --- greater than max batches
      200/1    0.66459    0.68297 +/- 0.00157
 Triggers unsatisfied, max unc./thresh. is 17.82970764669685 for absorption in
 tally 3
 WARNING: The estimated number of batches is 61996 --- greater than max batches
 Creating state point statepoint.200.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 2.9309e-01 seconds
   Reading cross sections          = 2.8108e-01 seconds
 Total time in simulation          = 1.1321e+01 seconds
   Time in transport only          = 1.1242e+01 seconds
   Time in inactive batches        = 1.6721e-01 seconds
   Time in active batches          = 1.1153e+01 seconds
   Time synchronizing fission bank = 2.2958e-02 seconds
     Sampling source sites         = 1.8701e-02 seconds
     SEND/RECV source sites        = 3.9403e-03 seconds
   Time accumulating tallies       = 9.9349e-04 seconds
 Total time for finalization       = 5.2200e-07 seconds
 Total time elapsed                = 1.1620e+01 seconds
 Calculation Rate (inactive)       = 74758.2 particles/second
 Calculation Rate (active)         = 43708.5 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.68198 +/- 0.00141
 k-effective (Track-length)  = 0.68297 +/- 0.00157
 k-effective (Absorption)    = 0.68161 +/- 0.00145
 Combined k-effective        = 0.68209 +/- 0.00118
 Leakage Fraction            = 0.34033 +/- 0.00074

Tally Data Processing
[17]:
# We do not know how many batches were needed to satisfy the
# tally trigger(s), so find the statepoint file(s)
statepoints = glob.glob('statepoint.*.h5')

# Load the last statepoint file
sp = openmc.StatePoint(statepoints[-1])

Analyze the mesh fission rate tally

[18]:
# Find the mesh tally with the StatePoint API
tally = sp.get_tally(name='mesh tally')

# Print a little info about the mesh tally to the screen
print(tally)
Tally
        ID             =        1
        Name           =        mesh tally
        Filters        =        MeshFilter, EnergyFilter
        Nuclides       =        total
        Scores         =        ['fission', 'nu-fission']
        Estimator      =        tracklength

Use the new Tally data retrieval API with pure NumPy

[19]:
# Get the relative error for the thermal fission reaction
# rates in the four corner pins
data = tally.get_values(scores=['fission'],
                        filters=[openmc.MeshFilter, openmc.EnergyFilter], \
                        filter_bins=[((1,1),(1,17), (17,1), (17,17)), \
                                    ((0., 0.625),)], value='rel_err')
print(data)
[[[0.04508259]]

 [[0.0221707 ]]

 [[0.10763375]]

 [[0.05107401]]]
[20]:
# Get a pandas dataframe for the mesh tally data
df = tally.get_pandas_dataframe(nuclides=False)

# Set the Pandas float display settings
pd.options.display.float_format = '{:.2e}'.format

# Print the first twenty rows in the dataframe
df.head(20)
[20]:
mesh 1 energy low [eV] energy high [eV] score mean std. dev.
x y z
0 1 1 1 0.00e+00 6.25e-01 fission 2.27e-04 1.02e-05
1 1 1 1 0.00e+00 6.25e-01 nu-fission 5.54e-04 2.50e-05
2 1 1 1 6.25e-01 2.00e+07 fission 7.19e-05 1.82e-06
3 1 1 1 6.25e-01 2.00e+07 nu-fission 1.89e-04 4.69e-06
4 2 1 1 0.00e+00 6.25e-01 fission 2.35e-04 9.82e-06
5 2 1 1 0.00e+00 6.25e-01 nu-fission 5.71e-04 2.39e-05
6 2 1 1 6.25e-01 2.00e+07 fission 6.88e-05 1.61e-06
7 2 1 1 6.25e-01 2.00e+07 nu-fission 1.81e-04 4.15e-06
8 3 1 1 0.00e+00 6.25e-01 fission 2.31e-04 1.13e-05
9 3 1 1 0.00e+00 6.25e-01 nu-fission 5.63e-04 2.76e-05
10 3 1 1 6.25e-01 2.00e+07 fission 6.95e-05 1.76e-06
11 3 1 1 6.25e-01 2.00e+07 nu-fission 1.83e-04 4.53e-06
12 4 1 1 0.00e+00 6.25e-01 fission 2.07e-04 9.85e-06
13 4 1 1 0.00e+00 6.25e-01 nu-fission 5.04e-04 2.40e-05
14 4 1 1 6.25e-01 2.00e+07 fission 6.48e-05 1.45e-06
15 4 1 1 6.25e-01 2.00e+07 nu-fission 1.71e-04 3.81e-06
16 5 1 1 0.00e+00 6.25e-01 fission 2.20e-04 1.07e-05
17 5 1 1 0.00e+00 6.25e-01 nu-fission 5.37e-04 2.60e-05
18 5 1 1 6.25e-01 2.00e+07 fission 6.76e-05 1.78e-06
19 5 1 1 6.25e-01 2.00e+07 nu-fission 1.78e-04 4.63e-06
[21]:
# Create a boxplot to view the distribution of
# fission and nu-fission rates in the pins
bp = df.boxplot(column='mean', by='score')
_images/examples_pandas-dataframes_39_0.png
[22]:
# Extract thermal nu-fission rates from pandas
fiss = df[df['score'] == 'nu-fission']
fiss = fiss[fiss['energy low [eV]'] == 0.0]

# Extract mean and reshape as 2D NumPy arrays
mean = fiss['mean'].values.reshape((17,17))

plt.imshow(mean, interpolation='nearest')
plt.title('fission rate')
plt.xlabel('x')
plt.ylabel('y')
plt.colorbar()
[22]:
<matplotlib.colorbar.Colorbar at 0x7fd070ca32b0>
_images/examples_pandas-dataframes_40_1.png

Analyze the cell+nuclides scatter-y2 rate tally

[23]:
# Find the cell Tally with the StatePoint API
tally = sp.get_tally(name='cell tally')

# Print a little info about the cell tally to the screen
print(tally)
Tally
        ID             =        2
        Name           =        cell tally
        Filters        =        CellFilter
        Nuclides       =        U235 U238
        Scores         =        ['scatter']
        Estimator      =        tracklength
[24]:
# Get a pandas dataframe for the cell tally data
df = tally.get_pandas_dataframe()

# Print the first twenty rows in the dataframe
df.head(20)
[24]:
cell nuclide score mean std. dev.
0 1 U235 scatter 3.81e-02 4.13e-05
1 1 U238 scatter 2.34e+00 2.41e-03

Use the new Tally data retrieval API with pure NumPy

[25]:
# Get the standard deviations the total scattering rate
data = tally.get_values(scores=['scatter'],
                        nuclides=['U238', 'U235'], value='std_dev')
print(data)
[[[2.41367509e-03]
  [4.12533801e-05]]]

Analyze the distribcell tally

[26]:
# Find the distribcell Tally with the StatePoint API
tally = sp.get_tally(name='distribcell tally')

# Print a little info about the distribcell tally to the screen
print(tally)
Tally
        ID             =        3
        Name           =        distribcell tally
        Filters        =        DistribcellFilter
        Nuclides       =        total
        Scores         =        ['absorption', 'scatter']
        Estimator      =        tracklength

Use the new Tally data retrieval API with pure NumPy

[27]:
# Get the relative error for the scattering reaction rates in
# the first 10 distribcell instances
data = tally.get_values(scores=['scatter'], filters=[openmc.DistribcellFilter],
                        filter_bins=[tuple(range(10))], value='rel_err')
print(data)
[[[0.0131914 ]]

 [[0.01252949]]

 [[0.01241481]]

 [[0.01194961]]

 [[0.01186091]]

 [[0.0127257 ]]

 [[0.01358576]]

 [[0.0130368 ]]

 [[0.014031  ]]

 [[0.0141883 ]]]

Print the distribcell tally dataframe

[28]:
# Get a pandas dataframe for the distribcell tally data
df = tally.get_pandas_dataframe(nuclides=False)

# Print the last twenty rows in the dataframe
df.tail(20)
[28]:
level 1 level 2 level 3 distribcell score mean std. dev.
univ cell lat univ cell
id id id x y id id
558 3 4 2 7 16 1 3 279 absorption 7.11e-04 1.10e-05
559 3 4 2 7 16 1 3 279 scatter 8.91e-02 6.60e-04
560 3 4 2 8 16 1 3 280 absorption 6.75e-04 1.02e-05
561 3 4 2 8 16 1 3 280 scatter 8.35e-02 6.11e-04
562 3 4 2 9 16 1 3 281 absorption 6.10e-04 1.02e-05
563 3 4 2 9 16 1 3 281 scatter 7.75e-02 6.10e-04
564 3 4 2 10 16 1 3 282 absorption 5.67e-04 9.88e-06
565 3 4 2 10 16 1 3 282 scatter 7.11e-02 5.99e-04
566 3 4 2 11 16 1 3 283 absorption 5.06e-04 9.35e-06
567 3 4 2 11 16 1 3 283 scatter 6.39e-02 5.53e-04
568 3 4 2 12 16 1 3 284 absorption 4.35e-04 8.22e-06
569 3 4 2 12 16 1 3 284 scatter 5.62e-02 5.18e-04
570 3 4 2 13 16 1 3 285 absorption 3.73e-04 7.90e-06
571 3 4 2 13 16 1 3 285 scatter 4.76e-02 4.92e-04
572 3 4 2 14 16 1 3 286 absorption 2.98e-04 7.30e-06
573 3 4 2 14 16 1 3 286 scatter 3.82e-02 4.17e-04
574 3 4 2 15 16 1 3 287 absorption 2.05e-04 5.96e-06
575 3 4 2 15 16 1 3 287 scatter 2.86e-02 3.72e-04
576 3 4 2 16 16 1 3 288 absorption 1.22e-04 4.12e-06
577 3 4 2 16 16 1 3 288 scatter 1.82e-02 2.59e-04
[29]:
# Show summary statistics for absorption distribcell tally data
absorption = df[df['score'] == 'absorption']
absorption[['mean', 'std. dev.']].dropna().describe()

# Note that the maximum standard deviation does indeed
# meet the 5e-5 threshold set by the tally trigger
[29]:
mean std. dev.
count 2.89e+02 2.89e+02
mean 4.19e-04 6.86e-06
std 2.41e-04 2.51e-06
min 1.68e-05 1.07e-06
25% 2.06e-04 5.09e-06
50% 3.98e-04 6.90e-06
75% 6.17e-04 8.44e-06
max 8.70e-04 1.52e-05

Perform a statistical test comparing the tally sample distributions for two categories of fuel pins.

[30]:
# Extract tally data from pins in the pins divided along y=-x diagonal
multi_index = ('level 2', 'lat',)
lower = df[df[multi_index + ('x',)] + df[multi_index + ('y',)] < 16]
upper = df[df[multi_index + ('x',)] + df[multi_index + ('y',)] > 16]
lower = lower[lower['score'] == 'absorption']
upper = upper[upper['score'] == 'absorption']

# Perform non-parametric Mann-Whitney U Test to see if the
# absorption rates (may) come from same sampling distribution
u, p = scipy.stats.mannwhitneyu(lower['mean'], upper['mean'])
print('Mann-Whitney Test p-value: {0}'.format(p))
Mann-Whitney Test p-value: 0.47449458604689265

Note that the symmetry implied by the y=-x diagonal ensures that the two sampling distributions are identical. Indeed, as illustrated by the test above, for any reasonable significance level (e.g., \(\alpha\)=0.05) one would not reject the null hypothesis that the two sampling distributions are identical.

Next, perform the same test but with two groupings of pins which are not symmetrically identical to one another.

[31]:
# Extract tally data from pins in the pins divided along y=x diagonal
multi_index = ('level 2', 'lat',)
lower = df[df[multi_index + ('x',)] > df[multi_index + ('y',)]]
upper = df[df[multi_index + ('x',)] < df[multi_index + ('y',)]]
lower = lower[lower['score'] == 'absorption']
upper = upper[upper['score'] == 'absorption']

# Perform non-parametric Mann-Whitney U Test to see if the
# absorption rates (may) come from same sampling distribution
u, p = scipy.stats.mannwhitneyu(lower['mean'], upper['mean'])
print('Mann-Whitney Test p-value: {0}'.format(p))
Mann-Whitney Test p-value: 2.499381683224802e-42

Note that the asymmetry implied by the y=x diagonal ensures that the two sampling distributions are not identical. Indeed, as illustrated by the test above, for any reasonable significance level (e.g., \(\alpha\)=0.05) one would reject the null hypothesis that the two sampling distributions are identical.

[32]:
# Extract the scatter tally data from pandas
scatter = df[df['score'] == 'scatter']

scatter['rel. err.'] = scatter['std. dev.'] / scatter['mean']

# Show a scatter plot of the mean vs. the std. dev.
scatter.plot(kind='scatter', x='mean', y='rel. err.', title='Scattering Rates')
<ipython-input-32-c935bd379fee>:4: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  scatter['rel. err.'] = scatter['std. dev.'] / scatter['mean']
[32]:
<AxesSubplot:title={'center':'Scattering Rates'}, xlabel='mean', ylabel='rel. err.'>
_images/examples_pandas-dataframes_58_2.png
[33]:
# Plot a histogram and kernel density estimate for the scattering rates
scatter['mean'].plot(kind='hist', bins=25)
scatter['mean'].plot(kind='kde')
plt.title('Scattering Rates')
plt.xlabel('Mean')
plt.legend(['KDE', 'Histogram'])
[33]:
<matplotlib.legend.Legend at 0x7fd070fbd1f0>
_images/examples_pandas-dataframes_59_1.png

Tally Arithmetic

This notebook shows the how tallies can be combined (added, subtracted, multiplied, etc.) using the Python API in order to create derived tallies. Since no covariance information is obtained, it is assumed that tallies are completely independent of one another when propagating uncertainties. The target problem is a simple pin cell.

[1]:
import glob

from IPython.display import Image
import numpy as np
import openmc
Generate Input Files

First we need to define materials that will be used in the problem. We’ll create three materials for the fuel, water, and cladding of the fuel pin.

[2]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)
water.add_nuclide('B10', 8.0042e-6)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our three materials, we can now create a materials file object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials collection
materials_file = openmc.Materials([fuel, water, zircaloy])

# Export to "materials.xml"
materials_file.export_to_xml()

Now let’s move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces – in this case two cylinders and six planes.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.45720)

# Create boundary planes to surround the geometry
# Use both reflective and vacuum boundaries to make life interesting
min_x = openmc.XPlane(x0=-0.63, boundary_type='reflective')
max_x = openmc.XPlane(x0=+0.63, boundary_type='reflective')
min_y = openmc.YPlane(y0=-0.63, boundary_type='reflective')
max_y = openmc.YPlane(y0=+0.63, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-100., boundary_type='vacuum')
max_z = openmc.ZPlane(z0=+100., boundary_type='vacuum')

With the surfaces defined, we can now create cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
pin_cell_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
pin_cell_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
pin_cell_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
pin_cell_universe.add_cell(moderator_cell)

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.

[6]:
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = pin_cell_universe

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(root_cell)

We now must create a geometry that is assigned a root universe, put the geometry into a geometry file, and export it to XML.

[7]:
# Create Geometry and set root Universe
geometry = openmc.Geometry(root_universe)
[8]:
# Export to "geometry.xml"
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 5 inactive batches and 15 active batches each with 2500 particles.

[9]:
# OpenMC simulation parameters
batches = 20
inactive = 5
particles = 2500

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -100., 0.63, 0.63, 100.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()

Let us also create a plot file that we can use to verify that our pin cell geometry was created successfully.

[10]:
# Instantiate a Plot
plot = openmc.Plot(plot_id=1)
plot.filename = 'materials-xy'
plot.origin = [0, 0, 0]
plot.width = [1.26, 1.26]
plot.pixels = [250, 250]
plot.color_by = 'material'

# Show plot
openmc.plot_inline(plot)
_images/examples_tally-arithmetic_19_0.png

As we can see from the plot, we have a nice pin cell with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a variety of tallies.

[11]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
[12]:
# Create Tallies to compute microscopic multi-group cross-sections

# Instantiate energy filter for multi-group cross-section Tallies
energy_filter = openmc.EnergyFilter([0., 0.625, 20.0e6])

# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='flux')
tally.filters = [openmc.CellFilter([fuel_cell, moderator_cell])]
tally.filters.append(energy_filter)
tally.scores = ['flux']
tallies_file.append(tally)

# Instantiate reaction rate Tally in fuel
tally = openmc.Tally(name='fuel rxn rates')
tally.filters = [openmc.CellFilter(fuel_cell)]
tally.filters.append(energy_filter)
tally.scores = ['nu-fission', 'scatter']
tally.nuclides = ['U238', 'U235']
tallies_file.append(tally)

# Instantiate reaction rate Tally in moderator
tally = openmc.Tally(name='moderator rxn rates')
tally.filters = [openmc.CellFilter(moderator_cell)]
tally.filters.append(energy_filter)
tally.scores = ['absorption', 'total']
tally.nuclides = ['O16', 'H1']
tallies_file.append(tally)

# Instantiate a tally mesh
mesh = openmc.RegularMesh(mesh_id=1)
mesh.dimension = [1, 1, 1]
mesh.lower_left = [-0.63, -0.63, -100.]
mesh.width = [1.26, 1.26, 200.]
meshsurface_filter = openmc.MeshSurfaceFilter(mesh)

# Instantiate thermal, fast, and total leakage tallies
leak = openmc.Tally(name='leakage')
leak.filters = [meshsurface_filter]
leak.scores = ['current']
tallies_file.append(leak)

thermal_leak = openmc.Tally(name='thermal leakage')
thermal_leak.filters = [meshsurface_filter, openmc.EnergyFilter([0., 0.625])]
thermal_leak.scores = ['current']
tallies_file.append(thermal_leak)

fast_leak = openmc.Tally(name='fast leakage')
fast_leak.filters = [meshsurface_filter, openmc.EnergyFilter([0.625, 20.0e6])]
fast_leak.scores = ['current']
tallies_file.append(fast_leak)
[13]:
# K-Eigenvalue (infinity) tallies
fiss_rate = openmc.Tally(name='fiss. rate')
abs_rate = openmc.Tally(name='abs. rate')
fiss_rate.scores = ['nu-fission']
abs_rate.scores = ['absorption']
tallies_file += (fiss_rate, abs_rate)
[14]:
# Resonance Escape Probability tallies
therm_abs_rate = openmc.Tally(name='therm. abs. rate')
therm_abs_rate.scores = ['absorption']
therm_abs_rate.filters = [openmc.EnergyFilter([0., 0.625])]
tallies_file.append(therm_abs_rate)
[15]:
# Thermal Flux Utilization tallies
fuel_therm_abs_rate = openmc.Tally(name='fuel therm. abs. rate')
fuel_therm_abs_rate.scores = ['absorption']
fuel_therm_abs_rate.filters = [openmc.EnergyFilter([0., 0.625]),
                               openmc.CellFilter([fuel_cell])]
tallies_file.append(fuel_therm_abs_rate)
[16]:
# Fast Fission Factor tallies
therm_fiss_rate = openmc.Tally(name='therm. fiss. rate')
therm_fiss_rate.scores = ['nu-fission']
therm_fiss_rate.filters = [openmc.EnergyFilter([0., 0.625])]
tallies_file.append(therm_fiss_rate)
[17]:
# Instantiate energy filter to illustrate Tally slicing
fine_energy_filter = openmc.EnergyFilter(np.logspace(np.log10(1e-2), np.log10(20.0e6), 10))

# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='need-to-slice')
tally.filters = [openmc.CellFilter([fuel_cell, moderator_cell])]
tally.filters.append(fine_energy_filter)
tally.scores = ['nu-fission', 'scatter']
tally.nuclides = ['H1', 'U238']
tallies_file.append(tally)
[18]:
# Export to "tallies.xml"
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=6.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=3.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[19]:
# Run OpenMC!
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-18 22:51:02
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading B10 from /opt/data/hdf5/nndc_hdf5_v15/B10.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    0.96168
        2/1    0.96651
        3/1    1.00678
        4/1    0.98773
        5/1    1.01883
        6/1    1.02959
        7/1    0.99859    1.01409 +/- 0.01550
        8/1    1.03441    1.02086 +/- 0.01123
        9/1    1.06097    1.03089 +/- 0.01279
       10/1    1.06132    1.03698 +/- 0.01163
       11/1    1.04687    1.03863 +/- 0.00964
       12/1    1.02982    1.03737 +/- 0.00824
       13/1    1.03520    1.03710 +/- 0.00714
       14/1    0.99508    1.03243 +/- 0.00784
       15/1    1.03987    1.03317 +/- 0.00705
       16/1    1.02743    1.03265 +/- 0.00640
       17/1    1.02975    1.03241 +/- 0.00585
       18/1    0.99671    1.02966 +/- 0.00604
       19/1    1.02040    1.02900 +/- 0.00563
       20/1    1.02024    1.02842 +/- 0.00527
 Creating state point statepoint.20.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 3.4427e-01 seconds
   Reading cross sections          = 3.1628e-01 seconds
 Total time in simulation          = 3.7319e+00 seconds
   Time in transport only          = 3.6302e+00 seconds
   Time in inactive batches        = 4.9601e-01 seconds
   Time in active batches          = 3.2359e+00 seconds
   Time synchronizing fission bank = 2.8100e-03 seconds
     Sampling source sites         = 2.4682e-03 seconds
     SEND/RECV source sites        = 3.2484e-04 seconds
   Time accumulating tallies       = 4.4538e-05 seconds
 Total time for finalization       = 9.3656e-04 seconds
 Total time elapsed                = 4.0859e+00 seconds
 Calculation Rate (inactive)       = 25201.2 particles/second
 Calculation Rate (active)         = 11588.7 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.02889 +/- 0.00492
 k-effective (Track-length)  = 1.02842 +/- 0.00527
 k-effective (Absorption)    = 1.02637 +/- 0.00349
 Combined k-effective        = 1.02700 +/- 0.00291
 Leakage Fraction            = 0.01717 +/- 0.00107

Tally Data Processing

Our simulation ran successfully and created a statepoint file with all the tally data in it. We begin our analysis here loading the statepoint file and ‘reading’ the results. By default, the tally results are not read into memory because they might be large, even large enough to exceed the available memory on a computer.

[20]:
# Load the statepoint file
sp = openmc.StatePoint('statepoint.20.h5')

We have a tally of the total fission rate and the total absorption rate, so we can calculate k-eff as:

\[k_{eff} = \frac{\langle \nu \Sigma_f \phi \rangle}{\langle \Sigma_a \phi \rangle + \langle L \rangle} In this notation, :math:`\langle \cdot \rangle^a_b` represents an OpenMC that is integrated over region :math:`a` and energy range :math:`b`. If :math:`a` or :math:`b` is not reported, it means the value represents an integral over all space or all energy, respectively.\]
[21]:
# Get the fission and absorption rate tallies
fiss_rate = sp.get_tally(name='fiss. rate')
abs_rate = sp.get_tally(name='abs. rate')

# Get the leakage tally
leak = sp.get_tally(name='leakage')
leak = leak.summation(filter_type=openmc.MeshSurfaceFilter, remove_filter=True)

# Compute k-infinity using tally arithmetic
keff = fiss_rate / (abs_rate + leak)
keff.get_pandas_dataframe()
[21]:
nuclide score mean std. dev.
0 total (nu-fission / (absorption + current)) 1.023002 0.006647

Notice that even though the neutron production rate, absorption rate, and current are separate tallies, we still get a first-order estimate of the uncertainty on the quotient of them automatically!

Often in textbooks you’ll see k-eff represented using the six-factor formula

\[k_{eff} = p \epsilon f \eta P_{FNL} P_{TNL}. \]

Let’s analyze each of these factors, starting with the resonance escape probability which is defined as

\[p=\frac{\langle\Sigma_a\phi\rangle_T + \langle L \rangle_T}{\langle\Sigma_a\phi\rangle + \langle L \rangle_T} where the subscript :math:`T` means thermal energies.\]
[22]:
# Compute resonance escape probability using tally arithmetic
therm_abs_rate = sp.get_tally(name='therm. abs. rate')
thermal_leak = sp.get_tally(name='thermal leakage')
thermal_leak = thermal_leak.summation(filter_type=openmc.MeshSurfaceFilter, remove_filter=True)
res_esc = (therm_abs_rate + thermal_leak) / (abs_rate + thermal_leak)
res_esc.get_pandas_dataframe()
[22]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total ((absorption + current) / (absorption + current)) 0.694368 0.004606

The fast fission factor can be calculated as

\[\epsilon=\frac{\langle\nu\Sigma_f\phi\rangle}{\langle\nu\Sigma_f\phi\rangle_T} \]
[23]:
# Compute fast fission factor factor using tally arithmetic
therm_fiss_rate = sp.get_tally(name='therm. fiss. rate')
fast_fiss = fiss_rate / therm_fiss_rate
fast_fiss.get_pandas_dataframe()
[23]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total (nu-fission / nu-fission) 1.203099 0.009615

The thermal flux utilization is calculated as

\[f=\frac{\langle\Sigma_a\phi\rangle^F_T}{\langle\Sigma_a\phi\rangle_T} where the superscript :math:`F` denotes fuel.\]
[24]:
# Compute thermal flux utilization factor using tally arithmetic
fuel_therm_abs_rate = sp.get_tally(name='fuel therm. abs. rate')
therm_util = fuel_therm_abs_rate / therm_abs_rate
therm_util.get_pandas_dataframe()
[24]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 1 total (absorption / absorption) 0.749423 0.006089

The next factor is the number of fission neutrons produced per absorption in fuel, calculated as

\[\eta = \frac{\langle \nu\Sigma_f\phi \rangle_T}{\langle \Sigma_a \phi \rangle^F_T} \]
[25]:
# Compute neutrons produced per absorption (eta) using tally arithmetic
eta = therm_fiss_rate / fuel_therm_abs_rate
eta.get_pandas_dataframe()
[25]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 1 total (nu-fission / absorption) 1.663727 0.014403

There are two leakage factors to account for fast and thermal leakage. The fast non-leakage probability is computed as

\[P_{FNL} = \frac{\langle \Sigma_a\phi \rangle + \langle L \rangle_T}{\langle \Sigma_a \phi \rangle + \langle L \rangle} \]
[26]:
p_fnl = (abs_rate + thermal_leak) / (abs_rate + leak)
p_fnl.get_pandas_dataframe()
[26]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total ((absorption + current) / (absorption + current)) 0.984668 0.005509

The final factor is the thermal non-leakage probability and is computed as

\[P_{TNL} = \frac{\langle \Sigma_a\phi \rangle_T}{\langle \Sigma_a \phi \rangle_T + \langle L \rangle_T} \]
[27]:
p_tnl = therm_abs_rate / (therm_abs_rate + thermal_leak)
p_tnl.get_pandas_dataframe()
[27]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total (absorption / (absorption + current)) 0.997439 0.007548

Now we can calculate \(k_{eff}\) using the product of the factors form the four-factor formula.

[28]:
keff = res_esc * fast_fiss * therm_util * eta * p_fnl * p_tnl
keff.get_pandas_dataframe()
[28]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 1 total (((((((absorption + current) / (absorption + c... 1.023002 0.018791

We see that the value we’ve obtained here has exactly the same mean as before. However, because of the way it was calculated, the standard deviation appears to be larger.

Let’s move on to a more complicated example now. Before we set up tallies to get reaction rates in the fuel and moderator in two energy groups for two different nuclides. We can use tally arithmetic to divide each of these reaction rates by the flux to get microscopic multi-group cross sections.

[29]:
# Compute microscopic multi-group cross-sections
flux = sp.get_tally(name='flux')
flux = flux.get_slice(filters=[openmc.CellFilter], filter_bins=[(fuel_cell.id,)])
fuel_rxn_rates = sp.get_tally(name='fuel rxn rates')
mod_rxn_rates = sp.get_tally(name='moderator rxn rates')
[30]:
fuel_xs = fuel_rxn_rates / flux
fuel_xs.get_pandas_dataframe()
[30]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 (U238 / total) (nu-fission / flux) 6.659486e-07 5.627975e-09
1 1 0.000 6.250000e-01 (U238 / total) (scatter / flux) 2.099901e-01 1.748379e-03
2 1 0.000 6.250000e-01 (U235 / total) (nu-fission / flux) 3.566329e-01 3.030782e-03
3 1 0.000 6.250000e-01 (U235 / total) (scatter / flux) 5.555466e-03 4.635318e-05
4 1 0.625 2.000000e+07 (U238 / total) (nu-fission / flux) 7.251304e-03 5.161998e-05
5 1 0.625 2.000000e+07 (U238 / total) (scatter / flux) 2.272661e-01 9.576939e-04
6 1 0.625 2.000000e+07 (U235 / total) (nu-fission / flux) 7.920169e-03 5.751231e-05
7 1 0.625 2.000000e+07 (U235 / total) (scatter / flux) 3.358280e-03 1.341281e-05

We see that when the two tallies with multiple bins were divided, the derived tally contains the outer product of the combinations. If the filters/scores are the same, no outer product is needed. The get_values(...) method allows us to obtain a subset of tally scores. In the following example, we obtain just the neutron production microscopic cross sections.

[31]:
# Show how to use Tally.get_values(...) with a CrossScore
nu_fiss_xs = fuel_xs.get_values(scores=['(nu-fission / flux)'])
print(nu_fiss_xs)
[[[6.65948580e-07]
  [3.56632881e-01]]

 [[7.25130446e-03]
  [7.92016892e-03]]]

The same idea can be used not only for scores but also for filters and nuclides.

[32]:
# Show how to use Tally.get_values(...) with a CrossScore and CrossNuclide
u235_scatter_xs = fuel_xs.get_values(nuclides=['(U235 / total)'],
                                scores=['(scatter / flux)'])
print(u235_scatter_xs)
[[[0.00555547]]

 [[0.00335828]]]
[33]:
# Show how to use Tally.get_values(...) with a CrossFilter and CrossScore
fast_scatter_xs = fuel_xs.get_values(filters=[openmc.EnergyFilter],
                                     filter_bins=[((0.625, 20.0e6),)],
                                     scores=['(scatter / flux)'])
print(fast_scatter_xs)
[[[0.22726611]
  [0.00335828]]]

A more advanced method is to use get_slice(...) to create a new derived tally that is a subset of an existing tally. This has the benefit that we can use get_pandas_dataframe() to see the tallies in a more human-readable format.

[34]:
# "Slice" the nu-fission data into a new derived Tally
nu_fission_rates = fuel_rxn_rates.get_slice(scores=['nu-fission'])
nu_fission_rates.get_pandas_dataframe()
[34]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 U238 nu-fission 0.000002 9.679304e-09
1 1 0.000 6.250000e-01 U235 nu-fission 0.854805 5.239673e-03
2 1 0.625 2.000000e+07 U238 nu-fission 0.082978 5.346135e-04
3 1 0.625 2.000000e+07 U235 nu-fission 0.090632 5.981942e-04
[35]:
# "Slice" the H-1 scatter data in the moderator Cell into a new derived Tally
need_to_slice = sp.get_tally(name='need-to-slice')
slice_test = need_to_slice.get_slice(scores=['scatter'], nuclides=['H1'],
                                     filters=[openmc.CellFilter], filter_bins=[(moderator_cell.id,)])
slice_test.get_pandas_dataframe()
[35]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 3 1.000000e-02 1.080060e-01 H1 scatter 4.541188 0.025230
1 3 1.080060e-01 1.166529e+00 H1 scatter 2.001332 0.006754
2 3 1.166529e+00 1.259921e+01 H1 scatter 1.639292 0.011374
3 3 1.259921e+01 1.360790e+02 H1 scatter 1.821633 0.009590
4 3 1.360790e+02 1.469734e+03 H1 scatter 2.032395 0.009953
5 3 1.469734e+03 1.587401e+04 H1 scatter 2.120745 0.011090
6 3 1.587401e+04 1.714488e+05 H1 scatter 2.181709 0.013602
7 3 1.714488e+05 1.851749e+06 H1 scatter 2.013644 0.009219
8 3 1.851749e+06 2.000000e+07 H1 scatter 0.372640 0.002903

Using the C/C++ API

This notebook shows how to use the OpenMC C/C++ API through the openmc.lib module. This module is particularly useful for multiphysics coupling because it allows you to update the density of materials and the temperatures of cells in memory, without stopping the simulation.

Warning: these bindings are still somewhat experimental and may be subject to change in future versions of OpenMC.

[1]:
%matplotlib inline
import openmc
import openmc.lib

Generate Input Files

Let’s start by creating a fuel rod geometry. We will make 10 zones in the z-direction which will allow us to make changes to each zone. Changes in temperature have to be made on the cell, so will make 10 cells in the axial direction. Changes in density have to be made on the material, so we will make 10 water materials.

Materials: we will make a fuel, helium, zircaloy, and 10 water materials.

[2]:
material_list = []
[3]:
uo2 = openmc.Material(material_id=1, name='UO2 fuel at 2.4% wt enrichment')
uo2.set_density('g/cm3', 10.29769)
uo2.add_element('U', 1., enrichment=2.4)
uo2.add_element('O', 2.)
material_list.append(uo2)

helium = openmc.Material(material_id=2, name='Helium for gap')
helium.set_density('g/cm3', 0.001598)
helium.add_element('He', 2.4044e-4)
material_list.append(helium)

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')
material_list.append(zircaloy)

for i in range(4, 14):
    water = openmc.Material(material_id=i)
    water.set_density('g/cm3', 0.7)
    water.add_element('H', 2.0)
    water.add_element('O', 1.0)
    water.add_s_alpha_beta('c_H_in_H2O')
    material_list.append(water)

materials_file = openmc.Materials(material_list)
materials_file.export_to_xml()

Cells: we will make a fuel cylinder, a gap cylinder, a cladding cylinder, and a water exterior. Each one will be broken into 10 cells which are the 10 axial zones. The z_list is the list of axial positions that delimit those 10 zones. To keep track of all the cells, we will create lists: fuel_list, gap_list, clad_list, and water_list.

[4]:
pitch = 1.25984
fuel_or = openmc.ZCylinder(r=0.39218)
clad_ir = openmc.ZCylinder(r=0.40005)
clad_or = openmc.ZCylinder(r=0.4572)
left = openmc.XPlane(x0=-pitch/2)
right = openmc.XPlane(x0=pitch/2)
back = openmc.YPlane(y0=-pitch/2)
front = openmc.YPlane(y0=pitch/2)
z = [0., 30., 60., 90., 120., 150., 180., 210., 240., 270., 300.]
z_list = [openmc.ZPlane(z0=z_i) for z_i in z]
[5]:
left.boundary_type = 'reflective'
right.boundary_type = 'reflective'
front.boundary_type = 'reflective'
back.boundary_type = 'reflective'
z_list[0].boundary_type = 'vacuum'
z_list[-1].boundary_type = 'vacuum'
[6]:
fuel_list = []
gap_list = []
clad_list = []
water_list = []
for i in range(1, 11):
    fuel_list.append(openmc.Cell(cell_id=i))
    gap_list.append(openmc.Cell(cell_id=i+10))
    clad_list.append(openmc.Cell(cell_id=i+20))
    water_list.append(openmc.Cell(cell_id=i+30))

for j, fuels in enumerate(fuel_list):
    fuels.region = -fuel_or & +z_list[j] & -z_list[j+1]
    fuels.fill = uo2
    fuels.temperature = 800.

for j, gaps in enumerate(gap_list):
    gaps.region = +fuel_or & -clad_ir & +z_list[j] & -z_list[j+1]
    gaps.fill = helium
    gaps.temperature = 700.

for j, clads in enumerate(clad_list):
    clads.region = +clad_ir & -clad_or & +z_list[j] & -z_list[j+1]
    clads.fill = zircaloy
    clads.temperature = 600.

for j, waters in enumerate(water_list):
    waters.region = +clad_or & +left & -right & +back & -front & +z_list[j] & -z_list[j+1]
    waters.fill = material_list[j+3]
    waters.temperature = 500.
[7]:
root = openmc.Universe(name='root universe')
root.add_cells(fuel_list)
root.add_cells(gap_list)
root.add_cells(clad_list)
root.add_cells(water_list)
geometry_file = openmc.Geometry(root)
geometry_file.export_to_xml()

If you are coupling this externally to a heat transfer solver, you will want to know the heat deposited by each fuel cell. So let’s create a cell filter for the recoverable fission heat.

[8]:
cell_filter = openmc.CellFilter(fuel_list)
t = openmc.Tally(tally_id=1)
t.filters.append(cell_filter)
t.scores = ['fission-q-recoverable']
tallies = openmc.Tallies([t])
tallies.export_to_xml()

Let’s plot our geometry to make sure it looks like we expect. Since we made new water materials in each axial cell, and we have centered the plot at 150, we should see one color for the water material in the bottom half and a different color for the water material in the top half.

[19]:
root.plot(basis='yz', width=[2, 10], color_by='material', origin=[0., 0., 150.], pixels=[400, 400])
[19]:
<matplotlib.image.AxesImage at 0x126d642e0>
_images/examples_capi_14_1.png

Settings: everything will be standard except for the temperature settings. Since we will be working with specified temperatures, you will need temperature dependent data. I typically use the endf data found here: https://openmc.org/official-data-libraries/ Make sure your cross sections environment variable is pointing to temperature-dependent data before using the following settings.

[13]:
lower_left = [-0.62992, -pitch/2, 0]
upper_right = [+0.62992, +pitch/2, +300]
uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True)

settings_file = openmc.Settings()
settings_file.batches = 100
settings_file.inactive = 10
settings_file.particles = 10000
settings_file.temperature = {'multipole': True, 'method': 'interpolation', 'range': [290, 2500]}
settings_file.source = openmc.source.Source(space=uniform_dist)
settings_file.export_to_xml()

To run a regular simulation, just use openmc.run(). However, we want to run a simulation that we can stop in the middle and update the material and cell properties. So we will use openmc.lib.

[14]:
openmc.lib.init()
openmc.lib.simulation_init()

There are 10 inactive batches, so we need to run next_batch() at least 10 times before the tally is activated.

[15]:
for _ in range(14):
    openmc.lib.next_batch()

Let’s take a look at the tally. There are 10 entries, one for each cell in the fuel.

[16]:
t = openmc.lib.tallies[1]
print(t.mean)
[[ 4178272.4202991 ]
 [ 9595363.82759911]
 [12307462.30060902]
 [11772927.66594472]
 [11892601.29001472]
 [12203397.88895767]
 [12851791.20965905]
 [11760027.45873386]
 [ 9293110.94735569]
 [ 4511597.61592287]]

Now, let’s make some changes to the temperatures. For this, we need to identify each cell by its id. We can use get_temperature() to compare the temperatures of the cells before and after the change.

[17]:
print("fuel temperature is: ")
print(openmc.lib.cells[5].get_temperature())
print("gap temperature is: ")
print(openmc.lib.cells[15].get_temperature())
print("clad temperature is: ")
print(openmc.lib.cells[25].get_temperature())
print("water temperature is: ")
print(openmc.lib.cells[35].get_temperature())
fuel temperature is:
800.0
gap temperature is:
700.0
clad temperature is:
600.0
water temperature is:
500.00000000000006
[18]:
for i in range(1, 11):
    temp = 900.0
    openmc.lib.cells[i].set_temperature(temp)
[19]:
print("fuel temperature is: ")
print(openmc.lib.cells[5].get_temperature())
fuel temperature is:
899.9999999999999

Let’s make a similar change for the water density. Again, we need to identify each material by its id.

[20]:
for i in range(4, 14):
    density = 0.65
    openmc.lib.materials[i].set_density(density, units='g/cm3')

The new batches we run will use the new material and cell properties.

[21]:
for _ in range(14):
    openmc.lib.next_batch()

When you’re ready to end the simulation, use the following:

[22]:
openmc.lib.simulation_finalize()
openmc.lib.finalize()

Functional Expansions

OpenMC’s general tally system accommodates a wide range of tally filters. While most filters are meant to identify regions of phase space that contribute to a tally, there are a special set of functional expansion filters that will multiply the tally by a set of orthogonal functions, e.g. Legendre polynomials, so that continuous functions of space or angle can be reconstructed from the tallied moments.

In this example, we will determine the spatial dependence of the flux along the \(z\) axis by making a Legendre polynomial expansion. Let us represent the flux along the z axis, \(\phi(z)\), by the function

\[\phi(z') = \sum\limits_{n=0}^N a_n P_n(z') \]

where \(z'\) is the position normalized to the range [-1, 1]. Since \(P_n(z')\) are known functions, our only task is to determine the expansion coefficients, \(a_n\). By the orthogonality properties of the Legendre polynomials, one can deduce that the coefficients, \(a_n\), are given by

\[a_n = \frac{2n + 1}{2} \int_{-1}^1 dz' P_n(z') \phi(z'). \]

Thus, the problem reduces to finding the integral of the flux times each Legendre polynomial – a problem which can be solved by using a Monte Carlo tally. By using a Legendre polynomial filter, we obtain stochastic estimates of these integrals for each polynomial order.

[1]:
%matplotlib inline
import openmc
import numpy as np
import matplotlib.pyplot as plt

To begin, let us first create a simple model. The model will be a slab of fuel material with reflective boundaries conditions in the x- and y-directions and vacuum boundaries in the z-direction. However, to make the distribution slightly more interesting, we’ll put some B4C in the middle of the slab.

[2]:
# Define fuel and B4C materials
fuel = openmc.Material()
fuel.add_element('U', 1.0, enrichment=4.5)
fuel.add_nuclide('O16', 2.0)
fuel.set_density('g/cm3', 10.0)

b4c = openmc.Material()
b4c.add_element('B', 4.0)
b4c.add_element('C', 1.0)
b4c.set_density('g/cm3', 2.5)
[3]:
# Define surfaces used to construct regions
zmin, zmax = -10., 10.
box = openmc.model.rectangular_prism(10., 10., boundary_type='reflective')
bottom = openmc.ZPlane(z0=zmin, boundary_type='vacuum')
boron_lower = openmc.ZPlane(z0=-0.5)
boron_upper = openmc.ZPlane(z0=0.5)
top = openmc.ZPlane(z0=zmax, boundary_type='vacuum')

# Create three cells and add them to geometry
fuel1 = openmc.Cell(fill=fuel, region=box & +bottom & -boron_lower)
absorber = openmc.Cell(fill=b4c, region=box & +boron_lower & -boron_upper)
fuel2 = openmc.Cell(fill=fuel, region=box & +boron_upper & -top)
geom = openmc.Geometry([fuel1, absorber, fuel2])

For the starting source, we’ll use a uniform distribution over the entire box geometry.

[4]:
settings = openmc.Settings()
spatial_dist = openmc.stats.Box(*geom.bounding_box)
settings.source = openmc.Source(space=spatial_dist)
settings.batches = 210
settings.inactive = 10
settings.particles = 1000

Defining the tally is relatively straightforward. One simply needs to list ‘flux’ as a score and then add an expansion filter. For this case, we will want to use the SpatialLegendreFilter class which multiplies tally scores by Legendre polynomials evaluated on normalized spatial positions along an axis.

[5]:
# Create a flux tally
flux_tally = openmc.Tally()
flux_tally.scores = ['flux']

# Create a Legendre polynomial expansion filter and add to tally
order = 8
expand_filter = openmc.SpatialLegendreFilter(order, 'z', zmin, zmax)
flux_tally.filters.append(expand_filter)

The last thing we need to do is create a Tallies collection and export the entire model, which we’ll do using the Model convenience class.

[6]:
tallies = openmc.Tallies([flux_tally])
model = openmc.model.Model(geometry=geom, settings=settings, tallies=tallies)

Running a simulation is now as simple as calling the run() method of Model.

[7]:
sp_file = model.run(output=False)

Now that the run is finished, we need to load the results from the statepoint file.

[8]:
with openmc.StatePoint(sp_file) as sp:
    df = sp.tallies[flux_tally.id].get_pandas_dataframe()

We’ve used the get_pandas_dataframe() method that returns tally data as a Pandas dataframe. Let’s see what the raw data looks like.

[9]:
df
[9]:
spatiallegendre nuclide score mean std. dev.
0 P0 total flux 36.523601 0.081540
1 P1 total flux -0.002830 0.041466
2 P2 total flux -4.411923 0.027161
3 P3 total flux 0.004316 0.020245
4 P4 total flux -0.277281 0.014558
5 P5 total flux 0.010604 0.011350
6 P6 total flux 0.109212 0.010280
7 P7 total flux -0.002705 0.009100
8 P8 total flux -0.088469 0.007889

Since the expansion coefficients are given as

\[a_n = \frac{2n + 1}{2} \int_{-1}^1 dz' P_n(z') \phi(z') \]

we just need to multiply the Legendre moments by \((2n + 1)/2\).

[10]:
n = np.arange(order + 1)
a_n = (2*n + 1)/2 * df['mean']

To plot the flux distribution, we can use the numpy.polynomial.Legendre class which represents a truncated Legendre polynomial series. Since we really want to plot \(\phi(z)\) and not \(\phi(z')\) we first need to perform a change of variables. Since

\[\lvert \phi(z) dz \rvert = \lvert \phi(z') dz' \rvert \]

and, for this case, \(z = 10z'\), it follows that

\[\phi(z) = \frac{\phi(z')}{10} = \sum_{n=0}^N \frac{a_n}{10} P_n(z'). \]
[11]:
phi = np.polynomial.Legendre(a_n/10, domain=(zmin, zmax))

Let’s plot it and see how our flux looks!

[12]:
z = np.linspace(zmin, zmax, 1000)
plt.plot(z, phi(z))
plt.xlabel('Z position [cm]')
plt.ylabel('Flux [n/src]')
[12]:
Text(0, 0.5, 'Flux [n/src]')
_images/examples_expansion-filters_22_1.png

As you might expect, we get a rough cosine shape but with a flux depression in the middle due to the boron slab that we introduced. To get a more accurate distribution, we’d likely need to use a higher order expansion.

One more thing we can do is confirm that integrating the distribution gives us the same value as the first moment (since \(P_0(z') = 1\)). This can easily be done by numerically integrating using the trapezoidal rule:

[13]:
np.trapz(phi(z), z)
[13]:
36.523562389125146

In addition to being able to tally Legendre moments, there are also functional expansion filters available for spherical harmonics (SphericalHarmonicsFilter) and Zernike polynomials over a unit disk (ZernikeFilter). A separate LegendreFilter class can also be used for determining Legendre scattering moments (i.e., an expansion of the scattering cosine, \(\mu\)).

Zernike polynomials

Now let’s look at an example of functional expansion tallies using Zernike polynomials as the basis functions.

In this example, we will determine the spatial dependence of the flux along the radial direction \(r'\) and \(/\) or azimuthal angle \(\theta\) by making a Zernike polynomial expansion. Let us represent the flux along the radial and azimuthal direction, \(\phi(r', \theta)\), by the function

\[ \phi(r', \theta) = \sum\limits_{n=0}^N \sum\limits_{m=-n}^n a_n^m Z_n^m(r', \theta)\]

where \(r'\) is the position normalized to the range [0, r] (r is the radius of cylindrical geometry), and the azimuthal lies within the range [0, $ 2:nbsphinx-math:`pi`$].

Since \(Z_n^m(r', \theta)\) are known functions, we need to determine the expansion coefficients, \(a_n^m\). By the orthogonality properties of the Zernike polynomials, one can deduce that the coefficients, \(a_n^m\), are given by

\[a_n^m = k_n^m \int_{0}^r dr' \int_{0}^{2\pi} d\theta Z_n^m(r',\theta) \phi(r', \theta). \]
\[k_n^m = \frac{2n + 2}{\pi}, m \ne 0. \]
\[k_n^m = \frac{n+1}{\pi}, m = 0. \]

Similarly, the problem reduces to finding the integral of the flux times each Zernike polynomial.

To begin with, let us first create a simple model. The model will be a pin-cell fuel material with vacuum boundary condition in both radial direction and axial direction.

[14]:
# Define fuel
fuel = openmc.Material()
fuel.add_element('U', 1.0, enrichment=5.0)
fuel.add_nuclide('O16', 2.0)
fuel.set_density('g/cm3', 10.0)
[15]:
# Define surfaces used to construct regions
zmin, zmax, radius = -1., 1., 0.5
pin = openmc.ZCylinder(x0=0.0, y0=0.0, r=radius, boundary_type='vacuum')
bottom = openmc.ZPlane(z0=zmin, boundary_type='vacuum')
top = openmc.ZPlane(z0=zmax, boundary_type='vacuum')

# Create three cells and add them to geometry
fuel = openmc.Cell(fill=fuel, region= -pin & +bottom & -top)
geom = openmc.Geometry([fuel])

For the starting source, we’ll use a uniform distribution over the entire box geometry.

[16]:
settings = openmc.Settings()
spatial_dist = openmc.stats.Box(*geom.bounding_box)
settings.source = openmc.Source(space=spatial_dist)
settings.batches = 100
settings.inactive = 20
settings.particles = 100000

Defining the tally is relatively straightforward. One simply needs to list ‘flux’ as a score and then add an expansion filter. For this case, we will want to use the SpatialLegendreFilter, ZernikeFilter, ZernikeRadialFilter classes which multiplies tally scores by Legendre, azimuthal Zernike and radial-only Zernike polynomials evaluated on normalized spatial positions along radial and axial directions.

[17]:
# Create a flux tally
flux_tally_legendre = openmc.Tally()
flux_tally_legendre.scores = ['flux']

# Create a Legendre polynomial expansion filter and add to tally
order = 10
cell_filter = openmc.CellFilter(fuel)
legendre_filter = openmc.SpatialLegendreFilter(order, 'z', zmin, zmax)
flux_tally_legendre.filters = [cell_filter, legendre_filter]

# Create a Zernike azimuthal polynomial expansion filter and add to tally
flux_tally_zernike = openmc.Tally()
flux_tally_zernike.scores = ['flux']
zernike_filter = openmc.ZernikeFilter(order=order, x=0.0, y=0.0, r=radius)
flux_tally_zernike.filters = [cell_filter, zernike_filter]

# Create a Zernike radial polynomial expansion filter and add to tally
flux_tally_zernike1d = openmc.Tally()
flux_tally_zernike1d.scores = ['flux']
zernike1d_filter = openmc.ZernikeRadialFilter(order=order, x=0.0, y=0.0, r=radius)
flux_tally_zernike1d.filters = [cell_filter, zernike1d_filter]

The last thing we need to do is create a Tallies collection and export the entire model, which we’ll do using the Model convenience class.

[18]:
tallies = openmc.Tallies([flux_tally_legendre, flux_tally_zernike, flux_tally_zernike1d])
model = openmc.model.Model(geometry=geom, settings=settings, tallies=tallies)

Running a simulation is now as simple as calling the run() method of Model.

[19]:
sp_file = model.run(output=False)

Now that the run is finished, we need to load the results from the statepoint file.

[20]:
with openmc.StatePoint(sp_file) as sp:
    df1 = sp.tallies[flux_tally_legendre.id].get_pandas_dataframe()

We’ve used the get_pandas_dataframe() method that returns tally data as a Pandas dataframe. Let’s see what the raw data looks like.

[21]:
df1
[21]:
cell spatiallegendre nuclide score mean std. dev.
0 4 P0 total flux 0.543425 0.000599
1 4 P1 total flux 0.000635 0.000523
2 4 P2 total flux -0.055989 0.000342
3 4 P3 total flux -0.000053 0.000284
4 4 P4 total flux -0.000749 0.000230
5 4 P5 total flux 0.000111 0.000149
6 4 P6 total flux -0.000692 0.000177
7 4 P7 total flux -0.000064 0.000152
8 4 P8 total flux -0.000139 0.000142
9 4 P9 total flux 0.000201 0.000119
10 4 P10 total flux -0.000051 0.000112

Since the scaling factors for expansion coefficients will be provided by the Python API, thus, we do not need to multiply the moments by scaling factors.

[22]:
a_n = df1['mean']

Loading the coefficients is realized via calling the OpenMC Python API as follows:

[23]:
phi = openmc.legendre_from_expcoef(a_n, domain=(zmin, zmax))

Let’s plot it and see how our flux looks!

[24]:
z = np.linspace(zmin, zmax, 1000)
plt.plot(z, phi(z))
plt.xlabel('Z position [cm]')
plt.ylabel('Flux [n/src]')
[24]:
Text(0, 0.5, 'Flux [n/src]')
_images/examples_expansion-filters_47_1.png

A rough cosine shape is obtained. One can also numerically integrate the function using the trapezoidal rule.

[25]:
np.trapz(phi(z), z)
[25]:
0.543424143829605

The following cases show how to reconstruct the flux distribution Zernike polynomials tallied results.

[26]:
with openmc.StatePoint(sp_file) as sp:
    df2 = sp.tallies[flux_tally_zernike.id].get_pandas_dataframe()
[27]:
df2
[27]:
cell zernike nuclide score mean std. dev.
0 4 Z0,0 total flux 0.543425 0.000599
1 4 Z1,-1 total flux -0.000236 0.000398
2 4 Z1,1 total flux 0.000126 0.000325
3 4 Z2,-2 total flux -0.000104 0.000206
4 4 Z2,0 total flux -0.064291 0.000404
... ... ... ... ... ... ...
61 4 Z10,2 total flux -0.000130 0.000099
62 4 Z10,4 total flux -0.000057 0.000092
63 4 Z10,6 total flux -0.000048 0.000109
64 4 Z10,8 total flux -0.000056 0.000092
65 4 Z10,10 total flux -0.000046 0.000098

66 rows × 6 columns

Let’s plot the flux in radial direction with specific azimuthal angle (\(\theta = 0.0\)).

[28]:
z_n = df2['mean']
zz = openmc.Zernike(z_n, radius)
rr = np.linspace(0, radius, 100)
plt.plot(rr, zz(rr, 0.0))
plt.xlabel('Radial position [cm]')
plt.ylabel('Flux')
[28]:
Text(0, 0.5, 'Flux')
_images/examples_expansion-filters_54_1.png

A polar figure with all azimuthal can be plotted like this:

[29]:
z_n = df2['mean']
zz = openmc.Zernike(z_n, radius=radius)
#
# Using linspace so that the endpoint of 360 is included...
azimuths = np.radians(np.linspace(0, 360, 50))
zeniths = np.linspace(0, radius, 100)
r, theta = np.meshgrid(zeniths, azimuths)
values = zz(zeniths, azimuths)
fig, ax = plt.subplots(subplot_kw=dict(projection='polar'))
ax.contourf(theta, r, values, cmap='jet')
plt.show()
_images/examples_expansion-filters_56_0.png

Sometimes, we just need the radial-only Zernike polynomial tallied flux distribution. Let us extract the tallied coefficients first.

[30]:
with openmc.StatePoint(sp_file) as sp:
    df3 = sp.tallies[flux_tally_zernike1d.id].get_pandas_dataframe()
[31]:
df3
[31]:
cell zernikeradial nuclide score mean std. dev.
0 4 Z0,0 total flux 0.543425 0.000599
1 4 Z2,0 total flux -0.064291 0.000404
2 4 Z4,0 total flux -0.000601 0.000223
3 4 Z6,0 total flux -0.000454 0.000227
4 4 Z8,0 total flux -0.000011 0.000166
5 4 Z10,0 total flux -0.000102 0.000161

A plot along with r-axis is also done.

[32]:
z_n = df3['mean']
zz = openmc.ZernikeRadial(z_n, radius=radius)
rr = np.linspace(0, radius, 50)
plt.plot(rr, zz(rr))
plt.xlabel('Radial position [cm]')
plt.ylabel('Flux')
[32]:
Text(0, 0.5, 'Flux')
_images/examples_expansion-filters_61_1.png

Similarly, we can also re-construct the polar figure based on radial-only Zernike polinomial coefficients.

[33]:
z_n = df3['mean']
zz = openmc.ZernikeRadial(z_n, radius=radius)
azimuths = np.radians(np.linspace(0, 360, 50))
zeniths = np.linspace(0, radius, 100)
r, theta = np.meshgrid(zeniths, azimuths)
values = [[i for i in zz(zeniths)] for j in range(len(azimuths))]
fig, ax = plt.subplots(subplot_kw=dict(projection='polar'), figsize=(6,6))
ax.contourf(theta, r, values, cmap='jet')
plt.show()
_images/examples_expansion-filters_63_0.png

Based on Legendre polynomial coefficients and the azimuthal or radial-only Zernike coefficient, it’s possible to reconstruct the flux both on radial and axial directions.

[34]:
# Reconstruct 3-D flux based on radial only Zernike and Legendre polynomials
z_n = df3['mean']
zz = openmc.ZernikeRadial(z_n, radius=radius)
azimuths = np.radians(np.linspace(0, 360, 100)) # azimuthal mesh
zeniths = np.linspace(0, radius, 100) # radial mesh
zmin, zmax = -1.0, 1.0
z = np.linspace(zmin, zmax, 100) # axial mesh
#
# flux = np.matmul(np.matrix(phi(z)).transpose(), np.matrix(zz(zeniths)))
# flux = np.array(flux) # change np.matrix to np.array
# np.matrix is not recommended for use anymore
flux = np.array([phi(z)]).T @ np.array([zz(zeniths)])
#
plt.figure(figsize=(5,10))
plt.title('Flux distribution')
plt.xlabel('Radial Position [cm]')
plt.ylabel('Axial Height [cm]')
plt.pcolor(zeniths, z, flux, cmap='jet')
plt.colorbar()
[34]:
<matplotlib.colorbar.Colorbar at 0x7ffe048a2f90>
_images/examples_expansion-filters_65_1.png

One can also reconstruct the 3D flux distribution based on Legendre and Zernike polynomial tallied coefficients.

[35]:
# Define needed function first
def cart2pol(x, y):
    rho = np.sqrt(x**2 + y**2)
    phi = np.arctan2(y, x)
    return(rho, phi)

# Reconstruct 3-D flux based on azimuthal Zernike and Legendre polynomials
z_n = df2['mean']
zz = openmc.Zernike(z_n, radius=radius)
#
xstep = 2.0*radius/20
hstep = (zmax - zmin)/20
x = np.linspace(-radius, radius, 50)
x = np.array(x)
[X,Y] = np.meshgrid(x,x)
h = np.linspace(zmin, zmax, 50)
h = np.array(h)
[r, theta] = cart2pol(X,Y)
flux3d = np.zeros((len(x), len(x), len(h)))
flux3d.fill(np.nan)
#
for i in range(len(x)):
    for j in range(len(x)):
        if r[i][j]<=radius:
            for k in range(len(h)):
                flux3d[i][j][k] = phi(h[k]) * zz(r[i][j], theta[i][j])

Let us print out with VTK format.

[36]:
# You'll need to install pyevtk as a prerequisite
from pyevtk.hl import gridToVTK
import numpy as np
#
# Dimensions
nx, ny, nz = len(x), len(x), len(h)
lx, ly, lz = 2.0*radius, 2.0*radius, (zmax-zmin)
dx, dy, dz = lx/nx, ly/ny, lz/nz
#
ncells = nx * ny * nz
npoints = (nx + 1) * (ny + 1) * (nz + 1)
#
# Coordinates
x = np.arange(0, lx + 0.1*dx, dx, dtype='float64')
y = np.arange(0, ly + 0.1*dy, dy, dtype='float64')
z = np.arange(0, lz + 0.1*dz, dz, dtype='float64')
# Print out
path = gridToVTK("./rectilinear", x, y, z, cellData = {"flux3d" : flux3d})

Use VisIt or ParaView to plot it as you want. Then, the plot can be loaded and shown as follows.

[37]:
f1 = plt.imread('./images/flux3d.png')
plt.imshow(f1, cmap='jet')
[37]:
<matplotlib.image.AxesImage at 0x7ffe050592d0>
_images/examples_expansion-filters_71_1.png
[ ]:

Nuclear Data

In this notebook, we will go through the salient features of the openmc.data package in the Python API. This package enables inspection, analysis, and conversion of nuclear data from ACE files. Most importantly, the package provides a mean to generate HDF5 nuclear data libraries that are used by the transport solver.

[1]:
%matplotlib inline
import os
from pprint import pprint
import shutil
import subprocess
import urllib.request

import h5py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm
from matplotlib.patches import Rectangle

import openmc.data
Physical Data

Some very helpful physical data is available as part of openmc.data: atomic masses, natural abundances, and atomic weights.

[2]:
openmc.data.atomic_mass('Fe54')
[2]:
53.939608306
[3]:
openmc.data.NATURAL_ABUNDANCE['H2']
[3]:
0.00015574
[4]:
openmc.data.atomic_weight('C')
[4]:
12.011115164864455
The IncidentNeutron class

The most useful class within the openmc.data API is IncidentNeutron, which stores to continuous-energy incident neutron data. This class has factory methods from_ace, from_endf, and from_hdf5 which take a data file on disk and parse it into a hierarchy of classes in memory. To demonstrate this feature, we will download an ACE file (which can be produced with NJOY 2016) and then load it in using the IncidentNeutron.from_ace method.

[5]:
url = 'https://anl.box.com/shared/static/kxm7s57z3xgfbeq29h54n7q6js8rd11c.ace'
filename, headers = urllib.request.urlretrieve(url, 'gd157.ace')
[6]:
# Load ACE data into object
gd157 = openmc.data.IncidentNeutron.from_ace('gd157.ace')
gd157
[6]:
<IncidentNeutron: Gd157>
Cross sections

From Python, it’s easy to explore (and modify) the nuclear data. Let’s start off by reading the total cross section. Reactions are indexed using their “MT” number – a unique identifier for each reaction defined by the ENDF-6 format. The MT number for the total cross section is 1.

[7]:
total = gd157[1]
total
[7]:
<Reaction: MT=1 (n,total)>

Cross sections for each reaction can be stored at multiple temperatures. To see what temperatures are available, we can look at the reaction’s xs attribute.

[8]:
total.xs
[8]:
{'294K': <openmc.data.function.Tabulated1D at 0x1461b4d2f160>}

To find the cross section at a particular energy, 1 eV for example, simply get the cross section at the appropriate temperature and then call it as a function. Note that our nuclear data uses eV as the unit of energy.

[9]:
total.xs['294K'](1.0)
[9]:
142.64747

The xs attribute can also be called on an array of energies.

[10]:
total.xs['294K']([1.0, 2.0, 3.0])
[10]:
array([142.64747   ,  38.6541761 , 175.40019642])

A quick way to plot cross sections is to use the energy attribute of IncidentNeutron. This gives an array of all the energy values used in cross section interpolation for each temperature present.

[11]:
gd157.energy
[11]:
{'294K': array([1.0000e-05, 1.0325e-05, 1.0650e-05, ..., 1.9500e+07, 1.9900e+07,
        2.0000e+07])}
[12]:
energies = gd157.energy['294K']
total_xs = total.xs['294K'](energies)
plt.loglog(energies, total_xs)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
[12]:
Text(0, 0.5, 'Cross section (b)')
_images/examples_nuclear-data_20_1.png
Reaction Data

Most of the interesting data for an IncidentNeutron instance is contained within the reactions attribute, which is a dictionary mapping MT values to Reaction objects.

[13]:
pprint(list(gd157.reactions.values())[:10])
[<Reaction: MT=1 (n,total)>,
 <Reaction: MT=101 (n,disappear)>,
 <Reaction: MT=301 heating>,
 <Reaction: MT=2 (n,elastic)>,
 <Reaction: MT=16 (n,2n)>,
 <Reaction: MT=17 (n,3n)>,
 <Reaction: MT=22 (n,na)>,
 <Reaction: MT=24 (n,2na)>,
 <Reaction: MT=28 (n,np)>,
 <Reaction: MT=41 (n,2np)>]

Let’s suppose we want to look more closely at the (n,2n) reaction. This reaction has an energy threshold

[14]:
n2n = gd157[16]
print('Threshold = {} eV'.format(n2n.xs['294K'].x[0]))
Threshold = 6400881.0 eV

The (n,2n) cross section, like all basic cross sections, is represented by the Tabulated1D class. The energy and cross section values in the table can be directly accessed with the x and y attributes. Using the x and y has the nice benefit of automatically acounting for reaction thresholds.

[15]:
n2n.xs
[15]:
{'294K': <openmc.data.function.Tabulated1D at 0x1461b4d594e0>}
[16]:
xs = n2n.xs['294K']
plt.plot(xs.x, xs.y)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
plt.xlim((xs.x[0], xs.x[-1]))
[16]:
(6400881.0, 20000000.0)
_images/examples_nuclear-data_27_1.png

To get information on the energy and angle distribution of the neutrons emitted in the reaction, we need to look at the products attribute.

[17]:
n2n.products
[17]:
[<Product: neutron, emission=prompt, yield=polynomial>,
 <Product: photon, emission=prompt, yield=tabulated>]
[18]:
neutron = n2n.products[0]
neutron.distribution
[18]:
[<openmc.data.correlated.CorrelatedAngleEnergy at 0x1461b4a66908>]

We see that the neutrons emitted have a correlated angle-energy distribution. Let’s look at the energy_out attribute to see what the outgoing energy distributions are.

[19]:
dist = neutron.distribution[0]
dist.energy_out
[19]:
[<openmc.stats.univariate.Tabular at 0x1461b4d59550>,
 <openmc.stats.univariate.Tabular at 0x1461b4d59630>,
 <openmc.stats.univariate.Tabular at 0x1461b4d596d8>,
 <openmc.stats.univariate.Tabular at 0x1461b4d59828>,
 <openmc.stats.univariate.Tabular at 0x1461b4d59a20>,
 <openmc.stats.univariate.Tabular at 0x1461b4d59c88>,
 <openmc.stats.univariate.Tabular at 0x1461b4d59f98>,
 <openmc.stats.univariate.Tabular at 0x1461b4d63358>,
 <openmc.stats.univariate.Tabular at 0x1461b4d63748>,
 <openmc.stats.univariate.Tabular at 0x1461b4d63ba8>,
 <openmc.stats.univariate.Tabular at 0x1461b4d6b0b8>,
 <openmc.stats.univariate.Tabular at 0x1461b4d6b588>,
 <openmc.stats.univariate.Tabular at 0x1461b4d6bb00>,
 <openmc.stats.univariate.Tabular at 0x1461b4aaa0f0>,
 <openmc.stats.univariate.Tabular at 0x1461b4aaa710>,
 <openmc.stats.univariate.Tabular at 0x1461b4aaad68>,
 <openmc.stats.univariate.Tabular at 0x1461b4ab1470>,
 <openmc.stats.univariate.Tabular at 0x1461b4ab1b70>,
 <openmc.stats.univariate.Tabular at 0x1461b4ab92b0>,
 <openmc.stats.univariate.Tabular at 0x1461b4ab9a20>,
 <openmc.stats.univariate.Tabular at 0x1461b4ac1208>,
 <openmc.stats.univariate.Tabular at 0x1461b4ac19e8>,
 <openmc.stats.univariate.Tabular at 0x1461b4ac9240>,
 <openmc.stats.univariate.Tabular at 0x1461b4ac9a90>,
 <openmc.stats.univariate.Tabular at 0x1461b4acf358>,
 <openmc.stats.univariate.Tabular at 0x1461b4acfc18>,
 <openmc.stats.univariate.Tabular at 0x1461b4ad7470>,
 <openmc.stats.univariate.Tabular at 0x1461b4ad7d30>,
 <openmc.stats.univariate.Tabular at 0x1461b4ae0668>,
 <openmc.stats.univariate.Tabular at 0x1461b4ae0f98>]

Here we see we have a tabulated outgoing energy distribution for each incoming energy. Note that the same probability distribution classes that we could use to create a source definition are also used within the openmc.data package. Let’s plot every fifth distribution to get an idea of what they look like.

[20]:
for e_in, e_out_dist in zip(dist.energy[::5], dist.energy_out[::5]):
    plt.semilogy(e_out_dist.x, e_out_dist.p, label='E={:.2f} MeV'.format(e_in/1e6))
plt.ylim(top=1e-6)
plt.legend()
plt.xlabel('Outgoing energy (eV)')
plt.ylabel('Probability/eV')
plt.show()
_images/examples_nuclear-data_34_0.png
Unresolved resonance probability tables

We can also look at unresolved resonance probability tables which are stored in a ProbabilityTables object. In the following example, we’ll create a plot showing what the total cross section probability tables look like as a function of incoming energy.

[21]:
fig = plt.figure()
ax = fig.add_subplot(111)
cm = matplotlib.cm.Spectral_r

# Determine size of probability tables
urr = gd157.urr['294K']
n_energy = urr.table.shape[0]
n_band = urr.table.shape[2]

for i in range(n_energy):
    # Get bounds on energy
    if i > 0:
        e_left = urr.energy[i] - 0.5*(urr.energy[i] - urr.energy[i-1])
    else:
        e_left = urr.energy[i] - 0.5*(urr.energy[i+1] - urr.energy[i])

    if i < n_energy - 1:
        e_right = urr.energy[i] + 0.5*(urr.energy[i+1] - urr.energy[i])
    else:
        e_right = urr.energy[i] + 0.5*(urr.energy[i] - urr.energy[i-1])

    for j in range(n_band):
        # Determine maximum probability for a single band
        max_prob = np.diff(urr.table[i,0,:]).max()

        # Determine bottom of band
        if j > 0:
            xs_bottom = urr.table[i,1,j] - 0.5*(urr.table[i,1,j] - urr.table[i,1,j-1])
            value = (urr.table[i,0,j] - urr.table[i,0,j-1])/max_prob
        else:
            xs_bottom = urr.table[i,1,j] - 0.5*(urr.table[i,1,j+1] - urr.table[i,1,j])
            value = urr.table[i,0,j]/max_prob

        # Determine top of band
        if j < n_band - 1:
            xs_top = urr.table[i,1,j] + 0.5*(urr.table[i,1,j+1] - urr.table[i,1,j])
        else:
            xs_top = urr.table[i,1,j] + 0.5*(urr.table[i,1,j] - urr.table[i,1,j-1])

        # Draw rectangle with appropriate color
        ax.add_patch(Rectangle((e_left, xs_bottom), e_right - e_left, xs_top - xs_bottom,
                     color=cm(value)))

# Overlay total cross section
ax.plot(gd157.energy['294K'], total.xs['294K'](gd157.energy['294K']), 'k')

# Make plot pretty and labeled
ax.set_xlim(1.0, 1.0e5)
ax.set_ylim(1e-1, 1e4)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('Energy (eV)')
ax.set_ylabel('Cross section(b)')
[21]:
Text(0, 0.5, 'Cross section(b)')
_images/examples_nuclear-data_36_1.png
Exporting HDF5 data

If you have an instance IncidentNeutron that was created from ACE or HDF5 data, you can easily write it to disk using the export_to_hdf5() method. This can be used to convert ACE to HDF5 or to take an existing data set and actually modify cross sections.

[22]:
gd157.export_to_hdf5('gd157.h5', 'w')

With few exceptions, the HDF5 file encodes the same data as the ACE file.

[23]:
gd157_reconstructed = openmc.data.IncidentNeutron.from_hdf5('gd157.h5')
np.all(gd157[16].xs['294K'].y == gd157_reconstructed[16].xs['294K'].y)
[23]:
True

And one of the best parts of using HDF5 is that it is a widely used format with lots of third-party support. You can use h5py, for example, to inspect the data.

[24]:
h5file = h5py.File('gd157.h5', 'r')
main_group = h5file['Gd157/reactions']
for name, obj in sorted(list(main_group.items()))[:10]:
    if 'reaction_' in name:
        print('{}, {}'.format(name, obj.attrs['label'].decode()))
reaction_002, (n,elastic)
reaction_016, (n,2n)
reaction_017, (n,3n)
reaction_022, (n,na)
reaction_024, (n,2na)
reaction_028, (n,np)
reaction_041, (n,2np)
reaction_051, (n,n1)
reaction_052, (n,n2)
reaction_053, (n,n3)
[25]:
n2n_group = main_group['reaction_016']
pprint(list(n2n_group.values()))
[<HDF5 group "/Gd157/reactions/reaction_016/294K" (1 members)>,
 <HDF5 group "/Gd157/reactions/reaction_016/product_0" (2 members)>,
 <HDF5 group "/Gd157/reactions/reaction_016/product_1" (2 members)>]

So we see that the hierarchy of data within the HDF5 mirrors the hierarchy of Python objects that we manipulated before.

[26]:
n2n_group['294K/xs'][()]
[26]:
array([0.000000e+00, 3.026796e-13, 1.291101e-02, 6.511110e-02,
       3.926270e-01, 5.752268e-01, 6.969600e-01, 7.399378e-01,
       9.635450e-01, 1.142130e+00, 1.308020e+00, 1.463500e+00,
       1.557600e+00, 1.640550e+00, 1.688960e+00, 1.711400e+00,
       1.739450e+00, 1.782070e+00, 1.816650e+00, 1.845280e+00,
       1.865409e+00, 1.867240e+00, 1.881558e+00, 1.881560e+00,
       1.881800e+00, 1.894470e+00, 1.869570e+00, 1.821200e+00,
       1.716000e+00, 1.600540e+00, 1.431620e+00, 1.283460e+00,
       1.101660e+00, 1.065300e+00, 9.307300e-01, 8.029800e-01,
       7.777400e-01])
Working with ENDF files

In addition to being able to load ACE and HDF5 data, we can also load ENDF data directly into an IncidentNeutron instance using the from_endf() factory method. Let’s download the ENDF/B-VII.1 evaluation for \(^{157}\)Gd and load it in:

[27]:
# Download ENDF file
url = 'https://t2.lanl.gov/nis/data/data/ENDFB-VII.1-neutron/Gd/157'
filename, headers = urllib.request.urlretrieve(url, 'gd157.endf')

# Load into memory
gd157_endf = openmc.data.IncidentNeutron.from_endf(filename)
gd157_endf
[27]:
<IncidentNeutron: Gd157>

Just as before, we can get a reaction by indexing the object directly:

[28]:
elastic = gd157_endf[2]

However, if we look at the cross section now, we see that it isn’t represented as tabulated data anymore.

[29]:
elastic.xs
[29]:
{'0K': <openmc.data.function.ResonancesWithBackground at 0x1461b11824a8>}

If you had Cython installed when you built/installed OpenMC, you should be able to evaluate resonant cross sections from ENDF data directly, i.e., OpenMC will reconstruct resonances behind the scenes for you.

[30]:
elastic.xs['0K'](0.0253)
[30]:
998.7871174521487

When data is loaded from an ENDF file, there is also a special resonances attribute that contains resolved and unresolved resonance region data (from MF=2 in an ENDF file).

[31]:
gd157_endf.resonances.ranges
[31]:
[<openmc.data.resonance.ReichMoore at 0x1461b1fcb898>,
 <openmc.data.resonance.Unresolved at 0x1461b2692ba8>]

We see that \(^{157}\)Gd has a resolved resonance region represented in the Reich-Moore format as well as an unresolved resonance region. We can look at the min/max energy of each region by doing the following:

[32]:
[(r.energy_min, r.energy_max) for r in gd157_endf.resonances.ranges]
[32]:
[(1e-05, 306.6), (306.6, 54881.1)]

With knowledge of the energy bounds, let’s create an array of energies over the entire resolved resonance range and plot the elastic scattering cross section.

[33]:
# Create log-spaced array of energies
resolved = gd157_endf.resonances.resolved
energies = np.logspace(np.log10(resolved.energy_min),
                       np.log10(resolved.energy_max), 1000)

# Evaluate elastic scattering xs at energies
xs = elastic.xs['0K'](energies)

# Plot cross section vs energies
plt.loglog(energies, xs)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
[33]:
Text(0, 0.5, 'Cross section (b)')
_images/examples_nuclear-data_59_1.png

Resonance ranges also have a useful parameters attribute that shows the energies and widths for resonances.

[34]:
resolved.parameters.head(10)
[34]:
energy L J neutronWidth captureWidth fissionWidthA fissionWidthB
0 0.0314 0 2.0 0.000474 0.1072 0.0 0.0
1 2.8250 0 2.0 0.000345 0.0970 0.0 0.0
2 16.2400 0 1.0 0.000400 0.0910 0.0 0.0
3 16.7700 0 2.0 0.012800 0.0805 0.0 0.0
4 20.5600 0 2.0 0.011360 0.0880 0.0 0.0
5 21.6500 0 2.0 0.000376 0.1140 0.0 0.0
6 23.3300 0 1.0 0.000813 0.1210 0.0 0.0
7 25.4000 0 2.0 0.001840 0.0850 0.0 0.0
8 40.1700 0 1.0 0.001307 0.1100 0.0 0.0
9 44.2200 0 2.0 0.008960 0.0960 0.0 0.0
Heavy-nuclide resonance scattering

OpenMC has two methods for accounting for resonance upscattering in heavy nuclides, DBRC and RVS. These methods rely on 0 K elastic scattering data being present. If you have an existing ACE/HDF5 dataset and you need to add 0 K elastic scattering data to it, this can be done using the IncidentNeutron.add_elastic_0K_from_endf() method. Let’s do this with our original gd157 object that we instantiated from an ACE file.

[35]:
gd157.add_elastic_0K_from_endf('gd157.endf')

Let’s check to make sure that we have both the room temperature elastic scattering cross section as well as a 0K cross section.

[36]:
gd157[2].xs
[36]:
{'294K': <openmc.data.function.Tabulated1D at 0x1461b4d2ff60>,
 '0K': <openmc.data.function.Tabulated1D at 0x1461b0e59128>}
Generating data from NJOY

To run OpenMC in continuous-energy mode, you generally need to have ACE files already available that can be converted to OpenMC’s native HDF5 format. If you don’t already have suitable ACE files or need to generate new data, both the IncidentNeutron and ThermalScattering classes include from_njoy() methods that will run NJOY to generate ACE files and then read those files to create OpenMC class instances. The from_njoy() methods take as input the name of an ENDF file on disk. By default, it is assumed that you have an executable named njoy available on your path. This can be configured with the optional njoy_exec argument. Additionally, if you want to show the progress of NJOY as it is running, you can pass stdout=True.

Let’s use IncidentNeutron.from_njoy() to run NJOY to create data for \(^2\)H using an ENDF file. We’ll specify that we want data specifically at 300, 400, and 500 K.

[37]:
# Download ENDF file
url = 'https://t2.lanl.gov/nis/data/data/ENDFB-VII.1-neutron/H/2'
filename, headers = urllib.request.urlretrieve(url, 'h2.endf')

# Run NJOY to create deuterium data
h2 = openmc.data.IncidentNeutron.from_njoy('h2.endf', temperatures=[300., 400., 500.], stdout=True)

 njoy 2016.49  25Jan19                                       07/19/19 06:12:49
 *****************************************************************************

 reconr...                                                                0.0s

 broadr...                                                                0.1s
      300.0 deg                                                           0.1s
      400.0 deg                                                           0.2s
      500.0 deg                                                           0.3s

 heatr...                                                                 0.3s

 gaspr...                                                                 0.6s

 purr...                                                                  0.7s

 mat =  128                                                               0.7s

 ---message from purr---mat  128 has no resonance parameters
                          copy as is to nout

 acer...                                                                  0.7s

 acer...                                                                  1.0s

 acer...                                                                  1.1s
                                                                          1.2s
 *****************************************************************************

Now we can use our h2 object just as we did before.

[38]:
h2[2].xs
[38]:
{'300K': <openmc.data.function.Tabulated1D at 0x1461b066ebe0>,
 '400K': <openmc.data.function.Tabulated1D at 0x1461b23a0978>,
 '500K': <openmc.data.function.Tabulated1D at 0x1461b1009400>,
 '0K': <openmc.data.function.Tabulated1D at 0x1461b101b358>}

Note that 0 K elastic scattering data is automatically added when using from_njoy() so that resonance elastic scattering treatments can be used.

Windowed multipole

OpenMC can also be used with an experimental format called windowed multipole. Windowed multipole allows for analytic on-the-fly Doppler broadening of the resolved resonance range. Windowed multipole data can be downloaded with the openmc-get-multipole-data script. This data can be used in the transport solver, but it can also be used directly in the Python API.

[39]:
url = 'https://github.com/mit-crpg/WMP_Library/releases/download/v1.1/092238.h5'
filename, headers = urllib.request.urlretrieve(url, '092238.h5')
[40]:
u238_multipole = openmc.data.WindowedMultipole.from_hdf5('092238.h5')

The WindowedMultipole object can be called with energy and temperature values. Calling the object gives a tuple of 3 cross sections: elastic scattering, radiative capture, and fission.

[41]:
u238_multipole(1.0, 294)
[41]:
(array(9.13284265), array(0.50530278), array(2.9316765e-06))

An array can be passed for the energy argument.

[42]:
E = np.linspace(5, 25, 1000)
plt.semilogy(E, u238_multipole(E, 293.606)[1])
[42]:
[<matplotlib.lines.Line2D at 0x1461b0ed9978>]
_images/examples_nuclear-data_77_1.png

The real advantage to multipole is that it can be used to generate cross sections at any temperature. For example, this plot shows the Doppler broadening of the 6.67 eV resonance between 0 K and 900 K.

[43]:
E = np.linspace(6.1, 7.1, 1000)
plt.semilogy(E, u238_multipole(E, 0)[1])
plt.semilogy(E, u238_multipole(E, 900)[1])
[43]:
[<matplotlib.lines.Line2D at 0x1461b09755c0>]
_images/examples_nuclear-data_79_1.png

Nuclear Data: Resonance Covariance

In this notebook we will explore features of the Python API that allow us to import and manipulate resonance covariance data. A full description of the ENDF-VI and ENDF-VII formats can be found in the ENDF102 manual.

[1]:
%matplotlib inline
import os
from pprint import pprint
import shutil
import subprocess
import urllib.request

import h5py
import numpy as np
import matplotlib.pyplot as plt

import openmc.data
ENDF: Resonance Covariance Data

Let’s download the ENDF/B-VII.1 evaluation for \(^{157}\)Gd and load it in:

[2]:
# Download ENDF file
url = 'https://t2.lanl.gov/nis/data/data/ENDFB-VII.1-neutron/Gd/157'
filename, headers = urllib.request.urlretrieve(url, 'gd157.endf')

# Load into memory
gd157_endf = openmc.data.IncidentNeutron.from_endf(filename, covariance=True)
gd157_endf
[2]:
<IncidentNeutron: Gd157>

We can access the parameters contained within File 32 in a similar manner to the File 2 parameters from before.

[3]:
gd157_endf.resonance_covariance.ranges[0].parameters[:5]
[3]:
energy J neutronWidth captureWidth fissionWidthA fissionWidthB L
0 0.0314 2.0 0.000474 0.1072 0.0 0.0 0
1 2.8250 2.0 0.000345 0.0970 0.0 0.0 0
2 16.2400 1.0 0.000400 0.0910 0.0 0.0 0
3 16.7700 2.0 0.012800 0.0805 0.0 0.0 0
4 20.5600 2.0 0.011360 0.0880 0.0 0.0 0

The newly created object will contain multiple resonance regions within gd157_endf.resonance_covariance.ranges. We can access the full covariance matrix from File 32 for a given range by:

[4]:
covariance = gd157_endf.resonance_covariance.ranges[0].covariance

This covariance matrix currently only stores the upper triangular portion as covariance matrices are symmetric. Plotting the covariance matrix:

[5]:
plt.imshow(covariance, cmap='seismic',vmin=-0.008, vmax=0.008)
plt.colorbar()
[5]:
<matplotlib.colorbar.Colorbar at 0x14cf90c73550>
_images/examples_nuclear-data-resonance-covariance_9_1.png

The correlation matrix can be constructed using the covariance matrix and also give some insight into the relations among the parameters.

[6]:
corr = np.zeros([len(covariance),len(covariance)])
for i in range(len(covariance)):
    for j in range(len(covariance)):
        corr[i, j]=covariance[i, j]/covariance[i, i]**(0.5)/covariance[j, j]**(0.5)
plt.imshow(corr, cmap='seismic',vmin=-1.0, vmax=1.0)
plt.colorbar()

[6]:
<matplotlib.colorbar.Colorbar at 0x14cf90b682e8>
_images/examples_nuclear-data-resonance-covariance_11_1.png
Sampling and Reconstruction

The covariance module also has the ability to sample a new set of parameters using the covariance matrix. Currently the sampling uses numpy.multivariate_normal(). Because parameters are assumed to have a multivariate normal distribution this method doesn’t not currently guarantee that sampled parameters will be positive.

[7]:
rm_resonance = gd157_endf.resonances.ranges[0]
n_samples = 5
samples = gd157_endf.resonance_covariance.ranges[0].sample(n_samples)
type(samples[0])

/home/romano/openmc/openmc/data/resonance_covariance.py:233: UserWarning: Sampling routine does not guarantee positive values for parameters. This can lead to undefined behavior in the reconstruction routine.
  warnings.warn(warn_str)
[7]:
openmc.data.resonance.ReichMoore

The sampling routine requires the incorporation of the openmc.data.ResonanceRange for the same resonance range object. This allows each sample itself to be its own openmc.data.ResonanceRange with a new set of parameters. Looking at some of the sampled parameters below:

[8]:
print('Sample 1')
samples[0].parameters[:5]
Sample 1
[8]:
energy L J neutronWidth captureWidth fissionWidthA fissionWidthB
0 0.030679 0 2.0 0.000473 0.108576 0.0 0.0
1 2.823843 0 2.0 0.000351 0.086418 0.0 0.0
2 16.281147 0 1.0 0.000458 0.106825 0.0 0.0
3 16.771760 0 2.0 0.013598 0.072837 0.0 0.0
4 20.561545 0 2.0 0.011164 0.086616 0.0 0.0
[9]:
print('Sample 2')
samples[1].parameters[:5]
Sample 2
[9]:
energy L J neutronWidth captureWidth fissionWidthA fissionWidthB
0 0.032858 0 2.0 0.000479 0.105208 0.0 0.0
1 2.823859 0 2.0 0.000361 0.093748 0.0 0.0
2 16.203069 0 1.0 0.000264 0.015233 0.0 0.0
3 16.765055 0 2.0 0.013648 0.076119 0.0 0.0
4 20.557679 0 2.0 0.011140 0.097548 0.0 0.0

We can reconstruct the cross section from the sampled parameters using the reconstruct method of openmc.data.ResonanceRange. For more on reconstruction see the Nuclear Data example notebook.

[10]:
gd157_endf.resonances.ranges
[10]:
[<openmc.data.resonance.ReichMoore at 0x14cf93855d68>,
 <openmc.data.resonance.Unresolved at 0x14cf938c02e8>]
[11]:
energy_range = [rm_resonance.energy_min, rm_resonance.energy_max]
energies = np.logspace(np.log10(energy_range[0]),
                       np.log10(energy_range[1]), 10000)
for sample in samples:
    xs = sample.reconstruct(energies)
    elastic_xs = xs[2]
    plt.loglog(energies, elastic_xs)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
[11]:
Text(0, 0.5, 'Cross section (b)')
_images/examples_nuclear-data-resonance-covariance_20_1.png
Subset Selection

Another capability of the covariance module is selecting a subset of the resonance parameters and the corresponding subset of the covariance matrix. We can do this by specifying the value we want to discriminate and the bounds within one energy region. Selecting only resonances with J=2:

[12]:
lower_bound = 2;  # inclusive
upper_bound = 2;  # inclusive
rm_res_cov_sub = gd157_endf.resonance_covariance.ranges[0].subset('J',[lower_bound,upper_bound])
rm_res_cov_sub.file2res.parameters[:5]
[12]:
energy L J neutronWidth captureWidth fissionWidthA fissionWidthB
0 0.0314 0 2.0 0.000474 0.1072 0.0 0.0
1 2.8250 0 2.0 0.000345 0.0970 0.0 0.0
3 16.7700 0 2.0 0.012800 0.0805 0.0 0.0
4 20.5600 0 2.0 0.011360 0.0880 0.0 0.0
5 21.6500 0 2.0 0.000376 0.1140 0.0 0.0

The subset method will also store the corresponding subset of the covariance matrix

[13]:
rm_res_cov_sub.covariance
gd157_endf.resonance_covariance.ranges[0].covariance.shape

[13]:
(180, 180)

Checking the size of the new covariance matrix to be sure it was sampled properly:

[14]:
old_n_parameters = gd157_endf.resonance_covariance.ranges[0].parameters.shape[0]
old_shape = gd157_endf.resonance_covariance.ranges[0].covariance.shape
new_n_parameters = rm_res_cov_sub.file2res.parameters.shape[0]
new_shape = rm_res_cov_sub.covariance.shape
print('Number of parameters\nOriginal: '+str(old_n_parameters)+'\nSubet: '+str(new_n_parameters)+'\nCovariance Size\nOriginal: '+str(old_shape)+'\nSubset: '+str(new_shape))

Number of parameters
Original: 60
Subet: 36
Covariance Size
Original: (180, 180)
Subset: (108, 108)

And finally, we can sample from the subset as well

[15]:
samples_sub = rm_res_cov_sub.sample(n_samples)
samples_sub[0].parameters[:5]
[15]:
energy L J neutronWidth captureWidth fissionWidthA fissionWidthB
0 0.030488 0 2.0 0.000473 0.108946 0.0 0.0
1 2.825944 0 2.0 0.000328 0.098328 0.0 0.0
2 16.773886 0 2.0 0.012984 0.076779 0.0 0.0
3 20.565737 0 2.0 0.011628 0.088958 0.0 0.0
4 21.646469 0 2.0 0.000389 0.127833 0.0 0.0

Pincell Depletion

This notebook is intended to introduce the reader to the depletion interface contained in OpenMC. It is recommended that you are moderately familiar with building models using the OpenMC Python API. The earlier examples are excellent starting points, as this notebook will not focus heavily on model building.

If you have a real power reactor, the fuel composition is constantly changing as fission events produce energy, remove some fissile isotopes, and produce fission products. Other reactions, like \((n, \alpha)\) and \((n, \gamma)\) will alter the composition as well. Furthermore, some nuclides undergo spontaneous decay with widely ranging frequencies. Depletion is the process of modeling this behavior.

In this notebook, we will model a simple fuel pin in an infinite lattice using the Python API. We will then build and examine some of the necessary components for performing depletion analysis. Then, we will use the depletion interface in OpenMC to simulate the fuel pin producing power over several months. Lastly, we will wrap up with some helpful tips to improve the fidelity of depletion simulations.

[1]:
%matplotlib inline
import math
import openmc
Build the Geometry

Much of this section is borrowed from the “Modeling a Pin-Cell” example. If you find yourself not understanding some aspects of this section, feel free to refer to that example, as some details may be glossed over for brevity.

First, we will create our fuel, cladding, and water materials to represent a typical PWR.

[2]:
fuel = openmc.Material(name="uo2")
[3]:
fuel.add_element("U", 1, percent_type="ao", enrichment=4.25)
fuel.add_element("O", 2)
fuel.set_density("g/cc", 10.4)
[4]:
clad = openmc.Material(name="clad")
[5]:
clad.add_element("Zr", 1)
clad.set_density("g/cc", 6)
[6]:
water = openmc.Material(name="water")
[7]:
water.add_element("O", 1)
water.add_element("H", 2)
water.set_density("g/cc", 1.0)
[8]:
water.add_s_alpha_beta("c_H_in_H2O")
[9]:
materials = openmc.Materials([fuel, clad, water])

Here, we are going to use the openmc.model.pin function to build our pin cell. The pin function anticipates concentric cylinders and materials to fill the inner regions. One additional material is needed than the number of cylinders to cover the domain outside the final ring.

To do this, we define two radii for the outer radius of our fuel pin, and the outer radius of the cladding.

[10]:
radii = [0.42, 0.45]

Using these radii, we define concentric ZCylinder objects. So long as the cylinders are concentric and increasing in radius, any orientation can be used. We also take advantage of the fact that the openmc.Materials object is a subclass of the list object to assign materials to the regions defined by the surfaces.

[11]:
pin_surfaces = [openmc.ZCylinder(r=r) for r in radii]
[12]:
pin_univ = openmc.model.pin(pin_surfaces, materials)

The first material, in our case fuel, is placed inside the first cylinder in the inner-most region. The second material, clad, fills the space between our cylinders, while water is placed outside the last ring. The pin function returns an openmc.Universe object, and has some additional features we will mention later.

[13]:
pin_univ.plot()
[13]:
<matplotlib.image.AxesImage at 0x7f9aea1a50d0>
_images/examples_pincell_depletion_17_1.png
[14]:
bound_box = openmc.rectangular_prism(0.62, 0.62, boundary_type="reflective")
[15]:
root_cell = openmc.Cell(fill=pin_univ, region=bound_box)
[16]:
root_univ = openmc.Universe(cells=[root_cell])
[17]:
geometry = openmc.Geometry(root_univ)

Lastly we construct our settings. For the sake of time, a relatively low number of particles will be used.

[18]:
settings = openmc.Settings()
[19]:
settings.particles = 100
settings.inactive = 10
settings.batches = 50

The depletion interface relies on OpenMC to perform the transport simulation and obtain reaction rates and other important information. We then have to create the xml input files that openmc expects, specifically geometry.xml, settings.xml, and materials.xml.

[20]:
geometry.export_to_xml()
[21]:
settings.export_to_xml()

Before we write the material file, we must add one bit of information: the volume of our fuel. In order to translate the reaction rates obtained by openmc to meaningful units for depletion, we have to normalize them to a correct power. This requires us to know, or be able to calculate, how much fuel is in our problem. Correctly setting the volumes is a critical step, and can lead to incorrect answers, as the fuel is over- or under-depleted due to poor normalization.

For our problem, we can assign the “volume” to be the cross-sectional area of our fuel. This is identical to modeling our fuel pin inside a box with height of 1 cm.

[23]:
fuel.volume = math.pi * radii[0] ** 2
[24]:
materials.export_to_xml()
Setting up for depletion

The OpenMC depletion interface can be accessed from the openmc.deplete module, and has a variety of classes that will help us.

[25]:
import openmc.deplete

In order to run the depletion calculation we need the following information:

  1. Nuclide decay, fission yield, and reaction data
  2. Operational power or power density
  3. Desired depletion schedule
  4. Desired time integration scheme

The first item is necessary to determine the paths by which nuclides transmute over the depletion simulation. This includes spontaneous decay, fission product yield distributions, and nuclides produced through neutron-reactions. For example, * Te129 decays to I129 with a half life of ~70 minutes * A fission event for U-235 produces fission products like Xe135 according to a distribution * For thermal problems, Am241 will produce metastable Am242 about 8% of the time during an \((n,\gamma)\) reaction. The other 92% of capture reactions will produce ground state Am242

These data are often distributed with other nuclear data, like incident neutron cross sections with ENDF/B-VII. OpenMC uses the `openmc.deplete.Chain <https://docs.openmc.org/en/latest/pythonapi/generated/openmc.deplete.Chain.html#openmc.deplete.Chain>`__ to collect represent the various decay and transmutation pathways in a single object. While a complete Chain can be created using nuclear data files, users may prefer to download pre-generated XML-representations instead. Such files can be found at https://openmc.org/depletion-chains/ and include full and compressed chains, with capture branching ratios derived using PWR- or SFR-spectra.

For this problem, we will be using a much smaller depletion chain that contains very few nuclides. In a realistic problem, over 1000 isotopes may be included in the depletion chain.

[26]:
chain = openmc.deplete.Chain.from_xml("./chain_simple.xml")
[27]:
chain.nuclide_dict
[27]:
OrderedDict([('I135', 0),
             ('Xe135', 1),
             ('Xe136', 2),
             ('Cs135', 3),
             ('Gd157', 4),
             ('Gd156', 5),
             ('U234', 6),
             ('U235', 7),
             ('U238', 8)])

The primary entry point for depletion is the openmc.deplete.Operator. It relies on the openmc.deplete.Chain and helper classes to run openmc, retrieve and normalize reaction rates, and other perform other tasks. For a thorough description, please see the full API documentation.

We will create our Operator using the geometry and settings from above, and our simple chain file. The materials are read in automatically using the materials.xml file.

[28]:
operator = openmc.deplete.Operator(geometry, settings, "./chain_simple.xml")

We will then simulate our fuel pin operating at linear power of 174 W/cm, or 174 W given a unit height for our problem.

[29]:
power = 174

For this problem, we will take depletion step sizes of 30 days, and instruct OpenMC to re-run a transport simulation every 30 days until we have modeled the problem over a six month cycle. The depletion interface expects the time to be given in seconds, so we will have to convert. Note that these values are not cumulative.

[30]:
time_steps = [30 * 24 * 60 * 60] * 6

And lastly, we will use the basic predictor, or forward Euler, time integration scheme. Other, more advanced methods are provided to the user through openmc.deplete

[31]:
integrator = openmc.deplete.PredictorIntegrator(operator, time_steps, power)

To perform the simulation, we use the integrate method, and let openmc take care of the rest.

[32]:
integrator.integrate()
Processing the outputs

The depletion simulation produces a few output files. First, the statepoint files from each individual transport simulation are written to openmc_simulation_n<N>.h5, where <N> indicates the current depletion step. Any tallies that we defined in tallies.xml will be included in these files across our simulations. We have 7 such files, one for each our of 6 depletion steps and the initial state.

[33]:
!ls *.h5
c5g7.h5                  openmc_simulation_n2.h5  openmc_simulation_n6.h5
depletion_results.h5     openmc_simulation_n3.h5  statepoint.50.h5
openmc_simulation_n0.h5  openmc_simulation_n4.h5  summary.h5
openmc_simulation_n1.h5  openmc_simulation_n5.h5

The depletion_results.h5 file contains information that is aggregated over all time steps through depletion. This includes the multiplication factor, as well as concentrations. We can process this file using the openmc.deplete.ResultsList object

[34]:
results = openmc.deplete.ResultsList.from_hdf5("./depletion_results.h5")
[35]:
time, k = results.get_eigenvalue()
[36]:
time /= (24 * 60 * 60)  # convert back to days from seconds
[37]:
k
[37]:
array([[0.76882937, 0.00982155],
       [0.75724033, 0.00827689],
       [0.75532242, 0.01031746],
       [0.74796855, 0.00919769],
       [0.74066561, 0.01157708],
       [0.73184492, 0.00971504],
       [0.7207293 , 0.00703074]])

The first column of k is the value of k-combined at each point in our simulation, while the second column contains the associated uncertainty. We can plot this using matplotlib

[38]:
from matplotlib import pyplot
[39]:
pyplot.errorbar(time, k[:, 0], yerr=k[:, 1])
pyplot.xlabel("Time [d]")
pyplot.ylabel("$k_{eff}\pm \sigma$");
_images/examples_pincell_depletion_56_0.png

Due to the low number of particles selected, we have not only a very high uncertainty, but likely a horrendously poor fission source. This pin cell should have \(k>1\), but we can still see the decline over time due to fuel consumption.

We can then examine concentrations of atoms in each of our materials. This requires knowing the material ID, which can be obtained from the materials.xml file.

[40]:
_time, u5 = results.get_atoms("1", "U235")
_time, xe135 = results.get_atoms("1", "Xe135")
[41]:
pyplot.plot(time, u5, label="U235")
pyplot.xlabel("Time [d]")
pyplot.ylabel("Number of atoms - U235");
_images/examples_pincell_depletion_60_0.png
[42]:
pyplot.plot(time, xe135, label="Xe135")
pyplot.xlabel("Time [d]")
pyplot.ylabel("Number of atoms - Xe135");
_images/examples_pincell_depletion_61_0.png

We can also examine reaction rates over time using the ResultsList

[43]:
_time, u5_fission = results.get_reaction_rate("1", "U235", "fission")
[44]:
pyplot.plot(time, u5_fission)
pyplot.xlabel("Time [d]")
pyplot.ylabel("Fission reactions / s");
_images/examples_pincell_depletion_64_0.png
Helpful tips

Depletion is a tricky task to get correct. Use too short of time steps and you may never get your results due to running many transport simulations. Use long of time steps and you may get incorrect answers. Consider the xenon plot from above. Xenon-135 is a fission product with a thermal absorption cross section on the order of millions of barns, but has a half life of ~9 hours. Taking smaller time steps at the beginning of your simulation to build up some equilibrium in your fission products is highly recommended.

When possible, differentiate materials that reappear in multiple places. If we had built an entire core with the single fuel material, every pin would be depleted using the same averaged spectrum and reaction rates which is incorrect. The Operator can differentiate these materials using the diff_burnable_mats argument, but note that the volumes will be copied from the original material.

Using higher-order integrators, like the CECMIntegrator, EPCRK4Integrator with a fourth order Runge-Kutta, or the LEQIIntegrator, can improve the accuracy of a simulation, or at least allow you to take longer depletion steps between transport simulations with similar accuracy.

Fuel pins with integrated burnable absorbers, like gadolinia, experience strong flux gradients until the absorbers are mostly burned away. This means that the spectrum and magnitude of the flux at the edge of the fuel pin can be vastly different than that in the interior. The helper pin function can be used to subdivide regions into equal volume segments, as follows.

[45]:
div_surfs_1 = [openmc.ZCylinder(r=1)]
div_1 = openmc.model.pin(div_surfs_1, [fuel, water], subdivisions={0: 10})
[46]:
div_1.plot(width=(2.0, 2.0))
[46]:
<matplotlib.image.AxesImage at 0x7f9ae8572a90>
_images/examples_pincell_depletion_67_1.png

The innermost region has been divided into 10 equal volume regions. We can pass additional arguments to divide multiple regions, except for the region outside the last cylinder.

Register depletion chain

The depletion chain we created can be registered into the OpenMC cross_sections.xml file, so we don’t have to always pass the chain_file argument to the Operator. To do this, we create a DataLibrary using openmc.data. Without any arguments, the from_xml method will look for the file located at OPENMC_CROSS_SECTIONS. For this example, we will just create a bare library.

[47]:
data_lib = openmc.data.DataLibrary()
[48]:
data_lib.register_file("./chain_simple.xml")
[49]:
data_lib.export_to_xml()
[50]:
!cat cross_sections.xml
<?xml version='1.0' encoding='utf-8'?>
<cross_sections>
  <depletion_chain path="chain_simple.xml" type="depletion_chain" />
</cross_sections>

This allows us to make an Operator simply with the geometry and settings arguments, provided we exported our library to OPENMC_CROSS_SECTIONS. For a problem where we built and registered a Chain using all the available nuclear data, we might see something like the following.

[51]:
new_op = openmc.deplete.Operator(geometry, settings)
[52]:
len(new_op.chain.nuclide_dict)
[52]:
3820
[53]:
[nuc.name for nuc in new_op.chain.nuclides[:10]]
[53]:
['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'H7', 'He3', 'He4', 'He5']
[54]:
[nuc.name for nuc in new_op.chain.nuclides[-10:]]
[54]:
['Ds268',
 'Ds269',
 'Ds270',
 'Ds270_m1',
 'Ds271',
 'Ds271_m1',
 'Ds272',
 'Ds273',
 'Ds279_m1',
 'Rg272']
Choice of depletion step size

A general rule of thumb is to use depletion step sizes around 2 MWd/kgHM, where kgHM is really the initial heavy metal mass in kg. If your problem includes integral burnable absorbers, these typically require shorter time steps at or below 1 MWd/kgHM. These are typically valid for the predictor scheme, as the point of recent schemes is to extend this step size. A good convergence study, where the step size is decreased until some convergence metric is satisfied, is a beneficial exercise.

We can use the Operator to determine our maximum step size using this recommendation. The heavy_metal attribute returns the mass of initial heavy metal in g, which, using our power, can be used to compute this step size.

\[\frac{2\,MWd}{kgHM} = \frac{P\times\Delta}{hm_{op}} \]
[55]:
operator.heavy_metal
[55]:
5.080339195584719
[56]:
max_step = 2 * operator.heavy_metal / power * 1E3
[57]:
print("\"Maximum\" depletion step: {:5.3} [d]".format(max_step))
"Maximum" depletion step:  58.4 [d]

Alternatively, if we were provided the power density of our problem, we can provide this directly with openmc.deplete.PredictorIntegrator(operator, time_steps, power_density=pdens). The values of power and power_density do not have to be scalars. For problems with variable power, we can provide an iterable with the same number of elements as time_steps.

Geometry

Modeling Hexagonal Lattices

In this example, we will create a hexagonal lattice and show how the orientation can be changed via the cell rotation property. Let’s first just set up some materials and universes that we will use to fill the lattice.

[1]:
%matplotlib inline
import openmc
[2]:
fuel = openmc.Material(name='fuel')
fuel.add_nuclide('U235', 1.0)
fuel.set_density('g/cm3', 10.0)

fuel2 = openmc.Material(name='fuel2')
fuel2.add_nuclide('U238', 1.0)
fuel2.set_density('g/cm3', 10.0)

water = openmc.Material(name='water')
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
water.set_density('g/cm3', 1.0)

materials = openmc.Materials((fuel, fuel2, water))
materials.export_to_xml()

With our three materials, we will set up two universes that represent pin-cells: one with a small pin and one with a big pin. Since we will be using these universes in a lattice, it’s always a good idea to have an “outer” universe as well that is applied outside the defined lattice.

[3]:
r_pin = openmc.ZCylinder(r=0.25)
fuel_cell = openmc.Cell(fill=fuel, region=-r_pin)
water_cell = openmc.Cell(fill=water, region=+r_pin)
pin_universe = openmc.Universe(cells=(fuel_cell, water_cell))

r_big_pin = openmc.ZCylinder(r=0.5)
fuel2_cell = openmc.Cell(fill=fuel2, region=-r_big_pin)
water2_cell = openmc.Cell(fill=water, region=+r_big_pin)
big_pin_universe = openmc.Universe(cells=(fuel2_cell, water2_cell))

all_water_cell = openmc.Cell(fill=water)
outer_universe = openmc.Universe(cells=(all_water_cell,))

Now let’s create a hexagonal lattice using the HexLattice class:

[4]:
lattice = openmc.HexLattice()

We need to set the center of the lattice, the pitch, an outer universe (which is applied to all lattice elements outside of those that are defined), and a list of universes. Let’s start with the easy ones first. Note that for a 2D lattice, we only need to specify a single number for the pitch.

[5]:
lattice.center = (0., 0.)
lattice.pitch = (1.25,)
lattice.outer = outer_universe

Now we need to set the universes property on our lattice. It needs to be set to a list of lists of Universes, where each list of Universes corresponds to a ring of the lattice. The rings are ordered from outermost to innermost, and within each ring the indexing starts at the “top”. To help visualize the proper indices, we can use the show_indices() helper method.

[6]:
print(lattice.show_indices(num_rings=4))
                  (0, 0)
            (0,17)      (0, 1)
      (0,16)      (1, 0)      (0, 2)
(0,15)      (1,11)      (1, 1)      (0, 3)
      (1,10)      (2, 0)      (1, 2)
(0,14)      (2, 5)      (2, 1)      (0, 4)
      (1, 9)      (3, 0)      (1, 3)
(0,13)      (2, 4)      (2, 2)      (0, 5)
      (1, 8)      (2, 3)      (1, 4)
(0,12)      (1, 7)      (1, 5)      (0, 6)
      (0,11)      (1, 6)      (0, 7)
            (0,10)      (0, 8)
                  (0, 9)

Let’s set up a lattice where the first element in each ring is the big pin universe and all other elements are regular pin universes.

From the diagram above, we see that the outer ring has 18 elements, the first ring has 12, and the second ring has 6 elements. The innermost ring of any hexagonal lattice will have only a single element.

We build these rings through ‘list concatenation’ as follows:

[7]:
outer_ring = [big_pin_universe] + [pin_universe]*17 # Adds up to 18

ring_1 = [big_pin_universe] + [pin_universe]*11 # Adds up to 12

ring_2 = [big_pin_universe] + [pin_universe]*5 # Adds up to 6

inner_ring = [big_pin_universe]

We can now assign the rings (and the universes they contain) to our lattice.

[8]:
lattice.universes = [outer_ring,
                     ring_1,
                     ring_2,
                     inner_ring]
print(lattice)
HexLattice
        ID             =        4
        Name           =
        Orientation    =        y
        # Rings        =        4
        # Axial        =        None
        Center         =        (0.0, 0.0)
        Pitch          =        (1.25,)
        Outer          =        3
        Universes
   2
  1 1
 1 2 1
1 1 1 1
 1 2 1
1 1 1 1
 1 2 1
1 1 1 1
 1 1 1
1 1 1 1
 1 1 1
  1 1
   1

Now let’s put our lattice inside a circular cell that will serve as the top-level cell for our geometry.

[9]:
outer_surface = openmc.ZCylinder(r=5.0, boundary_type='vacuum')
main_cell = openmc.Cell(fill=lattice, region=-outer_surface)
geometry = openmc.Geometry([main_cell])
geometry.export_to_xml()

Now let’s create a plot to see what our geometry looks like.

[10]:
plot = openmc.Plot.from_geometry(geometry)
plot.color_by = 'material'
plot.colors = colors = {
    water: 'blue',
    fuel: 'olive',
    fuel2: 'yellow'
}
plot.to_ipython_image()
[10]:
_images/examples_hexagonal-lattice_18_0.png

At this point, if we wanted to simulate the model, we would need to create an instance of openmc.Settings, export it to XML, and run.

Lattice orientation

Now let’s say we want our hexagonal lattice orientated such that two sides of the lattice are parallel to the x-axis. This can be achieved by two means: either we can rotate the cell that contains the lattice, or we can can change the HexLattice.orientation attribute. By default, the orientation is set to “y”, indicating that two sides of the lattice are parallel to the y-axis, but we can also change it to “x” to make them parallel to the x-axis.

[11]:
# Change the orientation of the lattice and re-export the geometry
lattice.orientation = 'x'
geometry.export_to_xml()

# Run OpenMC in plotting mode
plot.to_ipython_image()
[11]:
_images/examples_hexagonal-lattice_21_0.png

When we change the orientation to ‘x’, you can see that the first universe in each ring starts to the right along the x-axis. As before, the universes are defined in a clockwise fashion around each ring. To see the proper indices for a hexagonal lattice in this orientation, we can again call show_indices but pass an extra orientation argument:

[12]:
print(lattice.show_indices(4, orientation='x'))
                  (0,12)      (0,13)      (0,14)      (0,15)

            (0,11)      (1, 8)      (1, 9)      (1,10)      (0,16)

      (0,10)      (1, 7)      (2, 4)      (2, 5)      (1,11)      (0,17)

(0, 9)      (1, 6)      (2, 3)      (3, 0)      (2, 0)      (1, 0)      (0, 0)

      (0, 8)      (1, 5)      (2, 2)      (2, 1)      (1, 1)      (0, 1)

            (0, 7)      (1, 4)      (1, 3)      (1, 2)      (0, 2)

                  (0, 6)      (0, 5)      (0, 4)      (0, 3)
Hexagonal prisms

OpenMC also contains a convenience function that can create a hexagonal prism representing the interior region of six surfaces defining a hexagon. This can be useful as a bounding surface of a hexagonal lattice. For example, if we wanted the outer boundary of our geometry to be hexagonal, we could change the region of the main cell:

[13]:
main_cell.region = openmc.model.hexagonal_prism(
    edge_length=4*lattice.pitch[0],
    orientation='x',
    boundary_type='vacuum'
)
geometry.export_to_xml()

# Run OpenMC in plotting mode
plot.color_by = 'cell'
plot.to_ipython_image()
[13]:
_images/examples_hexagonal-lattice_25_0.png

Modeling TRISO Particles

OpenMC includes a few convenience functions for generationing TRISO particle locations and placing them in a lattice. To be clear, this capability is not a stochastic geometry capability like that included in MCNP. It’s also important to note that OpenMC does not use delta tracking, which would normally speed up calculations in geometries with tons of surfaces and cells. However, the computational burden can be eased by placing TRISO particles in a lattice.

[1]:
%matplotlib inline
from math import pi
import numpy as np
import matplotlib.pyplot as plt
import openmc
import openmc.model

Let’s first start by creating materials that will be used in our TRISO particles and the background material.

[2]:
fuel = openmc.Material(name='Fuel')
fuel.set_density('g/cm3', 10.5)
fuel.add_nuclide('U235', 4.6716e-02)
fuel.add_nuclide('U238', 2.8697e-01)
fuel.add_nuclide('O16',  5.0000e-01)
fuel.add_element('C', 1.6667e-01)

buff = openmc.Material(name='Buffer')
buff.set_density('g/cm3', 1.0)
buff.add_element('C', 1.0)
buff.add_s_alpha_beta('c_Graphite')

PyC1 = openmc.Material(name='PyC1')
PyC1.set_density('g/cm3', 1.9)
PyC1.add_element('C', 1.0)
PyC1.add_s_alpha_beta('c_Graphite')

PyC2 = openmc.Material(name='PyC2')
PyC2.set_density('g/cm3', 1.87)
PyC2.add_element('C', 1.0)
PyC2.add_s_alpha_beta('c_Graphite')

SiC = openmc.Material(name='SiC')
SiC.set_density('g/cm3', 3.2)
SiC.add_element('C', 0.5)
SiC.add_element('Si', 0.5)

graphite = openmc.Material()
graphite.set_density('g/cm3', 1.1995)
graphite.add_element('C', 1.0)
graphite.add_s_alpha_beta('c_Graphite')

To actually create individual TRISO particles, we first need to create a universe that will be used within each particle. The reason we use the same universe for each TRISO particle is to reduce the total number of cells/surfaces needed which can substantially improve performance over using unique cells/surfaces in each.

[3]:
# Create TRISO universe
spheres = [openmc.Sphere(r=1e-4*r)
           for r in [215., 315., 350., 385.]]
cells = [openmc.Cell(fill=fuel, region=-spheres[0]),
         openmc.Cell(fill=buff, region=+spheres[0] & -spheres[1]),
         openmc.Cell(fill=PyC1, region=+spheres[1] & -spheres[2]),
         openmc.Cell(fill=SiC, region=+spheres[2] & -spheres[3]),
         openmc.Cell(fill=PyC2, region=+spheres[3])]
triso_univ = openmc.Universe(cells=cells)

Next, we need a region to pack the TRISO particles in. We will use a 1 cm x 1 cm x 1 cm box centered at the origin.

[4]:
min_x = openmc.XPlane(x0=-0.5, boundary_type='reflective')
max_x = openmc.XPlane(x0=0.5, boundary_type='reflective')
min_y = openmc.YPlane(y0=-0.5, boundary_type='reflective')
max_y = openmc.YPlane(y0=0.5, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-0.5, boundary_type='reflective')
max_z = openmc.ZPlane(z0=0.5, boundary_type='reflective')
region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

Now we need to randomly select locations for the TRISO particles. In this example, we will select locations at random within the box with a packing fraction of 30%. Note that pack_spheres can handle up to the theoretical maximum of 60% (it will just be slow).

[5]:
outer_radius = 425.*1e-4
centers = openmc.model.pack_spheres(radius=outer_radius, region=region, pf=0.3)

Now that we have the locations of the TRISO particles determined and a universe that can be used for each particle, we can create the TRISO particles.

[6]:
trisos = [openmc.model.TRISO(outer_radius, triso_univ, center) for center in centers]

Each TRISO object actually is a Cell, in fact; we can look at the properties of the TRISO just as we would a cell:

[7]:
print(trisos[0])
Cell
        ID             =        6
        Name           =
        Fill           =        1
        Region         =        -11
        Rotation       =        None
        Translation    =        [-0.33455672  0.31790187  0.24135378]

Let’s confirm that all our TRISO particles are within the box.

[8]:
centers = np.vstack([triso.center for triso in trisos])
print(centers.min(axis=0))
print(centers.max(axis=0))
[-0.45718713 -0.45730405 -0.45725048]
[0.45705454 0.45743843 0.45741142]

We can also look at what the actual packing fraction turned out to be:

[9]:
len(trisos)*4/3*pi*outer_radius**3
[9]:
0.2996893513959326

Now that we have our TRISO particles created, we need to place them in a lattice to provide optimal tracking performance in OpenMC. We can use the box we created above to place the lattice in. Actually creating a lattice containing TRISO particles can be done with the model.create_triso_lattice() function. This function requires that we give it a list of TRISO particles, the lower-left coordinates of the lattice, the pitch of each lattice cell, the overall shape of the lattice (number of cells in each direction), and a background material.

[10]:
box = openmc.Cell(region=region)
lower_left, upper_right = box.region.bounding_box
shape = (3, 3, 3)
pitch = (upper_right - lower_left)/shape
lattice = openmc.model.create_triso_lattice(
    trisos, lower_left, pitch, shape, graphite)

Now we can set the fill of our box cell to be the lattice:

[11]:
box.fill = lattice

Finally, let’s take a look at our geometry by putting the box in a universe and plotting it. We’re going to use the Fortran-side plotter since it’s much faster.

[12]:
universe = openmc.Universe(cells=[box])

geometry = openmc.Geometry(universe)
geometry.export_to_xml()

materials = list(geometry.get_all_materials().values())
openmc.Materials(materials).export_to_xml()

settings = openmc.Settings()
settings.run_mode = 'plot'
settings.export_to_xml()

plot = openmc.Plot.from_geometry(geometry)
plot.to_ipython_image()
[12]:
_images/examples_triso_23_0.png

If we plot the universe by material rather than by cell, we can see that the entire background is just graphite.

[13]:
plot.color_by = 'material'
plot.colors = {graphite: 'gray'}
plot.to_ipython_image()
[13]:
_images/examples_triso_25_0.png

Modeling a CANDU Bundle

In this example, we will create a typical CANDU bundle with rings of fuel pins. At present, OpenMC does not have a specialized lattice for this type of fuel arrangement, so we must resort to manual creation of the array of fuel pins.

[1]:
%matplotlib inline
from math import pi, sin, cos
import numpy as np
import openmc

Let’s begin by creating the materials that will be used in our model.

[2]:
fuel = openmc.Material(name='fuel')
fuel.add_element('U', 1.0)
fuel.add_element('O', 2.0)
fuel.set_density('g/cm3', 10.0)

clad = openmc.Material(name='zircaloy')
clad.add_element('Zr', 1.0)
clad.set_density('g/cm3', 6.0)

heavy_water = openmc.Material(name='heavy water')
heavy_water.add_nuclide('H2', 2.0)
heavy_water.add_nuclide('O16', 1.0)
heavy_water.add_s_alpha_beta('c_D_in_D2O')
heavy_water.set_density('g/cm3', 1.1)

With our materials created, we’ll now define key dimensions in our model. These dimensions are taken from the example in section 11.1.3 of the Serpent manual.

[3]:
# Outer radius of fuel and clad
r_fuel = 0.6122
r_clad = 0.6540

# Pressure tube and calendria radii
pressure_tube_ir = 5.16890
pressure_tube_or = 5.60320
calendria_ir = 6.44780
calendria_or = 6.58750

# Radius to center of each ring of fuel pins
ring_radii = np.array([0.0, 1.4885, 2.8755, 4.3305])

To begin creating the bundle, we’ll first create annular regions completely filled with heavy water and add in the fuel pins later. The radii that we’ve specified above correspond to the center of each ring. We actually need to create cylindrical surfaces at radii that are half-way between the centers.

[4]:
# These are the surfaces that will divide each of the rings
radial_surf = [openmc.ZCylinder(r=r) for r in
               (ring_radii[:-1] + ring_radii[1:])/2]

water_cells = []
for i in range(ring_radii.size):
    # Create annular region
    if i == 0:
        water_region = -radial_surf[i]
    elif i == ring_radii.size - 1:
        water_region = +radial_surf[i-1]
    else:
        water_region = +radial_surf[i-1] & -radial_surf[i]

    water_cells.append(openmc.Cell(fill=heavy_water, region=water_region))

Let’s see what our geometry looks like so far. In order to plot the geometry, we create a universe that contains the annular water cells and then use the Universe.plot() method. While we’re at it, we’ll set some keyword arguments that can be reused for later plots.

[5]:
plot_args = {'width': (2*calendria_or, 2*calendria_or)}
bundle_universe = openmc.Universe(cells=water_cells)
bundle_universe.plot(**plot_args)
[5]:
<matplotlib.image.AxesImage at 0x154b1fe8f438>
_images/examples_candu_9_1.png

Now we need to create a universe that contains a fuel pin. Note that we don’t actually need to put water outside of the cladding in this universe because it will be truncated by a higher universe.

[6]:
surf_fuel = openmc.ZCylinder(r=r_fuel)

fuel_cell = openmc.Cell(fill=fuel, region=-surf_fuel)
clad_cell = openmc.Cell(fill=clad, region=+surf_fuel)

pin_universe = openmc.Universe(cells=(fuel_cell, clad_cell))
[7]:
pin_universe.plot(**plot_args)
[7]:
<matplotlib.image.AxesImage at 0x154b1de334a8>
_images/examples_candu_12_1.png

The code below works through each ring to create a cell containing the fuel pin universe. As each fuel pin is created, we modify the region of the water cell to include everything outside the fuel pin.

[8]:
num_pins = [1, 6, 12, 18]
angles = [0, 0, 15, 0]

for i, (r, n, a) in enumerate(zip(ring_radii, num_pins, angles)):
    for j in range(n):
        # Determine location of center of pin
        theta = (a + j/n*360.) * pi/180.
        x = r*cos(theta)
        y = r*sin(theta)

        pin_boundary = openmc.ZCylinder(x0=x, y0=y, r=r_clad)
        water_cells[i].region &= +pin_boundary

        # Create each fuel pin -- note that we explicitly assign an ID so
        # that we can identify the pin later when looking at tallies
        pin = openmc.Cell(fill=pin_universe, region=-pin_boundary)
        pin.translation = (x, y, 0)
        pin.id = (i + 1)*100 + j
        bundle_universe.add_cell(pin)
[9]:
bundle_universe.plot(**plot_args)
[9]:
<matplotlib.image.AxesImage at 0x154b1ddaadd8>
_images/examples_candu_15_1.png

Looking pretty good! Finally, we create cells for the pressure tube and calendria and then put our bundle in the middle of the pressure tube.

[10]:
pt_inner = openmc.ZCylinder(r=pressure_tube_ir)
pt_outer = openmc.ZCylinder(r=pressure_tube_or)
calendria_inner = openmc.ZCylinder(r=calendria_ir)
calendria_outer = openmc.ZCylinder(r=calendria_or, boundary_type='vacuum')

bundle = openmc.Cell(fill=bundle_universe, region=-pt_inner)
pressure_tube = openmc.Cell(fill=clad, region=+pt_inner & -pt_outer)
v1 = openmc.Cell(region=+pt_outer & -calendria_inner)
calendria = openmc.Cell(fill=clad, region=+calendria_inner & -calendria_outer)

root_universe = openmc.Universe(cells=[bundle, pressure_tube, v1, calendria])

Let’s look at the final product. We’ll export our geometry and materials and then use plot_inline() to get a nice-looking plot.

[11]:
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()

materials = openmc.Materials(geometry.get_all_materials().values())
materials.export_to_xml()
[12]:
plot = openmc.Plot.from_geometry(geometry)
plot.color_by = 'material'
plot.colors = {
    fuel: 'black',
    clad: 'silver',
    heavy_water: 'blue'
}
plot.to_ipython_image()
[12]:
_images/examples_candu_20_0.png
Interpreting Results

One of the difficulties of a geometry like this is identifying tally results when there was no lattice involved. To address this, we specifically gave an ID to each fuel pin of the form 100*ring + azimuthal position. Consequently, we can use a distribcell tally and then look at our DataFrame which will show these cell IDs.

[13]:
settings = openmc.Settings()
settings.particles = 1000
settings.batches = 20
settings.inactive = 10
settings.source = openmc.Source(space=openmc.stats.Point())
settings.export_to_xml()
[14]:
fuel_tally = openmc.Tally()
fuel_tally.filters = [openmc.DistribcellFilter(fuel_cell)]
fuel_tally.scores = ['flux']

tallies = openmc.Tallies([fuel_tally])
tallies.export_to_xml()
[15]:
openmc.run(output=False)

The return code of 0 indicates that OpenMC ran successfully. Now let’s load the statepoint into a openmc.StatePoint object and use the Tally.get_pandas_dataframe(...) method to see our results.

[16]:
sp = openmc.StatePoint('statepoint.{}.h5'.format(settings.batches))
[17]:
output_tally = sp.get_tally()
output_tally.get_pandas_dataframe()
[17]:
level 1 level 2 level 3 distribcell nuclide score mean std. dev.
univ cell univ cell univ cell
id id id id id id
0 3 44 1 100 2 5 0 total flux 0.207805 0.007037
1 3 44 1 200 2 5 1 total flux 0.197197 0.005272
2 3 44 1 201 2 5 2 total flux 0.190310 0.007816
3 3 44 1 202 2 5 3 total flux 0.194736 0.006469
4 3 44 1 203 2 5 4 total flux 0.191097 0.006431
5 3 44 1 204 2 5 5 total flux 0.189910 0.004891
6 3 44 1 205 2 5 6 total flux 0.182203 0.003851
7 3 44 1 300 2 5 7 total flux 0.165922 0.005815
8 3 44 1 301 2 5 8 total flux 0.168933 0.008300
9 3 44 1 302 2 5 9 total flux 0.159587 0.003085
10 3 44 1 303 2 5 10 total flux 0.159158 0.005910
11 3 44 1 304 2 5 11 total flux 0.148537 0.005308
12 3 44 1 305 2 5 12 total flux 0.150945 0.006654
13 3 44 1 306 2 5 13 total flux 0.154237 0.003665
14 3 44 1 307 2 5 14 total flux 0.165888 0.004733
15 3 44 1 308 2 5 15 total flux 0.156777 0.006540
16 3 44 1 309 2 5 16 total flux 0.165277 0.005935
17 3 44 1 310 2 5 17 total flux 0.156528 0.005732
18 3 44 1 311 2 5 18 total flux 0.159610 0.004584
19 3 44 1 400 2 5 19 total flux 0.096597 0.004466
20 3 44 1 401 2 5 20 total flux 0.118214 0.005451
21 3 44 1 402 2 5 21 total flux 0.106167 0.004722
22 3 44 1 403 2 5 22 total flux 0.110814 0.004208
23 3 44 1 404 2 5 23 total flux 0.112319 0.005079
24 3 44 1 405 2 5 24 total flux 0.110232 0.004153
25 3 44 1 406 2 5 25 total flux 0.099967 0.005085
26 3 44 1 407 2 5 26 total flux 0.095444 0.003615
27 3 44 1 408 2 5 27 total flux 0.092620 0.003997
28 3 44 1 409 2 5 28 total flux 0.095517 0.004022
29 3 44 1 410 2 5 29 total flux 0.113737 0.009530
30 3 44 1 411 2 5 30 total flux 0.108368 0.007241
31 3 44 1 412 2 5 31 total flux 0.106990 0.005716
32 3 44 1 413 2 5 32 total flux 0.112050 0.005002
33 3 44 1 414 2 5 33 total flux 0.115054 0.006239
34 3 44 1 415 2 5 34 total flux 0.114394 0.004919
35 3 44 1 416 2 5 35 total flux 0.114352 0.005322
36 3 44 1 417 2 5 36 total flux 0.110890 0.005051

We can see that in the ‘level 2’ column, the ‘cell id’ tells us how each row corresponds to a ring and azimuthal position.

Using CAD-Based Geometries

In this notebook we’ll be exploring how to use CAD-based geometries in OpenMC via the DagMC toolkit. The models we’ll be using in this notebook have already been created using Trelis and faceted into a surface mesh represented as .h5m files in the Mesh Oriented DatABase format. We’ll be retrieving these files using the function below.

[1]:
import urllib.request

fuel_pin_url = 'https://tinyurl.com/y3ugwz6w' # 1.2 MB
teapot_url = 'https://tinyurl.com/y4mcmc3u' # 29 MB

def download(url):
    """
    Helper function for retrieving dagmc models
    """
    u = urllib.request.urlopen(url)

    if u.status != 200:
        raise RuntimeError("Failed to download file.")

    # save file as dagmc.h5m
    with open("dagmc.h5m", 'wb') as f:
        f.write(u.read())

This notebook is intended to demonstrate how DagMC problems are run in OpenMC. For more information on how DagMC models are created, please refer to the DagMC User’s Guide.

[2]:
%matplotlib inline
from IPython.display import Image
import openmc

To start, we’ll be using a simple U235 fuel pin surrounded by a water moderator, so let’s create those materials.

[3]:
 # materials
u235 = openmc.Material(name="fuel")
u235.add_nuclide('U235', 1.0, 'ao')
u235.set_density('g/cc', 11)
u235.id = 40

water = openmc.Material(name="water")
water.add_nuclide('H1', 2.0, 'ao')
water.add_nuclide('O16', 1.0, 'ao')
water.set_density('g/cc', 1.0)
water.add_s_alpha_beta('c_H_in_H2O')
water.id = 41

mats = openmc.Materials([u235, water])
mats.export_to_xml()

Now let’s get our DAGMC geometry. We’ll be using prefabricated models in this notebook. For information on how to create your own DAGMC models, you can refer to the instructions here.

Let’s download the DAGMC model. These models come in the form of triangle surface meshes stored using the the Mesh Oriented datABase (MOAB) in an HDF5 file with the extension .h5m. An example of a coarse triangle mesh looks like:

[4]:
Image("./images/cylinder_mesh.png", width=350)
[4]:
_images/examples_cad-based-geometry_8_0.png

First we’ll need to grab some pre-made DagMC models.

[5]:
download(fuel_pin_url)

OpenMC expects that the model has the name “dagmc.h5m” so we’ll name the file that and indicate to OpenMC that a DAGMC geometry is being used by setting the settings.dagmc attribute to True.

[6]:
settings = openmc.Settings()
settings.dagmc = True
settings.batches = 10
settings.inactive = 2
settings.particles = 5000
settings.export_to_xml()

Unlike conventional geometries in OpenMC, we really have no way of knowing what our model looks like at this point. Thankfully DagMC geometries can be plotted just like any other OpenMC geometry to give us an idea of what we’re now working with.

Note that material assignments have already been applied to this model. Materials can be assigned either using ids or names of materials in the materials.xml file. It is recommended that material names are used for assignment for readability.

[7]:
p = openmc.Plot()
p.width = (25.0, 25.0)
p.pixels = (400, 400)
p.color_by = 'material'
p.colors = {u235: 'yellow', water: 'blue'}
openmc.plot_inline(p)
_images/examples_cad-based-geometry_14_0.png

Now that we’ve had a chance to examine the model a bit, we can finish applying our settings and add a source.

[8]:
settings.source = openmc.Source(space=openmc.stats.Box([-4., -4., -4.],
                                                       [ 4.,  4.,  4.]))
settings.export_to_xml()

Tallies work in the same way when using DAGMC geometries too. We’ll add a tally on the fuel cell here.

[9]:
tally = openmc.Tally()
tally.scores = ['total']
tally.filters = [openmc.CellFilter(1)]
tallies = openmc.Tallies([tally])
tallies.export_to_xml()

Note: Applying tally filters in DagMC models requires prior knowledge of the model. Here, we know that the fuel cell’s volume ID in the CAD sofware is 1. To identify cells without use of CAD software, load them into the OpenMC plotter where cell, material, and volume IDs can be identified for native both OpenMC and DagMC geometries.

Now we’re ready to run the simulation just like any other OpenMC run.

[10]:
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 2c0b16e73d5d81a2f849f4e2bfae5eb5319f2417
         Date/Time | 2019-06-18 08:54:17
    OpenMP Threads | 2

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading DAGMC geometry...
Loading file dagmc.h5m
Initializing the GeomQueryTool...
Using faceting tolerance: 0.0001
Building OBB Tree...
 Reading U235 from /home/shriwise/opt/openmc/xs/nndc_hdf5/U235.h5
 Reading H1 from /home/shriwise/opt/openmc/xs/nndc_hdf5/H1.h5
 Reading O16 from /home/shriwise/opt/openmc/xs/nndc_hdf5/O16.h5
 Reading c_H_in_H2O from /home/shriwise/opt/openmc/xs/nndc_hdf5/c_H_in_H2O.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.15789
        2/1    1.05169
        3/1    1.00736
        4/1    0.97863    0.99300 +/- 0.01436
        5/1    0.95316    0.97972 +/- 0.01566
        6/1    0.95079    0.97248 +/- 0.01322
        7/1    0.96879    0.97175 +/- 0.01027
        8/1    0.94253    0.96688 +/- 0.00970
        9/1    0.97406    0.96790 +/- 0.00826
       10/1    0.97362    0.96862 +/- 0.00719
 Creating state point statepoint.10.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 2.4575e-01 seconds
   Reading cross sections          = 1.3129e-01 seconds
 Total time in simulation          = 1.6897e+00 seconds
   Time in transport only          = 1.6829e+00 seconds
   Time in inactive batches        = 3.1605e-01 seconds
   Time in active batches          = 1.3737e+00 seconds
   Time synchronizing fission bank = 2.8739e-03 seconds
     Sampling source sites         = 2.5550e-03 seconds
     SEND/RECV source sites        = 2.8512e-04 seconds
   Time accumulating tallies       = 7.8450e-06 seconds
 Total time for finalization       = 1.7404e-04 seconds
 Total time elapsed                = 1.9588e+00 seconds
 Calculation Rate (inactive)       = 31640.8 particles/second
 Calculation Rate (active)         = 29119.1 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.97020 +/- 0.00750
 k-effective (Track-length)  = 0.96862 +/- 0.00719
 k-effective (Absorption)    = 0.95742 +/- 0.00791
 Combined k-effective        = 0.96203 +/- 0.00898
 Leakage Fraction            = 0.57677 +/- 0.00317

More Complicated Geometry

Neat! But this pincell is something we could’ve done with CSG. Let’s take a look at something more complex. We’ll download a pre-built model of the Utah teapot and use it here.

[11]:
download(teapot_url)
[12]:
Image("./images/teapot.jpg", width=600)
[12]:
_images/examples_cad-based-geometry_25_0.jpg

Our teapot is made out of iron, so we’ll want to create that material and make sure it is in our materials.xml file.

[13]:
iron = openmc.Material(name="iron")
iron.add_nuclide("Fe54", 0.0564555822608)
iron.add_nuclide("Fe56", 0.919015287728)
iron.add_nuclide("Fe57", 0.0216036861685)
iron.add_nuclide("Fe58", 0.00292544384231)
iron.set_density("g/cm3", 7.874)
mats = openmc.Materials([iron, water])
mats.export_to_xml()

To make sure we’ve updated the file correctly, let’s make a plot of the teapot.

[14]:
p = openmc.Plot()
p.basis = 'xz'
p.origin = (0.0, 0.0, 0.0)
p.width = (30.0, 20.0)
p.pixels = (450, 300)
p.color_by = 'material'
p.colors = {iron: 'gray', water: 'blue'}
openmc.plot_inline(p)
_images/examples_cad-based-geometry_29_0.png

Here we start to see some of the advantages CAD geometries provide. This particular file was pulled from the GrabCAD and pushed through the DAGMC workflow without modification (other than the addition of material assignments). It would take a considerable amount of time to create a model like this using CSG!

[15]:
p.width = (18.0, 6.0)
p.basis = 'xz'
p.origin = (10.0, 0.0, 5.0)
p.pixels = (600, 200)
p.color_by = 'material'
openmc.plot_inline(p)
_images/examples_cad-based-geometry_31_0.png

Now let’s brew some tea! … using a very hot neutron source. We’ll use some well-placed point sources distributed throughout the model.

[16]:
settings = openmc.Settings()
settings.dagmc = True
settings.batches = 10
settings.particles = 5000
settings.run_mode = "fixed source"

src_locations = ((-4.0, 0.0, -2.0),
                 ( 4.0, 0.0, -2.0),
                 ( 4.0, 0.0, -6.0),
                 (-4.0, 0.0, -6.0),
                 (10.0, 0.0, -4.0),
                 (-8.0, 0.0, -4.0))

# we'll use the same energy for each source
src_e = openmc.stats.Discrete(x=[12.0,], p=[1.0,])

# create source for each location
sources = []
for loc in src_locations:
    src_pnt = openmc.stats.Point(xyz=loc)
    src = openmc.Source(space=src_pnt, energy=src_e)
    sources.append(src)

src_str = 1.0 / len(sources)
for source in sources:
    source.strength = src_str

settings.source = sources
settings.export_to_xml()

…and setup a couple mesh tallies. One for the kettle, and one for the water inside.

[17]:
mesh = openmc.RegularMesh()
mesh.dimension = (120, 1, 40)
mesh.lower_left = (-20.0, 0.0, -10.0)
mesh.upper_right = (20.0, 1.0, 4.0)

mesh_filter = openmc.MeshFilter(mesh)

pot_filter = openmc.CellFilter([1])
pot_tally = openmc.Tally()
pot_tally.filters = [mesh_filter, pot_filter]
pot_tally.scores = ['flux']

water_filter = openmc.CellFilter([5])
water_tally = openmc.Tally()
water_tally.filters = [mesh_filter, water_filter]
water_tally.scores = ['flux']


tallies = openmc.Tallies([pot_tally, water_tally])
tallies.export_to_xml()
[18]:
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 2c0b16e73d5d81a2f849f4e2bfae5eb5319f2417
         Date/Time | 2019-06-18 08:54:35
    OpenMP Threads | 2

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading DAGMC geometry...
Loading file dagmc.h5m
Initializing the GeomQueryTool...
Using faceting tolerance: 0.001
Building OBB Tree...
 Reading Fe54 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe54.h5
 Reading Fe56 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe56.h5
 Reading Fe57 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe57.h5
 Reading Fe58 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe58.h5
 Reading H1 from /home/shriwise/opt/openmc/xs/nndc_hdf5/H1.h5
 Reading O16 from /home/shriwise/opt/openmc/xs/nndc_hdf5/O16.h5
 Reading c_H_in_H2O from /home/shriwise/opt/openmc/xs/nndc_hdf5/c_H_in_H2O.h5
 Maximum neutron transport energy: 20000000.000000 eV for Fe58
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ===============>     FIXED SOURCE TRANSPORT SIMULATION     <===============

 Simulating batch 1
 Simulating batch 2
 Simulating batch 3
 Simulating batch 4
 Simulating batch 5
 Simulating batch 6
 Simulating batch 7
 Simulating batch 8
 Simulating batch 9
 Simulating batch 10
 Creating state point statepoint.10.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 4.9659e+00 seconds
   Reading cross sections          = 6.3783e-01 seconds
 Total time in simulation          = 1.5112e+01 seconds
   Time in transport only          = 1.4102e+01 seconds
   Time in active batches          = 1.5112e+01 seconds
   Time accumulating tallies       = 2.8790e-04 seconds
 Total time for finalization       = 3.2375e-02 seconds
 Total time elapsed                = 2.0224e+01 seconds
 Calculation Rate (active)         = 3308.59 particles/second

 ============================>     RESULTS     <============================

 Leakage Fraction            = 0.62594 +/- 0.00117

Note that the performance is significantly lower than our pincell model due to the increased complexity of the model, but it allows us to examine tally results like these:

[19]:
sp = openmc.StatePoint("statepoint.10.h5")

water_tally = sp.get_tally(scores=['flux'], id=water_tally.id)
water_flux = water_tally.mean
water_flux.shape = (40, 120)
water_flux = water_flux[::-1, :]

pot_tally = sp.get_tally(scores=['flux'], id=pot_tally.id)
pot_flux = pot_tally.mean
pot_flux.shape = (40, 120)
pot_flux = pot_flux[::-1, :]

del sp
[20]:
from matplotlib import pyplot as plt
fig = plt.figure(figsize=(18, 16))

sub_plot1 = plt.subplot(121, title="Kettle Flux")
sub_plot1.imshow(pot_flux)

sub_plot2 = plt.subplot(122, title="Water Flux")
sub_plot2.imshow(water_flux)
[20]:
<matplotlib.image.AxesImage at 0x7f90d12b8198>
_images/examples_cad-based-geometry_39_1.png

Multigroup Cross Section Generation

Multigroup Cross Section Generation Part I: Introduction

This IPython Notebook introduces the use of the openmc.mgxs module to calculate multi-group cross sections for an infinite homogeneous medium. In particular, this Notebook introduces the the following features:

  • General equations for scalar-flux averaged multi-group cross sections
  • Creation of multi-group cross sections for an infinite homogeneous medium
  • Use of tally arithmetic to manipulate multi-group cross sections
Introduction to Multi-Group Cross Sections (MGXS)

Many Monte Carlo particle transport codes, including OpenMC, use continuous-energy nuclear cross section data. However, most deterministic neutron transport codes use multi-group cross sections defined over discretized energy bins or energy groups. An example of U-235’s continuous-energy fission cross section along with a 16-group cross section computed for a light water reactor spectrum is displayed below.

[1]:
from IPython.display import Image
Image(filename='images/mgxs.png', width=350)
[1]:
_images/examples_mgxs-part-i_3_0.png

A variety of tools employing different methodologies have been developed over the years to compute multi-group cross sections for certain applications, including NJOY (LANL), MC\(^2\)-3 (ANL), and Serpent (VTT). The openmc.mgxs Python module is designed to leverage OpenMC’s tally system to calculate multi-group cross sections with arbitrary energy discretizations for fine-mesh heterogeneous deterministic neutron transport applications.

Before proceeding to illustrate how one may use the openmc.mgxs module, it is worthwhile to define the general equations used to calculate multi-group cross sections. This is only intended as a brief overview of the methodology used by openmc.mgxs - we refer the interested reader to the large body of literature on the subject for a more comprehensive understanding of this complex topic.

Introductory Notation

The continuous real-valued microscopic cross section may be denoted \(\sigma_{n,x}(\mathbf{r}, E)\) for position vector \(\mathbf{r}\), energy \(E\), nuclide \(n\) and interaction type \(x\). Similarly, the scalar neutron flux may be denoted by \(\Phi(\mathbf{r},E)\) for position \(\mathbf{r}\) and energy \(E\). Note: Although nuclear cross sections are dependent on the temperature \(T\) of the interacting medium, the temperature variable is neglected here for brevity.

Spatial and Energy Discretization

The energy domain for critical systems such as thermal reactors spans more than 10 orders of magnitude of neutron energies from 10\(^{-5}\) - 10\(^7\) eV. The multi-group approximation discretization divides this energy range into one or more energy groups. In particular, for \(G\) total groups, we denote an energy group index \(g\) such that \(g \in \{1, 2, ..., G\}\). The energy group indices are defined such that the smaller group the higher the energy, and vice versa. The integration over neutron energies across a discrete energy group is commonly referred to as energy condensation.

Multi-group cross sections are computed for discretized spatial zones in the geometry of interest. The spatial zones may be defined on a structured and regular fuel assembly or pin cell mesh, an arbitrary unstructured mesh or the constructive solid geometry used by OpenMC. For a geometry with \(K\) distinct spatial zones, we designate each spatial zone an index \(k\) such that \(k \in \{1, 2, ..., K\}\). The volume of each spatial zone is denoted by \(V_{k}\). The integration over discrete spatial zones is commonly referred to as spatial homogenization.

General Scalar-Flux Weighted MGXS

The multi-group cross sections computed by openmc.mgxs are defined as a scalar flux-weighted average of the microscopic cross sections across each discrete energy group. This formulation is employed in order to preserve the reaction rates within each energy group and spatial zone. In particular, spatial homogenization and energy condensation are used to compute the general multi-group cross section \(\sigma_{n,x,k,g}\) as follows:

\[\sigma_{n,x,k,g} = \frac{\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\sigma_{n,x}(\mathbf{r},E')\Phi(\mathbf{r},E')}{\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\Phi(\mathbf{r},E')} \]

This scalar flux-weighted average microscopic cross section is computed by openmc.mgxs for most multi-group cross sections, including total, absorption, and fission reaction types. These double integrals are stochastically computed with OpenMC’s tally system - in particular, filters on the energy range and spatial zone (material, cell or universe) define the bounds of integration for both numerator and denominator.

Multi-Group Scattering Matrices

The general multi-group cross section \(\sigma_{n,x,k,g}\) is a vector of \(G\) values for each energy group \(g\). The equation presented above only discretizes the energy of the incoming neutron and neglects the outgoing energy of the neutron (if any). Hence, this formulation must be extended to account for the outgoing energy of neutrons in the discretized scattering matrix cross section used by deterministic neutron transport codes.

We denote the incoming and outgoing neutron energy groups as \(g\) and \(g'\) for the microscopic scattering matrix cross section \(\sigma_{n,s}(\mathbf{r},E)\). As before, spatial homogenization and energy condensation are used to find the multi-group scattering matrix cross section \(\sigma_{n,s,k,g \to g'}\) as follows:

\[\sigma_{n,s,k,g\rightarrow g'} = \frac{\int_{E_{g'}}^{E_{g'-1}}\mathrm{d}E''\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\sigma_{n,s}(\mathbf{r},E'\rightarrow E'')\Phi(\mathbf{r},E')}{\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\Phi(\mathbf{r},E')} \]

This scalar flux-weighted multi-group microscopic scattering matrix is computed using OpenMC tallies with both energy in and energy out filters.

Multi-Group Fission Spectrum

The energy spectrum of neutrons emitted from fission is denoted by \(\chi_{n}(\mathbf{r},E' \rightarrow E'')\) for incoming and outgoing energies \(E'\) and \(E''\), respectively. Unlike the multi-group cross sections \(\sigma_{n,x,k,g}\) considered up to this point, the fission spectrum is a probability distribution and must sum to unity. The outgoing energy is typically much less dependent on the incoming energy for fission than for scattering interactions. As a result, it is common practice to integrate over the incoming neutron energy when computing the multi-group fission spectrum. The fission spectrum may be simplified as \(\chi_{n}(\mathbf{r},E)\) with outgoing energy \(E\).

Unlike the multi-group cross sections defined up to this point, the multi-group fission spectrum is weighted by the fission production rate rather than the scalar flux. This formulation is intended to preserve the total fission production rate in the multi-group deterministic calculation. In order to mathematically define the multi-group fission spectrum, we denote the microscopic fission cross section as \(\sigma_{n,f}(\mathbf{r},E)\) and the average number of neutrons emitted from fission interactions with nuclide \(n\) as \(\nu_{n}(\mathbf{r},E)\). The multi-group fission spectrum \(\chi_{n,k,g}\) is then the probability of fission neutrons emitted into energy group \(g\).

Similar to before, spatial homogenization and energy condensation are used to find the multi-group fission spectrum \(\chi_{n,k,g}\) as follows:

\[\chi_{n,k,g'} = \frac{\int_{E_{g'}}^{E_{g'-1}}\mathrm{d}E''\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\chi_{n}(\mathbf{r},E'\rightarrow E'')\nu_{n}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')}{\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\nu_{n}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')} \]

The fission production-weighted multi-group fission spectrum is computed using OpenMC tallies with both energy in and energy out filters.

This concludes our brief overview on the methodology to compute multi-group cross sections. The following sections detail more concretely how users may employ the openmc.mgxs module to power simulation workflows requiring multi-group cross sections for downstream deterministic calculations.

Generate Input Files
[2]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

import openmc
import openmc.mgxs as mgxs

We being by creating a material for the homogeneous medium.

[3]:
# Instantiate a Material and register the Nuclides
inf_medium = openmc.Material(name='moderator')
inf_medium.set_density('g/cc', 5.)
inf_medium.add_nuclide('H1',  0.028999667)
inf_medium.add_nuclide('O16', 0.01450188)
inf_medium.add_nuclide('U235', 0.000114142)
inf_medium.add_nuclide('U238', 0.006886019)
inf_medium.add_nuclide('Zr90', 0.002116053)

With our material, we can now create a Materials object that can be exported to an actual XML file.

[4]:
# Instantiate a Materials collection and export to XML
materials_file = openmc.Materials([inf_medium])
materials_file.export_to_xml()

Now let’s move on to the geometry. This problem will be a simple square cell with reflective boundary conditions to simulate an infinite homogeneous medium. The first step is to create the outer bounding surfaces of the problem.

[5]:
# Instantiate boundary Planes
min_x = openmc.XPlane(boundary_type='reflective', x0=-0.63)
max_x = openmc.XPlane(boundary_type='reflective', x0=0.63)
min_y = openmc.YPlane(boundary_type='reflective', y0=-0.63)
max_y = openmc.YPlane(boundary_type='reflective', y0=0.63)

With the surfaces defined, we can now create a cell that is defined by intersections of half-spaces created by the surfaces.

[6]:
# Instantiate a Cell
cell = openmc.Cell(cell_id=1, name='cell')

# Register bounding Surfaces with the Cell
cell.region = +min_x & -max_x & +min_y & -max_y

# Fill the Cell with the Material
cell.fill = inf_medium

OpenMC requires that there is a “root” universe. Let us create a root universe and add our square cell to it.

[7]:
# Create root universe
root_universe = openmc.Universe(name='root universe', cells=[cell])

We now must create a geometry that is assigned a root universe and export it to XML.

[8]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry(root_universe)

# Export to "geometry.xml"
openmc_geometry.export_to_xml()

Next, we must define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 2500 particles.

[9]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 2500

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()

Now we are ready to generate multi-group cross sections! First, let’s define a 2-group structure using the built-in EnergyGroups class.

[10]:
# Instantiate a 2-group EnergyGroups object
groups = mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])

We can now use the EnergyGroups object, along with our previously created materials and geometry, to instantiate some MGXS objects from the openmc.mgxs module. In particular, the following are subclasses of the generic and abstract MGXS class:

  • TotalXS
  • TransportXS
  • AbsorptionXS
  • CaptureXS
  • FissionXS
  • KappaFissionXS
  • ScatterXS
  • ScatterMatrixXS
  • Chi
  • ChiPrompt
  • InverseVelocity
  • PromptNuFissionXS

Of course, we are aware that the fission cross section (FissionXS) can sometimes be paired with the fission neutron multiplication to become \(\nu\sigma_f\). This can be accomodated in to the FissionXS class by setting the nu parameter to True as shown below.

Additionally, scattering reactions (like (n,2n)) can also be defined to take in to account the neutron multiplication to become \(\nu\sigma_s\). This can be accomodated in the the transport (TransportXS), scattering (ScatterXS), and scattering-matrix (ScatterMatrixXS) cross sections types by setting the nu parameter to True as shown below.

These classes provide us with an interface to generate the tally inputs as well as perform post-processing of OpenMC’s tally data to compute the respective multi-group cross sections. In this case, let’s create the multi-group total, absorption and scattering cross sections with our 2-group structure.

[11]:
# Instantiate a few different sections
total = mgxs.TotalXS(domain=cell, groups=groups)
absorption = mgxs.AbsorptionXS(domain=cell, groups=groups)
scattering = mgxs.ScatterXS(domain=cell, groups=groups)

# Note that if we wanted to incorporate neutron multiplication in the
# scattering cross section we would write the previous line as:
# scattering = mgxs.ScatterXS(domain=cell, groups=groups, nu=True)

Each multi-group cross section object stores its tallies in a Python dictionary called tallies. We can inspect the tallies in the dictionary for our Absorption object as follows.

[12]:
absorption.tallies
[12]:
OrderedDict([('flux', Tally
                ID             =        1
                Name           =
                Filters        =        CellFilter, EnergyFilter
                Nuclides       =        total
                Scores         =        ['flux']
                Estimator      =        tracklength), ('absorption', Tally
                ID             =        2
                Name           =
                Filters        =        CellFilter, EnergyFilter
                Nuclides       =        total
                Scores         =        ['absorption']
                Estimator      =        tracklength)])

The Absorption object includes tracklength tallies for the ‘absorption’ and ‘flux’ scores in the 2-group structure in cell 1. Now that each MGXS object contains the tallies that it needs, we must add these tallies to a Tallies object to generate the “tallies.xml” input file for OpenMC.

[13]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()

# Add total tallies to the tallies file
tallies_file += total.tallies.values()

# Add absorption tallies to the tallies file
tallies_file += absorption.tallies.values()

# Add scattering tallies to the tallies file
tallies_file += scattering.tallies.values()

# Export to "tallies.xml"
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:61: IDWarning: Another CellFilter instance already exists with id=3.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:61: IDWarning: Another EnergyFilter instance already exists with id=4.
  warn(msg, IDWarning)

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[14]:
# Run OpenMC
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2017 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.9.0
          Git SHA1 | 9b7cebf7bc34d60e0f1750c3d6cb103df11e8dc4
         Date/Time | 2017-12-04 20:56:46
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Building neighboring cells lists for each surface...
 Reading H1 from /home/romano/openmc/scripts/nndc_hdf5/H1.h5
 Reading O16 from /home/romano/openmc/scripts/nndc_hdf5/O16.h5
 Reading U235 from /home/romano/openmc/scripts/nndc_hdf5/U235.h5
 Reading U238 from /home/romano/openmc/scripts/nndc_hdf5/U238.h5
 Reading Zr90 from /home/romano/openmc/scripts/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for H1
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.11184
        2/1    1.15820
        3/1    1.18468
        4/1    1.17492
        5/1    1.19645
        6/1    1.18436
        7/1    1.14070
        8/1    1.15150
        9/1    1.19202
       10/1    1.17677
       11/1    1.20272
       12/1    1.21366    1.20819 +/- 0.00547
       13/1    1.15906    1.19181 +/- 0.01668
       14/1    1.14687    1.18058 +/- 0.01629
       15/1    1.14570    1.17360 +/- 0.01442
       16/1    1.13480    1.16713 +/- 0.01343
       17/1    1.17680    1.16852 +/- 0.01144
       18/1    1.16866    1.16853 +/- 0.00990
       19/1    1.19253    1.17120 +/- 0.00913
       20/1    1.18124    1.17220 +/- 0.00823
       21/1    1.19206    1.17401 +/- 0.00766
       22/1    1.17681    1.17424 +/- 0.00700
       23/1    1.17634    1.17440 +/- 0.00644
       24/1    1.13659    1.17170 +/- 0.00654
       25/1    1.17144    1.17169 +/- 0.00609
       26/1    1.20649    1.17386 +/- 0.00610
       27/1    1.11238    1.17024 +/- 0.00678
       28/1    1.18911    1.17129 +/- 0.00647
       29/1    1.14681    1.17000 +/- 0.00626
       30/1    1.12152    1.16758 +/- 0.00641
       31/1    1.12729    1.16566 +/- 0.00639
       32/1    1.15399    1.16513 +/- 0.00612
       33/1    1.13547    1.16384 +/- 0.00599
       34/1    1.17723    1.16440 +/- 0.00576
       35/1    1.09296    1.16154 +/- 0.00622
       36/1    1.19621    1.16287 +/- 0.00612
       37/1    1.12560    1.16149 +/- 0.00605
       38/1    1.17872    1.16211 +/- 0.00586
       39/1    1.17721    1.16263 +/- 0.00568
       40/1    1.13724    1.16178 +/- 0.00555
       41/1    1.18526    1.16254 +/- 0.00542
       42/1    1.13779    1.16177 +/- 0.00531
       43/1    1.15066    1.16143 +/- 0.00516
       44/1    1.12174    1.16026 +/- 0.00514
       45/1    1.17478    1.16068 +/- 0.00501
       46/1    1.14146    1.16014 +/- 0.00489
       47/1    1.20464    1.16135 +/- 0.00491
       48/1    1.15119    1.16108 +/- 0.00479
       49/1    1.17938    1.16155 +/- 0.00468
       50/1    1.15798    1.16146 +/- 0.00457
 Creating state point statepoint.50.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     =  4.0504E-01 seconds
   Reading cross sections          =  3.6457E-01 seconds
 Total time in simulation          =  6.3478E+00 seconds
   Time in transport only          =  6.0079E+00 seconds
   Time in inactive batches        =  8.1713E-01 seconds
   Time in active batches          =  5.5307E+00 seconds
   Time synchronizing fission bank =  5.4640E-03 seconds
     Sampling source sites         =  4.0981E-03 seconds
     SEND/RECV source sites        =  1.2606E-03 seconds
   Time accumulating tallies       =  1.2030E-04 seconds
 Total time for finalization       =  9.6554E-04 seconds
 Total time elapsed                =  6.7713E+00 seconds
 Calculation Rate (inactive)       =  30594.8 neutrons/second
 Calculation Rate (active)         =  18080.8 neutrons/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.15984 +/-  0.00411
 k-effective (Track-length)  =  1.16146 +/-  0.00457
 k-effective (Absorption)    =  1.16177 +/-  0.00380
 Combined k-effective        =  1.16105 +/-  0.00364
 Leakage Fraction            =  0.00000 +/-  0.00000

[14]:
0
Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

[15]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.50.h5')

In addition to the statepoint file, our simulation also created a summary file which encapsulates information about the materials and geometry. By default, a Summary object is automatically linked when a StatePoint is loaded. This is necessary for the openmc.mgxs module to properly process the tally data.

The statepoint is now ready to be analyzed by our multi-group cross sections. We simply have to load the tallies from the StatePoint into each object as follows and our MGXS objects will compute the cross sections for us under-the-hood.

[16]:
# Load the tallies from the statepoint into each MGXS object
total.load_from_statepoint(sp)
absorption.load_from_statepoint(sp)
scattering.load_from_statepoint(sp)

Voila! Our multi-group cross sections are now ready to rock ‘n roll!

Extracting and Storing MGXS Data

Let’s first inspect our total cross section by printing it to the screen.

[17]:
total.print_xs()
Multi-Group XS
        Reaction Type  =        total
        Domain Type    =        cell
        Domain ID      =        1
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        6.81e-01 +/- 2.69e-01%
            Group 2 [0.0        - 0.625     eV]:        1.40e+00 +/- 5.93e-01%



Since the openmc.mgxs module uses tally arithmetic under-the-hood, the cross section is stored as a “derived” Tally object. This means that it can be queried and manipulated using all of the same methods supported for the Tally class in the OpenMC Python API. For example, we can construct a Pandas DataFrame of the multi-group cross section data.

[18]:
df = scattering.get_pandas_dataframe()
df.head(10)
[18]:
cell group in nuclide mean std. dev.
1 1 1 total 0.667787 0.001802
0 1 2 total 1.292013 0.007642

Each multi-group cross section object can be easily exported to a variety of file formats, including CSV, Excel, and LaTeX for storage or data processing.

[19]:
absorption.export_xs_data(filename='absorption-xs', format='excel')

The following code snippet shows how to export all three MGXS to the same HDF5 binary data store.

[20]:
total.build_hdf5_store(filename='mgxs', append=True)
absorption.build_hdf5_store(filename='mgxs', append=True)
scattering.build_hdf5_store(filename='mgxs', append=True)
Comparing MGXS with Tally Arithmetic

Finally, we illustrate how one can leverage OpenMC’s tally arithmetic data processing feature with MGXS objects. The openmc.mgxs module uses tally arithmetic to compute multi-group cross sections with automated uncertainty propagation. Each MGXS object includes an xs_tally attribute which is a “derived” Tally based on the tallies needed to compute the cross section type of interest. These derived tallies can be used in subsequent tally arithmetic operations. For example, we can use tally artithmetic to confirm that the TotalXS is equal to the sum of the AbsorptionXS and ScatterXS objects.

[21]:
# Use tally arithmetic to compute the difference between the total, absorption and scattering
difference = total.xs_tally - absorption.xs_tally - scattering.xs_tally

# The difference is a derived tally which can generate Pandas DataFrames for inspection
difference.get_pandas_dataframe()
[21]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 total (((total / flux) - (absorption / flux)) - (sca... -1.110223e-15 0.011292
1 1 0.625 2.000000e+07 total (((total / flux) - (absorption / flux)) - (sca... 7.771561e-16 0.002570

Similarly, we can use tally arithmetic to compute the ratio of AbsorptionXS and ScatterXS to the TotalXS.

[22]:
# Use tally arithmetic to compute the absorption-to-total MGXS ratio
absorption_to_total = absorption.xs_tally / total.xs_tally

# The absorption-to-total ratio is a derived tally which can generate Pandas DataFrames for inspection
absorption_to_total.get_pandas_dataframe()
[22]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 total ((absorption / flux) / (total / flux)) 0.076115 0.000649
1 1 0.625 2.000000e+07 total ((absorption / flux) / (total / flux)) 0.019263 0.000095
[23]:
# Use tally arithmetic to compute the scattering-to-total MGXS ratio
scattering_to_total = scattering.xs_tally / total.xs_tally

# The scattering-to-total ratio is a derived tally which can generate Pandas DataFrames for inspection
scattering_to_total.get_pandas_dataframe()
[23]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 total ((scatter / flux) / (total / flux)) 0.923885 0.007736
1 1 0.625 2.000000e+07 total ((scatter / flux) / (total / flux)) 0.980737 0.003737

Lastly, we sum the derived scatter-to-total and absorption-to-total ratios to confirm that they sum to unity.

[24]:
# Use tally arithmetic to ensure that the absorption- and scattering-to-total MGXS ratios sum to unity
sum_ratio = absorption_to_total + scattering_to_total

# The sum ratio is a derived tally which can generate Pandas DataFrames for inspection
sum_ratio.get_pandas_dataframe()
[24]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 1 0.000 6.250000e-01 total (((absorption / flux) / (total / flux)) + ((sc... 1.0 0.007763
1 1 0.625 2.000000e+07 total (((absorption / flux) / (total / flux)) + ((sc... 1.0 0.003739

Multigroup Cross Section Generation Part II: Advanced Features

This IPython Notebook illustrates the use of the openmc.mgxs module to calculate multi-group cross sections for a heterogeneous fuel pin cell geometry. In particular, this Notebook illustrates the following features:

  • Creation of multi-group cross sections on a heterogeneous geometry
  • Calculation of cross sections on a nuclide-by-nuclide basis
  • The use of `tally precision triggers <../io_formats/settings.rst#trigger-element>`__ with multi-group cross sections
  • Built-in features for energy condensation in downstream data processing
  • The use of the ``openmc.data`` module to plot continuous-energy vs. multi-group cross sections
  • Validation of multi-group cross sections with `OpenMOC <https://mit-crpg.github.io/OpenMOC/>`__

Note: This Notebook was created using OpenMOC to verify the multi-group cross-sections generated by OpenMC. You must install OpenMOC on your system in order to run this Notebook in its entirety. In addition, this Notebook illustrates the use of Pandas DataFrames to containerize multi-group cross section data.

Generate Input Files
[1]:
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-dark')

import openmoc

import openmc
import openmc.mgxs as mgxs
import openmc.data
from openmc.openmoc_compatible import get_openmoc_geometry

%matplotlib inline

First we need to define materials that will be used in the problem. We’ll create three distinct materials for water, clad and fuel.

[2]:
# 1.6% enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our materials, we can now create a Materials object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials collection
materials_file = openmc.Materials([fuel, water, zircaloy])

# Export to "materials.xml"
materials_file.export_to_xml()

Now let’s move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces – in this case two cylinders and six reflective planes.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.45720)

# Create box to surround the geometry
box = openmc.model.rectangular_prism(1.26, 1.26, boundary_type='reflective')

With the surfaces defined, we can now create cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
pin_cell_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
pin_cell_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
pin_cell_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius & box
pin_cell_universe.add_cell(moderator_cell)

We now must create a geometry with the pin cell universe and export it to XML.

[6]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry(pin_cell_universe)

# Export to "geometry.xml"
openmc_geometry.export_to_xml()

Next, we must define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 10,000 particles.

[7]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 10000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Activate tally precision triggers
settings_file.trigger_active = True
settings_file.trigger_max_batches = settings_file.batches * 4

# Export to "settings.xml"
settings_file.export_to_xml()

Now we are finally ready to make use of the openmc.mgxs module to generate multi-group cross sections! First, let’s define “coarse” 2-group and “fine” 8-group structures using the built-in EnergyGroups class.

[8]:
# Instantiate a "coarse" 2-group EnergyGroups object
coarse_groups = mgxs.EnergyGroups([0., 0.625, 20.0e6])

# Instantiate a "fine" 8-group EnergyGroups object
fine_groups = mgxs.EnergyGroups([0., 0.058, 0.14, 0.28,
                                 0.625, 4.0, 5.53e3, 821.0e3, 20.0e6])

Now we will instantiate a variety of MGXS objects needed to run an OpenMOC simulation to verify the accuracy of our cross sections. In particular, we define transport, fission, nu-fission, nu-scatter and chi cross sections for each of the three cells in the fuel pin with the 8-group structure as our energy groups.

[9]:
# Extract all Cells filled by Materials
openmc_cells = openmc_geometry.get_all_material_cells().values()

# Create dictionary to store multi-group cross sections for all cells
xs_library = {}

# Instantiate 8-group cross sections for each cell
for cell in openmc_cells:
    xs_library[cell.id] = {}
    xs_library[cell.id]['transport']  = mgxs.TransportXS(groups=fine_groups)
    xs_library[cell.id]['fission'] = mgxs.FissionXS(groups=fine_groups)
    xs_library[cell.id]['nu-fission'] = mgxs.FissionXS(groups=fine_groups, nu=True)
    xs_library[cell.id]['nu-scatter'] = mgxs.ScatterMatrixXS(groups=fine_groups, nu=True)
    xs_library[cell.id]['chi'] = mgxs.Chi(groups=fine_groups)

Next, we showcase the use of OpenMC’s tally precision trigger feature in conjunction with the openmc.mgxs module. In particular, we will assign a tally trigger of 1E-2 on the standard deviation for each of the tallies used to compute multi-group cross sections.

[10]:
# Create a tally trigger for +/- 0.01 on each tally used to compute the multi-group cross sections
tally_trigger = openmc.Trigger('std_dev', 1e-2)

# Add the tally trigger to each of the multi-group cross section tallies
for cell in openmc_cells:
    for mgxs_type in xs_library[cell.id]:
        xs_library[cell.id][mgxs_type].tally_trigger = tally_trigger

Now, we must loop over all cells to set the cross section domains to the various cells - fuel, clad and moderator - included in the geometry. In addition, we will set each cross section to tally cross sections on a per-nuclide basis through the use of the MGXS class’ boolean by_nuclide instance attribute.

[11]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()

# Iterate over all cells and cross section types
for cell in openmc_cells:
    for rxn_type in xs_library[cell.id]:

        # Set the cross sections domain to the cell
        xs_library[cell.id][rxn_type].domain = cell

        # Tally cross sections by nuclide
        xs_library[cell.id][rxn_type].by_nuclide = True

        # Add OpenMC tallies to the tallies file for XML generation
        for tally in xs_library[cell.id][rxn_type].tallies.values():
            tallies_file.append(tally, merge=True)

# Export to "tallies.xml"
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=53.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=21.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=3.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=4.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=41.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=15.
  warn(msg, IDWarning)

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[12]:
# Run OpenMC
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-19 07:08:16
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.20332
        2/1    1.22209
        3/1    1.24322
        4/1    1.21622
        5/1    1.25850
        6/1    1.22581
        7/1    1.21118
        8/1    1.23377
        9/1    1.24254
       10/1    1.21241
       11/1    1.21042
       12/1    1.23539    1.22290 +/- 0.01249
       13/1    1.22436    1.22339 +/- 0.00723
       14/1    1.22888    1.22476 +/- 0.00529
       15/1    1.22553    1.22491 +/- 0.00410
       16/1    1.24194    1.22775 +/- 0.00439
       17/1    1.24755    1.23058 +/- 0.00466
       18/1    1.21117    1.22815 +/- 0.00471
       19/1    1.22530    1.22784 +/- 0.00417
       20/1    1.20762    1.22582 +/- 0.00424
       21/1    1.20377    1.22381 +/- 0.00433
       22/1    1.24305    1.22541 +/- 0.00426
       23/1    1.22434    1.22533 +/- 0.00392
       24/1    1.22937    1.22562 +/- 0.00364
       25/1    1.22458    1.22555 +/- 0.00339
       26/1    1.18978    1.22332 +/- 0.00388
       27/1    1.20582    1.22229 +/- 0.00379
       28/1    1.22719    1.22256 +/- 0.00358
       29/1    1.21307    1.22206 +/- 0.00343
       30/1    1.20915    1.22141 +/- 0.00331
       31/1    1.22799    1.22173 +/- 0.00317
       32/1    1.21251    1.22131 +/- 0.00305
       33/1    1.20540    1.22062 +/- 0.00299
       34/1    1.20052    1.21978 +/- 0.00299
       35/1    1.24552    1.22081 +/- 0.00304
       36/1    1.21685    1.22066 +/- 0.00293
       37/1    1.22395    1.22078 +/- 0.00282
       38/1    1.22379    1.22089 +/- 0.00272
       39/1    1.20951    1.22049 +/- 0.00265
       40/1    1.25199    1.22154 +/- 0.00277
       41/1    1.23243    1.22190 +/- 0.00270
       42/1    1.20973    1.22152 +/- 0.00264
       43/1    1.24682    1.22228 +/- 0.00268
       44/1    1.20694    1.22183 +/- 0.00263
       45/1    1.22196    1.22183 +/- 0.00256
       46/1    1.20687    1.22142 +/- 0.00252
       47/1    1.22023    1.22139 +/- 0.00245
       48/1    1.22204    1.22140 +/- 0.00239
       49/1    1.22077    1.22139 +/- 0.00232
       50/1    1.23166    1.22164 +/- 0.00228
 Triggers unsatisfied, max unc./thresh. is 1.17623 for flux in tally 53
 The estimated number of batches is 66
 Creating state point statepoint.050.h5...
       51/1    1.20071    1.22113 +/- 0.00228
 Triggers unsatisfied, max unc./thresh. is 1.26577 for flux in tally 53
 The estimated number of batches is 76
       52/1    1.21423    1.22097 +/- 0.00223
 Triggers unsatisfied, max unc./thresh. is 1.24 for flux in tally 53
 The estimated number of batches is 75
       53/1    1.25595    1.22178 +/- 0.00233
 Triggers unsatisfied, max unc./thresh. is 1.2112 for flux in tally 53
 The estimated number of batches is 74
       54/1    1.21806    1.22170 +/- 0.00227
 Triggers unsatisfied, max unc./thresh. is 1.18484 for flux in tally 53
 The estimated number of batches is 72
       55/1    1.22911    1.22186 +/- 0.00223
 Triggers unsatisfied, max unc./thresh. is 1.1596 for flux in tally 53
 The estimated number of batches is 71
       56/1    1.23054    1.22205 +/- 0.00219
 Triggers unsatisfied, max unc./thresh. is 1.13453 for flux in tally 53
 The estimated number of batches is 70
       57/1    1.19384    1.22145 +/- 0.00222
 Triggers unsatisfied, max unc./thresh. is 1.11914 for flux in tally 53
 The estimated number of batches is 69
       58/1    1.20625    1.22114 +/- 0.00220
 Triggers unsatisfied, max unc./thresh. is 1.11471 for flux in tally 53
 The estimated number of batches is 70
       59/1    1.21977    1.22111 +/- 0.00216
 Triggers unsatisfied, max unc./thresh. is 1.10334 for flux in tally 53
 The estimated number of batches is 70
       60/1    1.20813    1.22085 +/- 0.00213
 Triggers unsatisfied, max unc./thresh. is 1.09813 for flux in tally 53
 The estimated number of batches is 71
       61/1    1.22077    1.22085 +/- 0.00209
 Triggers unsatisfied, max unc./thresh. is 1.10221 for flux in tally 53
 The estimated number of batches is 72
       62/1    1.21956    1.22082 +/- 0.00205
 Triggers unsatisfied, max unc./thresh. is 1.11395 for flux in tally 53
 The estimated number of batches is 75
       63/1    1.22360    1.22087 +/- 0.00201
 Triggers unsatisfied, max unc./thresh. is 1.09283 for flux in tally 53
 The estimated number of batches is 74
       64/1    1.23955    1.22122 +/- 0.00200
 Triggers unsatisfied, max unc./thresh. is 1.07416 for flux in tally 53
 The estimated number of batches is 73
       65/1    1.21143    1.22104 +/- 0.00197
 Triggers unsatisfied, max unc./thresh. is 1.06461 for flux in tally 53
 The estimated number of batches is 73
       66/1    1.21791    1.22099 +/- 0.00194
 Triggers unsatisfied, max unc./thresh. is 1.13207 for flux in tally 53
 The estimated number of batches is 82
       67/1    1.24897    1.22148 +/- 0.00196
 Triggers unsatisfied, max unc./thresh. is 1.11277 for flux in tally 53
 The estimated number of batches is 81
       68/1    1.22221    1.22149 +/- 0.00193
 Triggers unsatisfied, max unc./thresh. is 1.09514 for flux in tally 53
 The estimated number of batches is 80
       69/1    1.25627    1.22208 +/- 0.00199
 Triggers unsatisfied, max unc./thresh. is 1.07653 for flux in tally 53
 The estimated number of batches is 79
       70/1    1.21493    1.22196 +/- 0.00196
 Triggers unsatisfied, max unc./thresh. is 1.12831 for flux in tally 53
 The estimated number of batches is 87
       71/1    1.23406    1.22216 +/- 0.00193
 Triggers unsatisfied, max unc./thresh. is 1.11005 for flux in tally 53
 The estimated number of batches is 86
       72/1    1.23842    1.22242 +/- 0.00192
 Triggers unsatisfied, max unc./thresh. is 1.09352 for flux in tally 53
 The estimated number of batches is 85
       73/1    1.24542    1.22279 +/- 0.00193
 Triggers unsatisfied, max unc./thresh. is 1.08766 for flux in tally 53
 The estimated number of batches is 85
       74/1    1.21314    1.22263 +/- 0.00190
 Triggers unsatisfied, max unc./thresh. is 1.07419 for flux in tally 53
 The estimated number of batches is 84
       75/1    1.26484    1.22328 +/- 0.00198
 Triggers unsatisfied, max unc./thresh. is 1.06788 for flux in tally 53
 The estimated number of batches is 85
       76/1    1.22243    1.22327 +/- 0.00195
 Triggers unsatisfied, max unc./thresh. is 1.05164 for flux in tally 53
 The estimated number of batches is 83
       77/1    1.21865    1.22320 +/- 0.00192
 Triggers unsatisfied, max unc./thresh. is 1.04022 for flux in tally 53
 The estimated number of batches is 83
       78/1    1.23500    1.22338 +/- 0.00190
 Triggers unsatisfied, max unc./thresh. is 1.0275 for flux in tally 53
 The estimated number of batches is 82
       79/1    1.22125    1.22334 +/- 0.00187
 Triggers unsatisfied, max unc./thresh. is 1.0283 for flux in tally 53
 The estimated number of batches is 83
       80/1    1.23793    1.22355 +/- 0.00186
 Triggers unsatisfied, max unc./thresh. is 1.01363 for flux in tally 53
 The estimated number of batches is 82
       81/1    1.24238    1.22382 +/- 0.00185
 Triggers unsatisfied, max unc./thresh. is 1.01172 for flux in tally 53
 The estimated number of batches is 83
       82/1    1.23493    1.22397 +/- 0.00183
 Triggers satisfied for batch 82
 Creating state point statepoint.082.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 9.5644e-01 seconds
   Reading cross sections          = 9.0579e-01 seconds
 Total time in simulation          = 9.9887e+01 seconds
   Time in transport only          = 9.9333e+01 seconds
   Time in inactive batches        = 5.4841e+00 seconds
   Time in active batches          = 9.4403e+01 seconds
   Time synchronizing fission bank = 7.3998e-02 seconds
     Sampling source sites         = 5.9021e-02 seconds
     SEND/RECV source sites        = 1.4787e-02 seconds
   Time accumulating tallies       = 1.2234e-03 seconds
 Total time for finalization       = 2.8416e-02 seconds
 Total time elapsed                = 1.0094e+02 seconds
 Calculation Rate (inactive)       = 18234.5 particles/second
 Calculation Rate (active)         = 7626.89 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.22348 +/- 0.00169
 k-effective (Track-length)  = 1.22397 +/- 0.00183
 k-effective (Absorption)    = 1.22467 +/- 0.00117
 Combined k-effective        = 1.22448 +/- 0.00108
 Leakage Fraction            = 0.00000 +/- 0.00000

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

[13]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.082.h5')

The statepoint is now ready to be analyzed by our multi-group cross sections. We simply have to load the tallies from the StatePoint into each object as follows and our MGXS objects will compute the cross sections for us under-the-hood.

[14]:
# Iterate over all cells and cross section types
for cell in openmc_cells:
    for rxn_type in xs_library[cell.id]:
        xs_library[cell.id][rxn_type].load_from_statepoint(sp)

That’s it! Our multi-group cross sections are now ready for the big spotlight. This time we have cross sections in three distinct spatial zones - fuel, clad and moderator - on a per-nuclide basis.

Extracting and Storing MGXS Data

Let’s first inspect one of our cross sections by printing it to the screen as a microscopic cross section in units of barns.

[15]:
nufission = xs_library[fuel_cell.id]['nu-fission']
nufission.print_xs(xs_type='micro', nuclides=['U235', 'U238'])
Multi-Group XS
        Reaction Type  =        nu-fission
        Domain Type    =        cell
        Domain ID      =        1
        Nuclide        =        U235
        Cross Sections [barns]:
            Group 1 [821000.0   - 20000000.0eV]:        3.30e+00 +/- 2.14e-01%
            Group 2 [5530.0     - 821000.0  eV]:        3.96e+00 +/- 1.33e-01%
            Group 3 [4.0        - 5530.0    eV]:        5.50e+01 +/- 2.29e-01%
            Group 4 [0.625      - 4.0       eV]:        8.85e+01 +/- 3.10e-01%
            Group 5 [0.28       - 0.625     eV]:        2.90e+02 +/- 3.94e-01%
            Group 6 [0.14       - 0.28      eV]:        4.49e+02 +/- 4.12e-01%
            Group 7 [0.058      - 0.14      eV]:        6.87e+02 +/- 3.01e-01%
            Group 8 [0.0        - 0.058     eV]:        1.44e+03 +/- 2.79e-01%

        Nuclide        =        U238
        Cross Sections [barns]:
            Group 1 [821000.0   - 20000000.0eV]:        1.06e+00 +/- 2.53e-01%
            Group 2 [5530.0     - 821000.0  eV]:        1.21e-03 +/- 2.60e-01%
            Group 3 [4.0        - 5530.0    eV]:        5.73e-04 +/- 2.93e+00%
            Group 4 [0.625      - 4.0       eV]:        6.54e-06 +/- 2.72e-01%
            Group 5 [0.28       - 0.625     eV]:        1.07e-05 +/- 3.83e-01%
            Group 6 [0.14       - 0.28      eV]:        1.55e-05 +/- 4.13e-01%
            Group 7 [0.058      - 0.14      eV]:        2.30e-05 +/- 3.01e-01%
            Group 8 [0.0        - 0.058     eV]:        4.24e-05 +/- 2.79e-01%



Our multi-group cross sections are capable of summing across all nuclides to provide us with macroscopic cross sections as well.

[16]:
nufission = xs_library[fuel_cell.id]['nu-fission']
nufission.print_xs(xs_type='macro', nuclides='sum')
Multi-Group XS
        Reaction Type  =        nu-fission
        Domain Type    =        cell
        Domain ID      =        1
        Cross Sections [cm^-1]:
            Group 1 [821000.0   - 20000000.0eV]:        2.52e-02 +/- 2.41e-01%
            Group 2 [5530.0     - 821000.0  eV]:        1.51e-03 +/- 1.31e-01%
            Group 3 [4.0        - 5530.0    eV]:        2.07e-02 +/- 2.29e-01%
            Group 4 [0.625      - 4.0       eV]:        3.32e-02 +/- 3.10e-01%
            Group 5 [0.28       - 0.625     eV]:        1.09e-01 +/- 3.94e-01%
            Group 6 [0.14       - 0.28      eV]:        1.69e-01 +/- 4.12e-01%
            Group 7 [0.058      - 0.14      eV]:        2.58e-01 +/- 3.01e-01%
            Group 8 [0.0        - 0.058     eV]:        5.40e-01 +/- 2.79e-01%



Although a printed report is nice, it is not scalable or flexible. Let’s extract the microscopic cross section data for the moderator as a Pandas DataFrame .

[17]:
nuscatter = xs_library[moderator_cell.id]['nu-scatter']
df = nuscatter.get_pandas_dataframe(xs_type='micro')
df.head(10)
[17]:
cell group in group out nuclide mean std. dev.
126 3 1 1 H1 0.233991 0.003752
127 3 1 1 O16 1.569288 0.006360
124 3 1 2 H1 1.587279 0.003098
125 3 1 2 O16 0.285599 0.001422
122 3 1 3 H1 0.010482 0.000220
123 3 1 3 O16 0.000000 0.000000
120 3 1 4 H1 0.000009 0.000006
121 3 1 4 O16 0.000000 0.000000
118 3 1 5 H1 0.000005 0.000005
119 3 1 5 O16 0.000000 0.000000

Next, we illustate how one can easily take multi-group cross sections and condense them down to a coarser energy group structure. The MGXS class includes a get_condensed_xs(...) method which takes an EnergyGroups parameter with a coarse(r) group structure and returns a new MGXS condensed to the coarse groups. We illustrate this process below using the 2-group structure created earlier.

[18]:
# Extract the 8-group transport cross section for the fuel
fine_xs = xs_library[fuel_cell.id]['transport']

# Condense to the 2-group structure
condensed_xs = fine_xs.get_condensed_xs(coarse_groups)

Group condensation is as simple as that! We now have a new coarse 2-group TransportXS in addition to our original 8-group TransportXS. Let’s inspect the 2-group TransportXS by printing it to the screen and extracting a Pandas DataFrame as we have already learned how to do.

[19]:
condensed_xs.print_xs()
Multi-Group XS
        Reaction Type  =        transport
        Domain Type    =        cell
        Domain ID      =        1
        Nuclide        =        U235
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        7.79e-03 +/- 2.12e-01%
            Group 2 [0.0        - 0.625     eV]:        1.82e-01 +/- 1.92e-01%

        Nuclide        =        U238
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        2.17e-01 +/- 1.12e-01%
            Group 2 [0.0        - 0.625     eV]:        2.53e-01 +/- 1.89e-01%

        Nuclide        =        O16
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        1.45e-01 +/- 1.12e-01%
            Group 2 [0.0        - 0.625     eV]:        1.74e-01 +/- 2.03e-01%



[20]:
df = condensed_xs.get_pandas_dataframe(xs_type='micro')
df
[20]:
cell group in nuclide mean std. dev.
3 1 1 U235 20.763062 0.044093
4 1 1 U238 9.579086 0.010757
5 1 1 O16 3.157274 0.003531
0 1 2 U235 485.349036 0.930937
1 1 2 U238 11.199167 0.021167
2 1 2 O16 3.788383 0.007676
Verification with OpenMOC

Now, let’s verify our cross sections using OpenMOC. First, we construct an equivalent OpenMOC geometry.

[21]:
# Create an OpenMOC Geometry from the OpenMC Geometry
openmoc_geometry = get_openmoc_geometry(sp.summary.geometry)

Next, we we can inject the multi-group cross sections into the equivalent fuel pin cell OpenMOC geometry.

[22]:
# Get all OpenMOC cells in the gometry
openmoc_cells = openmoc_geometry.getRootUniverse().getAllCells()

# Inject multi-group cross sections into OpenMOC Materials
for cell_id, cell in openmoc_cells.items():

    # Ignore the root cell
    if cell.getName() == 'root cell':
        continue

    # Get a reference to the Material filling this Cell
    openmoc_material = cell.getFillMaterial()

    # Set the number of energy groups for the Material
    openmoc_material.setNumEnergyGroups(fine_groups.num_groups)

    # Extract the appropriate cross section objects for this cell
    transport = xs_library[cell_id]['transport']
    nufission = xs_library[cell_id]['nu-fission']
    nuscatter = xs_library[cell_id]['nu-scatter']
    chi = xs_library[cell_id]['chi']

    # Inject NumPy arrays of cross section data into the Material
    # NOTE: Sum across nuclides to get macro cross sections needed by OpenMOC
    openmoc_material.setSigmaT(transport.get_xs(nuclides='sum').flatten())
    openmoc_material.setNuSigmaF(nufission.get_xs(nuclides='sum').flatten())
    openmoc_material.setSigmaS(nuscatter.get_xs(nuclides='sum').flatten())
    openmoc_material.setChi(chi.get_xs(nuclides='sum').flatten())

We are now ready to run OpenMOC to verify our cross-sections from OpenMC.

[23]:
# Generate tracks for OpenMOC
track_generator = openmoc.TrackGenerator(openmoc_geometry, num_azim=128, azim_spacing=0.1)
track_generator.generateTracks()

# Run OpenMOC
solver = openmoc.CPUSolver(track_generator)
solver.computeEigenvalue()
[  NORMAL ]  Initializing a default angular quadrature...
[  NORMAL ]  Initializing 2D tracks...
[  NORMAL ]  Initializing 2D tracks reflections...
[  NORMAL ]  Initializing 2D tracks array...
[  NORMAL ]  Ray tracing for 2D track segmentation...
[  NORMAL ]  Progress Segmenting 2D tracks: 0.09 %
[  NORMAL ]  Progress Segmenting 2D tracks: 10.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 19.94 %
[  NORMAL ]  Progress Segmenting 2D tracks: 29.87 %
[  NORMAL ]  Progress Segmenting 2D tracks: 39.80 %
[  NORMAL ]  Progress Segmenting 2D tracks: 49.72 %
[  NORMAL ]  Progress Segmenting 2D tracks: 59.65 %
[  NORMAL ]  Progress Segmenting 2D tracks: 69.58 %
[  NORMAL ]  Progress Segmenting 2D tracks: 79.50 %
[  NORMAL ]  Progress Segmenting 2D tracks: 89.43 %
[  NORMAL ]  Progress Segmenting 2D tracks: 100.00 %
[  NORMAL ]  Initializing FSR lookup vectors
[  NORMAL ]  Total number of FSRs 3
[  RESULT ]  Total Track Generation & Segmentation Time...........2.5566E-02 sec
[  NORMAL ]  Initializing MOC eigenvalue solver...
[  NORMAL ]  Initializing solver arrays...
[  NORMAL ]  Centering segments around FSR centroid...
[  NORMAL ]  Max boundary angular flux storage per domain =   0.42 MB
[  NORMAL ]  Max scalar flux storage per domain =   0.00 MB
[  NORMAL ]  Max source storage per domain =   0.00 MB
[  NORMAL ]  Number of azimuthal angles = 128
[  NORMAL ]  Azimuthal ray spacing = 0.100000
[  NORMAL ]  Number of polar angles = 6
[  NORMAL ]  Source type = Flat
[  NORMAL ]  MOC transport undamped
[  NORMAL ]  CMFD acceleration: OFF
[  NORMAL ]  Using 1 threads
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:  k_eff = 0.423133   res = 5.671E-09  delta-k (pcm) =
[  NORMAL ]  ...  -57686 D.R. = 0.00
[  NORMAL ]  Iteration 1:  k_eff = 0.475953   res = 2.442E-08  delta-k (pcm) =
[  NORMAL ]  ...  5282 D.R. = 4.31
[  NORMAL ]  Iteration 2:  k_eff = 0.491468   res = 4.764E-08  delta-k (pcm) =
[  NORMAL ]  ...  1551 D.R. = 1.95
[  NORMAL ]  Iteration 3:  k_eff = 0.487446   res = 2.253E-08  delta-k (pcm) =
[  NORMAL ]  ...  -402 D.R. = 0.47
[  NORMAL ]  Iteration 4:  k_eff = 0.483930   res = 6.957E-09  delta-k (pcm) =
[  NORMAL ]  ...  -351 D.R. = 0.31
[  NORMAL ]  Iteration 5:  k_eff = 0.477280   res = 3.902E-08  delta-k (pcm) =
[  NORMAL ]  ...  -665 D.R. = 5.61
[  NORMAL ]  Iteration 6:  k_eff = 0.468938   res = 3.161E-08  delta-k (pcm) =
[  NORMAL ]  ...  -834 D.R. = 0.81
[  NORMAL ]  Iteration 7:  k_eff = 0.460319   res = 2.480E-08  delta-k (pcm) =
[  NORMAL ]  ...  -861 D.R. = 0.78
[  NORMAL ]  Iteration 8:  k_eff = 0.450591   res = 9.377E-09  delta-k (pcm) =
[  NORMAL ]  ...  -972 D.R. = 0.38
[  NORMAL ]  Iteration 9:  k_eff = 0.441377   res = 3.085E-08  delta-k (pcm) =
[  NORMAL ]  ...  -921 D.R. = 3.29
[  NORMAL ]  Iteration 10:  k_eff = 0.431990   res = 1.028E-08  delta-k (pcm) =
[  NORMAL ]  ...  -938 D.R. = 0.33
[  NORMAL ]  Iteration 11:  k_eff = 0.422932   res = 1.180E-08  delta-k (pcm) =
[  NORMAL ]  ...  -905 D.R. = 1.15
[  NORMAL ]  Iteration 12:  k_eff = 0.414487   res = 1.633E-08  delta-k (pcm) =
[  NORMAL ]  ...  -844 D.R. = 1.38
[  NORMAL ]  Iteration 13:  k_eff = 0.406708   res = 1.754E-08  delta-k (pcm) =
[  NORMAL ]  ...  -777 D.R. = 1.07
[  NORMAL ]  Iteration 14:  k_eff = 0.399378   res = 5.021E-08  delta-k (pcm) =
[  NORMAL ]  ...  -732 D.R. = 2.86
[  NORMAL ]  Iteration 15:  k_eff = 0.393067   res = 9.074E-09  delta-k (pcm) =
[  NORMAL ]  ...  -631 D.R. = 0.18
[  NORMAL ]  Iteration 16:  k_eff = 0.387427   res = 4.840E-09  delta-k (pcm) =
[  NORMAL ]  ...  -564 D.R. = 0.53
[  NORMAL ]  Iteration 17:  k_eff = 0.382668   res = 2.299E-08  delta-k (pcm) =
[  NORMAL ]  ...  -475 D.R. = 4.75
[  NORMAL ]  Iteration 18:  k_eff = 0.378741   res = 1.573E-08  delta-k (pcm) =
[  NORMAL ]  ...  -392 D.R. = 0.68
[  NORMAL ]  Iteration 19:  k_eff = 0.375642   res = 7.017E-08  delta-k (pcm) =
[  NORMAL ]  ...  -309 D.R. = 4.46
[  NORMAL ]  Iteration 20:  k_eff = 0.373489   res = 4.053E-08  delta-k (pcm) =
[  NORMAL ]  ...  -215 D.R. = 0.58
[  NORMAL ]  Iteration 21:  k_eff = 0.372357   res = 4.235E-08  delta-k (pcm) =
[  NORMAL ]  ...  -113 D.R. = 1.04
[  NORMAL ]  Iteration 22:  k_eff = 0.371974   res = 6.352E-08  delta-k (pcm) =
[  NORMAL ]  ...  -38 D.R. = 1.50
[  NORMAL ]  Iteration 23:  k_eff = 0.372581   res = 3.267E-08  delta-k (pcm) =
[  NORMAL ]  ...  60 D.R. = 0.51
[  NORMAL ]  Iteration 24:  k_eff = 0.374056   res = 1.573E-08  delta-k (pcm) =
[  NORMAL ]  ...  147 D.R. = 0.48
[  NORMAL ]  Iteration 25:  k_eff = 0.376384   res = 3.630E-08  delta-k (pcm) =
[  NORMAL ]  ...  232 D.R. = 2.31
[  NORMAL ]  Iteration 26:  k_eff = 0.379563   res = 2.420E-09  delta-k (pcm) =
[  NORMAL ]  ...  317 D.R. = 0.07
[  NORMAL ]  Iteration 27:  k_eff = 0.383583   res = 3.146E-08  delta-k (pcm) =
[  NORMAL ]  ...  401 D.R. = 13.00
[  NORMAL ]  Iteration 28:  k_eff = 0.388380   res = 1.089E-08  delta-k (pcm) =
[  NORMAL ]  ...  479 D.R. = 0.35
[  NORMAL ]  Iteration 29:  k_eff = 0.393938   res = 6.049E-08  delta-k (pcm) =
[  NORMAL ]  ...  555 D.R. = 5.56
[  NORMAL ]  Iteration 30:  k_eff = 0.400234   res = 3.267E-08  delta-k (pcm) =
[  NORMAL ]  ...  629 D.R. = 0.54
[  NORMAL ]  Iteration 31:  k_eff = 0.407235   res = 2.420E-08  delta-k (pcm) =
[  NORMAL ]  ...  700 D.R. = 0.74
[  NORMAL ]  Iteration 32:  k_eff = 0.414884   res = 1.815E-08  delta-k (pcm) =
[  NORMAL ]  ...  764 D.R. = 0.75
[  NORMAL ]  Iteration 33:  k_eff = 0.423172   res = 7.259E-09  delta-k (pcm) =
[  NORMAL ]  ...  828 D.R. = 0.40
[  NORMAL ]  Iteration 34:  k_eff = 0.432051   res = 6.049E-08  delta-k (pcm) =
[  NORMAL ]  ...  887 D.R. = 8.33
[  NORMAL ]  Iteration 35:  k_eff = 0.441471   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  942 D.R. = 0.96
[  NORMAL ]  Iteration 36:  k_eff = 0.451430   res = 2.662E-08  delta-k (pcm) =
[  NORMAL ]  ...  995 D.R. = 0.46
[  NORMAL ]  Iteration 37:  k_eff = 0.461853   res = 8.227E-08  delta-k (pcm) =
[  NORMAL ]  ...  1042 D.R. = 3.09
[  NORMAL ]  Iteration 38:  k_eff = 0.472730   res = 5.928E-08  delta-k (pcm) =
[  NORMAL ]  ...  1087 D.R. = 0.72
[  NORMAL ]  Iteration 39:  k_eff = 0.484006   res = 2.299E-08  delta-k (pcm) =
[  NORMAL ]  ...  1127 D.R. = 0.39
[  NORMAL ]  Iteration 40:  k_eff = 0.495653   res = 2.299E-08  delta-k (pcm) =
[  NORMAL ]  ...  1164 D.R. = 1.00
[  NORMAL ]  Iteration 41:  k_eff = 0.507634   res = 7.017E-08  delta-k (pcm) =
[  NORMAL ]  ...  1198 D.R. = 3.05
[  NORMAL ]  Iteration 42:  k_eff = 0.519914   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  1227 D.R. = 0.28
[  NORMAL ]  Iteration 43:  k_eff = 0.532458   res = 1.089E-08  delta-k (pcm) =
[  NORMAL ]  ...  1254 D.R. = 0.56
[  NORMAL ]  Iteration 44:  k_eff = 0.545234   res = 6.291E-08  delta-k (pcm) =
[  NORMAL ]  ...  1277 D.R. = 5.78
[  NORMAL ]  Iteration 45:  k_eff = 0.558210   res = 3.509E-08  delta-k (pcm) =
[  NORMAL ]  ...  1297 D.R. = 0.56
[  NORMAL ]  Iteration 46:  k_eff = 0.571353   res = 3.025E-08  delta-k (pcm) =
[  NORMAL ]  ...  1314 D.R. = 0.86
[  NORMAL ]  Iteration 47:  k_eff = 0.584635   res = 7.259E-09  delta-k (pcm) =
[  NORMAL ]  ...  1328 D.R. = 0.24
[  NORMAL ]  Iteration 48:  k_eff = 0.598027   res = 4.961E-08  delta-k (pcm) =
[  NORMAL ]  ...  1339 D.R. = 6.83
[  NORMAL ]  Iteration 49:  k_eff = 0.611500   res = 9.014E-08  delta-k (pcm) =
[  NORMAL ]  ...  1347 D.R. = 1.82
[  NORMAL ]  Iteration 50:  k_eff = 0.625029   res = 5.203E-08  delta-k (pcm) =
[  NORMAL ]  ...  1352 D.R. = 0.58
[  NORMAL ]  Iteration 51:  k_eff = 0.638590   res = 1.512E-08  delta-k (pcm) =
[  NORMAL ]  ...  1356 D.R. = 0.29
[  NORMAL ]  Iteration 52:  k_eff = 0.652158   res = 2.359E-08  delta-k (pcm) =
[  NORMAL ]  ...  1356 D.R. = 1.56
[  NORMAL ]  Iteration 53:  k_eff = 0.665710   res = 4.598E-08  delta-k (pcm) =
[  NORMAL ]  ...  1355 D.R. = 1.95
[  NORMAL ]  Iteration 54:  k_eff = 0.679228   res = 2.783E-08  delta-k (pcm) =
[  NORMAL ]  ...  1351 D.R. = 0.61
[  NORMAL ]  Iteration 55:  k_eff = 0.692689   res = 2.117E-08  delta-k (pcm) =
[  NORMAL ]  ...  1346 D.R. = 0.76
[  NORMAL ]  Iteration 56:  k_eff = 0.706075   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  1338 D.R. = 0.91
[  NORMAL ]  Iteration 57:  k_eff = 0.719370   res = 5.505E-08  delta-k (pcm) =
[  NORMAL ]  ...  1329 D.R. = 2.84
[  NORMAL ]  Iteration 58:  k_eff = 0.732556   res = 2.238E-08  delta-k (pcm) =
[  NORMAL ]  ...  1318 D.R. = 0.41
[  NORMAL ]  Iteration 59:  k_eff = 0.745619   res = 7.864E-09  delta-k (pcm) =
[  NORMAL ]  ...  1306 D.R. = 0.35
[  NORMAL ]  Iteration 60:  k_eff = 0.758546   res = 4.477E-08  delta-k (pcm) =
[  NORMAL ]  ...  1292 D.R. = 5.69
[  NORMAL ]  Iteration 61:  k_eff = 0.771323   res = 4.658E-08  delta-k (pcm) =
[  NORMAL ]  ...  1277 D.R. = 1.04
[  NORMAL ]  Iteration 62:  k_eff = 0.783939   res = 9.679E-09  delta-k (pcm) =
[  NORMAL ]  ...  1261 D.R. = 0.21
[  NORMAL ]  Iteration 63:  k_eff = 0.796383   res = 4.235E-09  delta-k (pcm) =
[  NORMAL ]  ...  1244 D.R. = 0.44
[  NORMAL ]  Iteration 64:  k_eff = 0.808646   res = 9.074E-09  delta-k (pcm) =
[  NORMAL ]  ...  1226 D.R. = 2.14
[  NORMAL ]  Iteration 65:  k_eff = 0.820719   res = 6.412E-08  delta-k (pcm) =
[  NORMAL ]  ...  1207 D.R. = 7.07
[  NORMAL ]  Iteration 66:  k_eff = 0.832594   res = 2.420E-09  delta-k (pcm) =
[  NORMAL ]  ...  1187 D.R. = 0.04
[  NORMAL ]  Iteration 67:  k_eff = 0.844264   res = 2.299E-08  delta-k (pcm) =
[  NORMAL ]  ...  1167 D.R. = 9.50
[  NORMAL ]  Iteration 68:  k_eff = 0.855724   res = 5.928E-08  delta-k (pcm) =
[  NORMAL ]  ...  1146 D.R. = 2.58
[  NORMAL ]  Iteration 69:  k_eff = 0.866968   res = 6.049E-08  delta-k (pcm) =
[  NORMAL ]  ...  1124 D.R. = 1.02
[  NORMAL ]  Iteration 70:  k_eff = 0.877992   res = 2.722E-08  delta-k (pcm) =
[  NORMAL ]  ...  1102 D.R. = 0.45
[  NORMAL ]  Iteration 71:  k_eff = 0.888792   res = 1.633E-08  delta-k (pcm) =
[  NORMAL ]  ...  1079 D.R. = 0.60
[  NORMAL ]  Iteration 72:  k_eff = 0.899364   res = 2.117E-08  delta-k (pcm) =
[  NORMAL ]  ...  1057 D.R. = 1.30
[  NORMAL ]  Iteration 73:  k_eff = 0.909708   res = 3.327E-08  delta-k (pcm) =
[  NORMAL ]  ...  1034 D.R. = 1.57
[  NORMAL ]  Iteration 74:  k_eff = 0.919819   res = 4.477E-08  delta-k (pcm) =
[  NORMAL ]  ...  1011 D.R. = 1.35
[  NORMAL ]  Iteration 75:  k_eff = 0.929699   res = 3.569E-08  delta-k (pcm) =
[  NORMAL ]  ...  987 D.R. = 0.80
[  NORMAL ]  Iteration 76:  k_eff = 0.939346   res = 4.840E-09  delta-k (pcm) =
[  NORMAL ]  ...  964 D.R. = 0.14
[  NORMAL ]  Iteration 77:  k_eff = 0.948758   res = 4.840E-09  delta-k (pcm) =
[  NORMAL ]  ...  941 D.R. = 1.00
[  NORMAL ]  Iteration 78:  k_eff = 0.957938   res = 6.049E-10  delta-k (pcm) =
[  NORMAL ]  ...  917 D.R. = 0.12
[  NORMAL ]  Iteration 79:  k_eff = 0.966885   res = 2.057E-08  delta-k (pcm) =
[  NORMAL ]  ...  894 D.R. = 34.00
[  NORMAL ]  Iteration 80:  k_eff = 0.975601   res = 4.235E-08  delta-k (pcm) =
[  NORMAL ]  ...  871 D.R. = 2.06
[  NORMAL ]  Iteration 81:  k_eff = 0.984087   res = 5.868E-08  delta-k (pcm) =
[  NORMAL ]  ...  848 D.R. = 1.39
[  NORMAL ]  Iteration 82:  k_eff = 0.992344   res = 7.259E-09  delta-k (pcm) =
[  NORMAL ]  ...  825 D.R. = 0.12
[  NORMAL ]  Iteration 83:  k_eff = 1.000375   res = 1.512E-08  delta-k (pcm) =
[  NORMAL ]  ...  803 D.R. = 2.08
[  NORMAL ]  Iteration 84:  k_eff = 1.008182   res = 7.864E-09  delta-k (pcm) =
[  NORMAL ]  ...  780 D.R. = 0.52
[  NORMAL ]  Iteration 85:  k_eff = 1.015768   res = 7.259E-09  delta-k (pcm) =
[  NORMAL ]  ...  758 D.R. = 0.92
[  NORMAL ]  Iteration 86:  k_eff = 1.023136   res = 3.025E-09  delta-k (pcm) =
[  NORMAL ]  ...  736 D.R. = 0.42
[  NORMAL ]  Iteration 87:  k_eff = 1.030288   res = 1.210E-08  delta-k (pcm) =
[  NORMAL ]  ...  715 D.R. = 4.00
[  NORMAL ]  Iteration 88:  k_eff = 1.037228   res = 3.690E-08  delta-k (pcm) =
[  NORMAL ]  ...  693 D.R. = 3.05
[  NORMAL ]  Iteration 89:  k_eff = 1.043960   res = 5.203E-08  delta-k (pcm) =
[  NORMAL ]  ...  673 D.R. = 1.41
[  NORMAL ]  Iteration 90:  k_eff = 1.050486   res = 6.231E-08  delta-k (pcm) =
[  NORMAL ]  ...  652 D.R. = 1.20
[  NORMAL ]  Iteration 91:  k_eff = 1.056812   res = 6.775E-08  delta-k (pcm) =
[  NORMAL ]  ...  632 D.R. = 1.09
[  NORMAL ]  Iteration 92:  k_eff = 1.062939   res = 2.964E-08  delta-k (pcm) =
[  NORMAL ]  ...  612 D.R. = 0.44
[  NORMAL ]  Iteration 93:  k_eff = 1.068872   res = 5.505E-08  delta-k (pcm) =
[  NORMAL ]  ...  593 D.R. = 1.86
[  NORMAL ]  Iteration 94:  k_eff = 1.074616   res = 4.235E-09  delta-k (pcm) =
[  NORMAL ]  ...  574 D.R. = 0.08
[  NORMAL ]  Iteration 95:  k_eff = 1.080173   res = 2.541E-08  delta-k (pcm) =
[  NORMAL ]  ...  555 D.R. = 6.00
[  NORMAL ]  Iteration 96:  k_eff = 1.085550   res = 1.996E-08  delta-k (pcm) =
[  NORMAL ]  ...  537 D.R. = 0.79
[  NORMAL ]  Iteration 97:  k_eff = 1.090748   res = 3.388E-08  delta-k (pcm) =
[  NORMAL ]  ...  519 D.R. = 1.70
[  NORMAL ]  Iteration 98:  k_eff = 1.095774   res = 3.085E-08  delta-k (pcm) =
[  NORMAL ]  ...  502 D.R. = 0.91
[  NORMAL ]  Iteration 99:  k_eff = 1.100629   res = 3.267E-08  delta-k (pcm) =
[  NORMAL ]  ...  485 D.R. = 1.06
[  NORMAL ]  Iteration 100:  k_eff = 1.105320   res = 3.025E-09  delta-k (pcm)
[  NORMAL ]  ...  = 469 D.R. = 0.09
[  NORMAL ]  Iteration 101:  k_eff = 1.109851   res = 6.654E-09  delta-k (pcm)
[  NORMAL ]  ...  = 453 D.R. = 2.20
[  NORMAL ]  Iteration 102:  k_eff = 1.114224   res = 3.569E-08  delta-k (pcm)
[  NORMAL ]  ...  = 437 D.R. = 5.36
[  NORMAL ]  Iteration 103:  k_eff = 1.118444   res = 5.203E-08  delta-k (pcm)
[  NORMAL ]  ...  = 421 D.R. = 1.46
[  NORMAL ]  Iteration 104:  k_eff = 1.122516   res = 1.452E-08  delta-k (pcm)
[  NORMAL ]  ...  = 407 D.R. = 0.28
[  NORMAL ]  Iteration 105:  k_eff = 1.126445   res = 5.445E-09  delta-k (pcm)
[  NORMAL ]  ...  = 392 D.R. = 0.38
[  NORMAL ]  Iteration 106:  k_eff = 1.130232   res = 2.783E-08  delta-k (pcm)
[  NORMAL ]  ...  = 378 D.R. = 5.11
[  NORMAL ]  Iteration 107:  k_eff = 1.133884   res = 1.996E-08  delta-k (pcm)
[  NORMAL ]  ...  = 365 D.R. = 0.72
[  NORMAL ]  Iteration 108:  k_eff = 1.137403   res = 4.235E-08  delta-k (pcm)
[  NORMAL ]  ...  = 351 D.R. = 2.12
[  NORMAL ]  Iteration 109:  k_eff = 1.140793   res = 3.751E-08  delta-k (pcm)
[  NORMAL ]  ...  = 339 D.R. = 0.89
[  NORMAL ]  Iteration 110:  k_eff = 1.144059   res = 4.174E-08  delta-k (pcm)
[  NORMAL ]  ...  = 326 D.R. = 1.11
[  NORMAL ]  Iteration 111:  k_eff = 1.147203   res = 4.416E-08  delta-k (pcm)
[  NORMAL ]  ...  = 314 D.R. = 1.06
[  NORMAL ]  Iteration 112:  k_eff = 1.150231   res = 3.025E-08  delta-k (pcm)
[  NORMAL ]  ...  = 302 D.R. = 0.68
[  NORMAL ]  Iteration 113:  k_eff = 1.153146   res = 5.384E-08  delta-k (pcm)
[  NORMAL ]  ...  = 291 D.R. = 1.78
[  NORMAL ]  Iteration 114:  k_eff = 1.155950   res = 3.569E-08  delta-k (pcm)
[  NORMAL ]  ...  = 280 D.R. = 0.66
[  NORMAL ]  Iteration 115:  k_eff = 1.158649   res = 5.142E-08  delta-k (pcm)
[  NORMAL ]  ...  = 269 D.R. = 1.44
[  NORMAL ]  Iteration 116:  k_eff = 1.161244   res = 2.843E-08  delta-k (pcm)
[  NORMAL ]  ...  = 259 D.R. = 0.55
[  NORMAL ]  Iteration 117:  k_eff = 1.163739   res = 3.267E-08  delta-k (pcm)
[  NORMAL ]  ...  = 249 D.R. = 1.15
[  NORMAL ]  Iteration 118:  k_eff = 1.166139   res = 5.505E-08  delta-k (pcm)
[  NORMAL ]  ...  = 239 D.R. = 1.69
[  NORMAL ]  Iteration 119:  k_eff = 1.168445   res = 4.719E-08  delta-k (pcm)
[  NORMAL ]  ...  = 230 D.R. = 0.86
[  NORMAL ]  Iteration 120:  k_eff = 1.170662   res = 6.170E-08  delta-k (pcm)
[  NORMAL ]  ...  = 221 D.R. = 1.31
[  NORMAL ]  Iteration 121:  k_eff = 1.172791   res = 5.384E-08  delta-k (pcm)
[  NORMAL ]  ...  = 212 D.R. = 0.87
[  NORMAL ]  Iteration 122:  k_eff = 1.174837   res = 1.331E-08  delta-k (pcm)
[  NORMAL ]  ...  = 204 D.R. = 0.25
[  NORMAL ]  Iteration 123:  k_eff = 1.176801   res = 1.391E-08  delta-k (pcm)
[  NORMAL ]  ...  = 196 D.R. = 1.05
[  NORMAL ]  Iteration 124:  k_eff = 1.178688   res = 1.089E-08  delta-k (pcm)
[  NORMAL ]  ...  = 188 D.R. = 0.78
[  NORMAL ]  Iteration 125:  k_eff = 1.180500   res = 3.448E-08  delta-k (pcm)
[  NORMAL ]  ...  = 181 D.R. = 3.17
[  NORMAL ]  Iteration 126:  k_eff = 1.182238   res = 1.041E-07  delta-k (pcm)
[  NORMAL ]  ...  = 173 D.R. = 3.02
[  NORMAL ]  Iteration 127:  k_eff = 1.183907   res = 3.690E-08  delta-k (pcm)
[  NORMAL ]  ...  = 166 D.R. = 0.35
[  NORMAL ]  Iteration 128:  k_eff = 1.185508   res = 2.722E-08  delta-k (pcm)
[  NORMAL ]  ...  = 160 D.R. = 0.74
[  NORMAL ]  Iteration 129:  k_eff = 1.187044   res = 9.074E-09  delta-k (pcm)
[  NORMAL ]  ...  = 153 D.R. = 0.33
[  NORMAL ]  Iteration 130:  k_eff = 1.188518   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 147 D.R. = 3.20
[  NORMAL ]  Iteration 131:  k_eff = 1.189930   res = 2.783E-08  delta-k (pcm)
[  NORMAL ]  ...  = 141 D.R. = 0.96
[  NORMAL ]  Iteration 132:  k_eff = 1.191285   res = 2.541E-08  delta-k (pcm)
[  NORMAL ]  ...  = 135 D.R. = 0.91
[  NORMAL ]  Iteration 133:  k_eff = 1.192584   res = 4.537E-08  delta-k (pcm)
[  NORMAL ]  ...  = 129 D.R. = 1.79
[  NORMAL ]  Iteration 134:  k_eff = 1.193829   res = 5.505E-08  delta-k (pcm)
[  NORMAL ]  ...  = 124 D.R. = 1.21
[  NORMAL ]  Iteration 135:  k_eff = 1.195022   res = 6.412E-08  delta-k (pcm)
[  NORMAL ]  ...  = 119 D.R. = 1.16
[  NORMAL ]  Iteration 136:  k_eff = 1.196166   res = 6.049E-10  delta-k (pcm)
[  NORMAL ]  ...  = 114 D.R. = 0.01
[  NORMAL ]  Iteration 137:  k_eff = 1.197262   res = 4.416E-08  delta-k (pcm)
[  NORMAL ]  ...  = 109 D.R. = 73.00
[  NORMAL ]  Iteration 138:  k_eff = 1.198311   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 104 D.R. = 0.88
[  NORMAL ]  Iteration 139:  k_eff = 1.199316   res = 2.420E-09  delta-k (pcm)
[  NORMAL ]  ...  = 100 D.R. = 0.06
[  NORMAL ]  Iteration 140:  k_eff = 1.200279   res = 4.053E-08  delta-k (pcm)
[  NORMAL ]  ...  = 96 D.R. = 16.75
[  NORMAL ]  Iteration 141:  k_eff = 1.201201   res = 1.028E-08  delta-k (pcm)
[  NORMAL ]  ...  = 92 D.R. = 0.25
[  NORMAL ]  Iteration 142:  k_eff = 1.202083   res = 3.509E-08  delta-k (pcm)
[  NORMAL ]  ...  = 88 D.R. = 3.41
[  NORMAL ]  Iteration 143:  k_eff = 1.202927   res = 1.815E-08  delta-k (pcm)
[  NORMAL ]  ...  = 84 D.R. = 0.52
[  NORMAL ]  Iteration 144:  k_eff = 1.203736   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 80 D.R. = 1.07
[  NORMAL ]  Iteration 145:  k_eff = 1.204510   res = 6.836E-08  delta-k (pcm)
[  NORMAL ]  ...  = 77 D.R. = 3.53
[  NORMAL ]  Iteration 146:  k_eff = 1.205251   res = 5.324E-08  delta-k (pcm)
[  NORMAL ]  ...  = 74 D.R. = 0.78
[  NORMAL ]  Iteration 147:  k_eff = 1.205959   res = 2.299E-08  delta-k (pcm)
[  NORMAL ]  ...  = 70 D.R. = 0.43
[  NORMAL ]  Iteration 148:  k_eff = 1.206637   res = 1.815E-08  delta-k (pcm)
[  NORMAL ]  ...  = 67 D.R. = 0.79
[  NORMAL ]  Iteration 149:  k_eff = 1.207285   res = 8.469E-09  delta-k (pcm)
[  NORMAL ]  ...  = 64 D.R. = 0.47
[  NORMAL ]  Iteration 150:  k_eff = 1.207905   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 61 D.R. = 2.29
[  NORMAL ]  Iteration 151:  k_eff = 1.208498   res = 9.074E-09  delta-k (pcm)
[  NORMAL ]  ...  = 59 D.R. = 0.47
[  NORMAL ]  Iteration 152:  k_eff = 1.209065   res = 2.178E-08  delta-k (pcm)
[  NORMAL ]  ...  = 56 D.R. = 2.40
[  NORMAL ]  Iteration 153:  k_eff = 1.209607   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 54 D.R. = 3.11
[  NORMAL ]  Iteration 154:  k_eff = 1.210125   res = 9.074E-08  delta-k (pcm)
[  NORMAL ]  ...  = 51 D.R. = 1.34
[  NORMAL ]  Iteration 155:  k_eff = 1.210621   res = 5.445E-09  delta-k (pcm)
[  NORMAL ]  ...  = 49 D.R. = 0.06
[  NORMAL ]  Iteration 156:  k_eff = 1.211094   res = 6.049E-09  delta-k (pcm)
[  NORMAL ]  ...  = 47 D.R. = 1.11
[  NORMAL ]  Iteration 157:  k_eff = 1.211546   res = 6.049E-09  delta-k (pcm)
[  NORMAL ]  ...  = 45 D.R. = 1.00
[  NORMAL ]  Iteration 158:  k_eff = 1.211978   res = 4.658E-08  delta-k (pcm)
[  NORMAL ]  ...  = 43 D.R. = 7.70
[  NORMAL ]  Iteration 159:  k_eff = 1.212391   res = 1.815E-09  delta-k (pcm)
[  NORMAL ]  ...  = 41 D.R. = 0.04
[  NORMAL ]  Iteration 160:  k_eff = 1.212786   res = 1.210E-08  delta-k (pcm)
[  NORMAL ]  ...  = 39 D.R. = 6.67
[  NORMAL ]  Iteration 161:  k_eff = 1.213162   res = 6.049E-10  delta-k (pcm)
[  NORMAL ]  ...  = 37 D.R. = 0.05
[  NORMAL ]  Iteration 162:  k_eff = 1.213522   res = 1.996E-08  delta-k (pcm)
[  NORMAL ]  ...  = 35 D.R. = 33.00
[  NORMAL ]  Iteration 163:  k_eff = 1.213866   res = 4.658E-08  delta-k (pcm)
[  NORMAL ]  ...  = 34 D.R. = 2.33
[  NORMAL ]  Iteration 164:  k_eff = 1.214194   res = 1.996E-08  delta-k (pcm)
[  NORMAL ]  ...  = 32 D.R. = 0.43
[  NORMAL ]  Iteration 165:  k_eff = 1.214507   res = 1.210E-09  delta-k (pcm)
[  NORMAL ]  ...  = 31 D.R. = 0.06
[  NORMAL ]  Iteration 166:  k_eff = 1.214806   res = 1.875E-08  delta-k (pcm)
[  NORMAL ]  ...  = 29 D.R. = 15.50
[  NORMAL ]  Iteration 167:  k_eff = 1.215092   res = 4.961E-08  delta-k (pcm)
[  NORMAL ]  ...  = 28 D.R. = 2.65
[  NORMAL ]  Iteration 168:  k_eff = 1.215365   res = 6.049E-08  delta-k (pcm)
[  NORMAL ]  ...  = 27 D.R. = 1.22
[  NORMAL ]  Iteration 169:  k_eff = 1.215625   res = 2.964E-08  delta-k (pcm)
[  NORMAL ]  ...  = 26 D.R. = 0.49
[  NORMAL ]  Iteration 170:  k_eff = 1.215874   res = 5.505E-08  delta-k (pcm)
[  NORMAL ]  ...  = 24 D.R. = 1.86
[  NORMAL ]  Iteration 171:  k_eff = 1.216110   res = 2.117E-08  delta-k (pcm)
[  NORMAL ]  ...  = 23 D.R. = 0.38
[  NORMAL ]  Iteration 172:  k_eff = 1.216337   res = 4.174E-08  delta-k (pcm)
[  NORMAL ]  ...  = 22 D.R. = 1.97
[  NORMAL ]  Iteration 173:  k_eff = 1.216552   res = 3.509E-08  delta-k (pcm)
[  NORMAL ]  ...  = 21 D.R. = 0.84
[  NORMAL ]  Iteration 174:  k_eff = 1.216759   res = 2.722E-08  delta-k (pcm)
[  NORMAL ]  ...  = 20 D.R. = 0.78
[  NORMAL ]  Iteration 175:  k_eff = 1.216954   res = 2.601E-08  delta-k (pcm)
[  NORMAL ]  ...  = 19 D.R. = 0.96
[  NORMAL ]  Iteration 176:  k_eff = 1.217142   res = 4.295E-08  delta-k (pcm)
[  NORMAL ]  ...  = 18 D.R. = 1.65
[  NORMAL ]  Iteration 177:  k_eff = 1.217320   res = 4.598E-08  delta-k (pcm)
[  NORMAL ]  ...  = 17 D.R. = 1.07
[  NORMAL ]  Iteration 178:  k_eff = 1.217491   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 17 D.R. = 1.26
[  NORMAL ]  Iteration 179:  k_eff = 1.217654   res = 2.480E-08  delta-k (pcm)
[  NORMAL ]  ...  = 16 D.R. = 0.43
[  NORMAL ]  Iteration 180:  k_eff = 1.217809   res = 6.049E-09  delta-k (pcm)
[  NORMAL ]  ...  = 15 D.R. = 0.24
[  NORMAL ]  Iteration 181:  k_eff = 1.217956   res = 6.412E-08  delta-k (pcm)
[  NORMAL ]  ...  = 14 D.R. = 10.60
[  NORMAL ]  Iteration 182:  k_eff = 1.218098   res = 7.138E-08  delta-k (pcm)
[  NORMAL ]  ...  = 14 D.R. = 1.11
[  NORMAL ]  Iteration 183:  k_eff = 1.218232   res = 1.633E-08  delta-k (pcm)
[  NORMAL ]  ...  = 13 D.R. = 0.23
[  NORMAL ]  Iteration 184:  k_eff = 1.218360   res = 2.541E-08  delta-k (pcm)
[  NORMAL ]  ...  = 12 D.R. = 1.56
[  NORMAL ]  Iteration 185:  k_eff = 1.218482   res = 1.028E-08  delta-k (pcm)
[  NORMAL ]  ...  = 12 D.R. = 0.40
[  NORMAL ]  Iteration 186:  k_eff = 1.218599   res = 7.864E-09  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 0.76
[  NORMAL ]  Iteration 187:  k_eff = 1.218709   res = 4.053E-08  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 5.15
[  NORMAL ]  Iteration 188:  k_eff = 1.218815   res = 2.299E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 0.57
[  NORMAL ]  Iteration 189:  k_eff = 1.218916   res = 5.445E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 2.37
[  NORMAL ]  Iteration 190:  k_eff = 1.219011   res = 3.327E-08  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = 0.61
[  NORMAL ]  Iteration 191:  k_eff = 1.219103   res = 4.840E-09  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = 0.15
[  NORMAL ]  Iteration 192:  k_eff = 1.219190   res = 1.210E-08  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 2.50
[  NORMAL ]  Iteration 193:  k_eff = 1.219273   res = 1.573E-08  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 1.30
[  NORMAL ]  Iteration 194:  k_eff = 1.219352   res = 1.331E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 0.85
[  NORMAL ]  Iteration 195:  k_eff = 1.219428   res = 2.783E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 2.09
[  NORMAL ]  Iteration 196:  k_eff = 1.219499   res = 2.722E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 0.98
[  NORMAL ]  Iteration 197:  k_eff = 1.219567   res = 2.057E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.76
[  NORMAL ]  Iteration 198:  k_eff = 1.219633   res = 1.331E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.65
[  NORMAL ]  Iteration 199:  k_eff = 1.219695   res = 3.932E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 2.95
[  NORMAL ]  Iteration 200:  k_eff = 1.219753   res = 1.996E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.51
[  NORMAL ]  Iteration 201:  k_eff = 1.219810   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 2.42
[  NORMAL ]  Iteration 202:  k_eff = 1.219863   res = 1.270E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.26
[  NORMAL ]  Iteration 203:  k_eff = 1.219914   res = 2.662E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 2.10
[  NORMAL ]  Iteration 204:  k_eff = 1.219962   res = 5.445E-09  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.20
[  NORMAL ]  Iteration 205:  k_eff = 1.220009   res = 3.327E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 6.11
[  NORMAL ]  Iteration 206:  k_eff = 1.220052   res = 4.658E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 1.40
[  NORMAL ]  Iteration 207:  k_eff = 1.220094   res = 3.025E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.65
[  NORMAL ]  Iteration 208:  k_eff = 1.220134   res = 2.117E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.70
[  NORMAL ]  Iteration 209:  k_eff = 1.220172   res = 1.875E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.89
[  NORMAL ]  Iteration 210:  k_eff = 1.220208   res = 1.028E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.55
[  NORMAL ]  Iteration 211:  k_eff = 1.220243   res = 5.263E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 5.12
[  NORMAL ]  Iteration 212:  k_eff = 1.220275   res = 2.480E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.47
[  NORMAL ]  Iteration 213:  k_eff = 1.220306   res = 4.598E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 1.85
[  NORMAL ]  Iteration 214:  k_eff = 1.220336   res = 3.630E-09  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.08
[  NORMAL ]  Iteration 215:  k_eff = 1.220364   res = 1.391E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 3.83
[  NORMAL ]  Iteration 216:  k_eff = 1.220391   res = 6.049E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 4.35
[  NORMAL ]  Iteration 217:  k_eff = 1.220416   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.32
[  NORMAL ]  Iteration 218:  k_eff = 1.220441   res = 6.049E-10  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.03
[  NORMAL ]  Iteration 219:  k_eff = 1.220464   res = 1.270E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 21.00
[  NORMAL ]  Iteration 220:  k_eff = 1.220486   res = 1.452E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 1.14
[  NORMAL ]  Iteration 221:  k_eff = 1.220507   res = 2.299E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 1.58
[  NORMAL ]  Iteration 222:  k_eff = 1.220527   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.42
[  NORMAL ]  Iteration 223:  k_eff = 1.220545   res = 7.078E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 7.31
[  NORMAL ]  Iteration 224:  k_eff = 1.220563   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.14
[  NORMAL ]  Iteration 225:  k_eff = 1.220580   res = 3.146E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 3.25
[  NORMAL ]  Iteration 226:  k_eff = 1.220596   res = 1.633E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.52
[  NORMAL ]  Iteration 227:  k_eff = 1.220612   res = 3.569E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 2.19
[  NORMAL ]  Iteration 228:  k_eff = 1.220627   res = 2.722E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.76
[  NORMAL ]  Iteration 229:  k_eff = 1.220641   res = 1.875E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.69
[  NORMAL ]  Iteration 230:  k_eff = 1.220655   res = 3.448E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.84
[  NORMAL ]  Iteration 231:  k_eff = 1.220667   res = 8.046E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 2.33
[  NORMAL ]  Iteration 232:  k_eff = 1.220679   res = 3.751E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.47
[  NORMAL ]  Iteration 233:  k_eff = 1.220690   res = 5.686E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.52
[  NORMAL ]  Iteration 234:  k_eff = 1.220701   res = 1.270E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.22
[  NORMAL ]  Iteration 235:  k_eff = 1.220711   res = 6.715E-08  delta-k (pcm)
[  NORMAL ]  ...  = 0 D.R. = 5.29

We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.

[24]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined.n
bias = (openmoc_keff - openmc_keff) * 1e5

print('openmc keff = {0:1.6f}'.format(openmc_keff))
print('openmoc keff = {0:1.6f}'.format(openmoc_keff))
print('bias [pcm]: {0:1.1f}'.format(bias))
openmc keff = 1.224484
openmoc keff = 1.220711
bias [pcm]: -377.3

As a sanity check, let’s run a simulation with the coarse 2-group cross sections to ensure that they also produce a reasonable result.

[25]:
openmoc_geometry = get_openmoc_geometry(sp.summary.geometry)
openmoc_cells = openmoc_geometry.getRootUniverse().getAllCells()

# Inject multi-group cross sections into OpenMOC Materials
for cell_id, cell in openmoc_cells.items():

    # Ignore the root cell
    if cell.getName() == 'root cell':
        continue

    openmoc_material = cell.getFillMaterial()
    openmoc_material.setNumEnergyGroups(coarse_groups.num_groups)

    # Extract the appropriate cross section objects for this cell
    transport = xs_library[cell_id]['transport']
    nufission = xs_library[cell_id]['nu-fission']
    nuscatter = xs_library[cell_id]['nu-scatter']
    chi = xs_library[cell_id]['chi']

    # Perform group condensation
    transport = transport.get_condensed_xs(coarse_groups)
    nufission = nufission.get_condensed_xs(coarse_groups)
    nuscatter = nuscatter.get_condensed_xs(coarse_groups)
    chi = chi.get_condensed_xs(coarse_groups)

    # Inject NumPy arrays of cross section data into the Material
    openmoc_material.setSigmaT(transport.get_xs(nuclides='sum').flatten())
    openmoc_material.setNuSigmaF(nufission.get_xs(nuclides='sum').flatten())
    openmoc_material.setSigmaS(nuscatter.get_xs(nuclides='sum').flatten())
    openmoc_material.setChi(chi.get_xs(nuclides='sum').flatten())
[26]:
# Generate tracks for OpenMOC
track_generator = openmoc.TrackGenerator(openmoc_geometry, num_azim=128, azim_spacing=0.1)
track_generator.generateTracks()

# Run OpenMOC
solver = openmoc.CPUSolver(track_generator)
solver.computeEigenvalue()
[  NORMAL ]  Initializing a default angular quadrature...
[  NORMAL ]  Initializing 2D tracks...
[  NORMAL ]  Initializing 2D tracks reflections...
[  NORMAL ]  Initializing 2D tracks array...
[  NORMAL ]  Ray tracing for 2D track segmentation...
[  NORMAL ]  Progress Segmenting 2D tracks: 0.09 %
[  NORMAL ]  Progress Segmenting 2D tracks: 10.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 19.94 %
[  NORMAL ]  Progress Segmenting 2D tracks: 29.87 %
[  NORMAL ]  Progress Segmenting 2D tracks: 39.80 %
[  NORMAL ]  Progress Segmenting 2D tracks: 49.72 %
[  NORMAL ]  Progress Segmenting 2D tracks: 59.65 %
[  NORMAL ]  Progress Segmenting 2D tracks: 69.58 %
[  NORMAL ]  Progress Segmenting 2D tracks: 79.50 %
[  NORMAL ]  Progress Segmenting 2D tracks: 89.43 %
[  NORMAL ]  Progress Segmenting 2D tracks: 100.00 %
[  NORMAL ]  Initializing FSR lookup vectors
[  NORMAL ]  Total number of FSRs 3
[  RESULT ]  Total Track Generation & Segmentation Time...........3.9517E-02 sec
[  NORMAL ]  Initializing MOC eigenvalue solver...
[  NORMAL ]  Initializing solver arrays...
[  NORMAL ]  Centering segments around FSR centroid...
[  NORMAL ]  Max boundary angular flux storage per domain =   0.10 MB
[  NORMAL ]  Max scalar flux storage per domain =   0.00 MB
[  NORMAL ]  Max source storage per domain =   0.00 MB
[  NORMAL ]  Number of azimuthal angles = 128
[  NORMAL ]  Azimuthal ray spacing = 0.100000
[  NORMAL ]  Number of polar angles = 6
[  NORMAL ]  Source type = Flat
[  NORMAL ]  MOC transport undamped
[  NORMAL ]  CMFD acceleration: OFF
[  NORMAL ]  Using 1 threads
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:  k_eff = 0.366880   res = 4.840E-08  delta-k (pcm) =
[  NORMAL ]  ...  -63312 D.R. = 0.00
[  NORMAL ]  Iteration 1:  k_eff = 0.391184   res = 9.679E-09  delta-k (pcm) =
[  NORMAL ]  ...  2430 D.R. = 0.20
[  NORMAL ]  Iteration 2:  k_eff = 0.392990   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  180 D.R. = 6.00
[  NORMAL ]  Iteration 3:  k_eff = 0.381099   res = 9.195E-08  delta-k (pcm) =
[  NORMAL ]  ...  -1189 D.R. = 1.58
[  NORMAL ]  Iteration 4:  k_eff = 0.375018   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  -608 D.R. = 0.00
[  NORMAL ]  Iteration 5:  k_eff = 0.369593   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  -542 D.R. = inf
[  NORMAL ]  Iteration 6:  k_eff = 0.365543   res = 1.065E-07  delta-k (pcm) =
[  NORMAL ]  ...  -405 D.R. = 5.50
[  NORMAL ]  Iteration 7:  k_eff = 0.363054   res = 1.065E-07  delta-k (pcm) =
[  NORMAL ]  ...  -248 D.R. = 1.00
[  NORMAL ]  Iteration 8:  k_eff = 0.361473   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  -158 D.R. = 0.18
[  NORMAL ]  Iteration 9:  k_eff = 0.361280   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  -19 D.R. = 3.00
[  NORMAL ]  Iteration 10:  k_eff = 0.362003   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  72 D.R. = 0.33
[  NORMAL ]  Iteration 11:  k_eff = 0.363718   res = 4.840E-08  delta-k (pcm) =
[  NORMAL ]  ...  171 D.R. = 2.50
[  NORMAL ]  Iteration 12:  k_eff = 0.366338   res = 1.258E-07  delta-k (pcm) =
[  NORMAL ]  ...  262 D.R. = 2.60
[  NORMAL ]  Iteration 13:  k_eff = 0.369804   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  346 D.R. = 0.77
[  NORMAL ]  Iteration 14:  k_eff = 0.373989   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  418 D.R. = 0.40
[  NORMAL ]  Iteration 15:  k_eff = 0.378923   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  493 D.R. = 0.00
[  NORMAL ]  Iteration 16:  k_eff = 0.384479   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  555 D.R. = inf
[  NORMAL ]  Iteration 17:  k_eff = 0.390637   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  615 D.R. = 1.00
[  NORMAL ]  Iteration 18:  k_eff = 0.397338   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  670 D.R. = 0.00
[  NORMAL ]  Iteration 19:  k_eff = 0.404533   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  719 D.R. = inf
[  NORMAL ]  Iteration 20:  k_eff = 0.412184   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  765 D.R. = 1.67
[  NORMAL ]  Iteration 21:  k_eff = 0.420253   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  806 D.R. = 0.80
[  NORMAL ]  Iteration 22:  k_eff = 0.428686   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  843 D.R. = 0.75
[  NORMAL ]  Iteration 23:  k_eff = 0.437462   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  877 D.R. = 1.67
[  NORMAL ]  Iteration 24:  k_eff = 0.446538   res = 1.161E-07  delta-k (pcm) =
[  NORMAL ]  ...  907 D.R. = 1.20
[  NORMAL ]  Iteration 25:  k_eff = 0.455883   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  934 D.R. = 0.17
[  NORMAL ]  Iteration 26:  k_eff = 0.465469   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  958 D.R. = 1.00
[  NORMAL ]  Iteration 27:  k_eff = 0.475265   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  979 D.R. = 2.00
[  NORMAL ]  Iteration 28:  k_eff = 0.485246   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  998 D.R. = 1.00
[  NORMAL ]  Iteration 29:  k_eff = 0.495385   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  1013 D.R. = 0.50
[  NORMAL ]  Iteration 30:  k_eff = 0.505661   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  1027 D.R. = 4.00
[  NORMAL ]  Iteration 31:  k_eff = 0.516051   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1038 D.R. = 1.25
[  NORMAL ]  Iteration 32:  k_eff = 0.526534   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1048 D.R. = 1.00
[  NORMAL ]  Iteration 33:  k_eff = 0.537092   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1055 D.R. = 1.00
[  NORMAL ]  Iteration 34:  k_eff = 0.547706   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  1061 D.R. = 0.60
[  NORMAL ]  Iteration 35:  k_eff = 0.558361   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  1065 D.R. = 0.33
[  NORMAL ]  Iteration 36:  k_eff = 0.569040   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  1067 D.R. = 3.00
[  NORMAL ]  Iteration 37:  k_eff = 0.579730   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1068 D.R. = 1.67
[  NORMAL ]  Iteration 38:  k_eff = 0.590416   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  1068 D.R. = 0.00
[  NORMAL ]  Iteration 39:  k_eff = 0.601087   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1067 D.R. = inf
[  NORMAL ]  Iteration 40:  k_eff = 0.611731   res = 1.549E-07  delta-k (pcm) =
[  NORMAL ]  ...  1064 D.R. = 1.60
[  NORMAL ]  Iteration 41:  k_eff = 0.622338   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  1060 D.R. = 0.50
[  NORMAL ]  Iteration 42:  k_eff = 0.632897   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  1055 D.R. = 1.00
[  NORMAL ]  Iteration 43:  k_eff = 0.643400   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  1050 D.R. = 0.75
[  NORMAL ]  Iteration 44:  k_eff = 0.653837   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  1043 D.R. = 0.00
[  NORMAL ]  Iteration 45:  k_eff = 0.664203   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  1036 D.R. = inf
[  NORMAL ]  Iteration 46:  k_eff = 0.674488   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1028 D.R. = 1.25
[  NORMAL ]  Iteration 47:  k_eff = 0.684688   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  1020 D.R. = 1.00
[  NORMAL ]  Iteration 48:  k_eff = 0.694796   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  1010 D.R. = 0.40
[  NORMAL ]  Iteration 49:  k_eff = 0.704807   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  1001 D.R. = 1.00
[  NORMAL ]  Iteration 50:  k_eff = 0.714715   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  990 D.R. = 0.50
[  NORMAL ]  Iteration 51:  k_eff = 0.724517   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  980 D.R. = 1.00
[  NORMAL ]  Iteration 52:  k_eff = 0.734209   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  969 D.R. = 1.00
[  NORMAL ]  Iteration 53:  k_eff = 0.743787   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  957 D.R. = 0.00
[  NORMAL ]  Iteration 54:  k_eff = 0.753247   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  946 D.R. = inf
[  NORMAL ]  Iteration 55:  k_eff = 0.762588   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  934 D.R. = 1.50
[  NORMAL ]  Iteration 56:  k_eff = 0.771806   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  921 D.R. = 0.33
[  NORMAL ]  Iteration 57:  k_eff = 0.780901   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  909 D.R. = 2.00
[  NORMAL ]  Iteration 58:  k_eff = 0.789868   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  896 D.R. = 0.50
[  NORMAL ]  Iteration 59:  k_eff = 0.798708   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  884 D.R. = 1.00
[  NORMAL ]  Iteration 60:  k_eff = 0.807419   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  871 D.R. = 4.00
[  NORMAL ]  Iteration 61:  k_eff = 0.816000   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  858 D.R. = 0.50
[  NORMAL ]  Iteration 62:  k_eff = 0.824450   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  844 D.R. = 1.00
[  NORMAL ]  Iteration 63:  k_eff = 0.832768   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  831 D.R. = 2.00
[  NORMAL ]  Iteration 64:  k_eff = 0.840954   res = 1.742E-07  delta-k (pcm) =
[  NORMAL ]  ...  818 D.R. = 2.25
[  NORMAL ]  Iteration 65:  k_eff = 0.849008   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  805 D.R. = 0.44
[  NORMAL ]  Iteration 66:  k_eff = 0.856930   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  792 D.R. = 0.50
[  NORMAL ]  Iteration 67:  k_eff = 0.864720   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  778 D.R. = 1.00
[  NORMAL ]  Iteration 68:  k_eff = 0.872378   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  765 D.R. = 1.00
[  NORMAL ]  Iteration 69:  k_eff = 0.879905   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  752 D.R. = 0.50
[  NORMAL ]  Iteration 70:  k_eff = 0.887301   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  739 D.R. = 2.00
[  NORMAL ]  Iteration 71:  k_eff = 0.894566   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  726 D.R. = 1.50
[  NORMAL ]  Iteration 72:  k_eff = 0.901702   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  713 D.R. = 0.33
[  NORMAL ]  Iteration 73:  k_eff = 0.908710   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  700 D.R. = 2.00
[  NORMAL ]  Iteration 74:  k_eff = 0.915590   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  687 D.R. = 1.50
[  NORMAL ]  Iteration 75:  k_eff = 0.922342   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  675 D.R. = 0.00
[  NORMAL ]  Iteration 76:  k_eff = 0.928971   res = 1.161E-07  delta-k (pcm) =
[  NORMAL ]  ...  662 D.R. = inf
[  NORMAL ]  Iteration 77:  k_eff = 0.935474   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  650 D.R. = 0.17
[  NORMAL ]  Iteration 78:  k_eff = 0.941853   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  637 D.R. = 1.00
[  NORMAL ]  Iteration 79:  k_eff = 0.948112   res = 9.679E-08  delta-k (pcm) =
[  NORMAL ]  ...  625 D.R. = 5.00
[  NORMAL ]  Iteration 80:  k_eff = 0.954249   res = 1.065E-07  delta-k (pcm) =
[  NORMAL ]  ...  613 D.R. = 1.10
[  NORMAL ]  Iteration 81:  k_eff = 0.960267   res = 9.679E-09  delta-k (pcm) =
[  NORMAL ]  ...  601 D.R. = 0.09
[  NORMAL ]  Iteration 82:  k_eff = 0.966168   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  590 D.R. = 8.00
[  NORMAL ]  Iteration 83:  k_eff = 0.971953   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  578 D.R. = 0.75
[  NORMAL ]  Iteration 84:  k_eff = 0.977623   res = 1.936E-08  delta-k (pcm) =
[  NORMAL ]  ...  566 D.R. = 0.33
[  NORMAL ]  Iteration 85:  k_eff = 0.983179   res = 2.904E-08  delta-k (pcm) =
[  NORMAL ]  ...  555 D.R. = 1.50
[  NORMAL ]  Iteration 86:  k_eff = 0.988624   res = 0.000E+00  delta-k (pcm) =
[  NORMAL ]  ...  544 D.R. = 0.00
[  NORMAL ]  Iteration 87:  k_eff = 0.993958   res = 6.775E-08  delta-k (pcm) =
[  NORMAL ]  ...  533 D.R. = inf
[  NORMAL ]  Iteration 88:  k_eff = 0.999184   res = 9.679E-09  delta-k (pcm) =
[  NORMAL ]  ...  522 D.R. = 0.14
[  NORMAL ]  Iteration 89:  k_eff = 1.004304   res = 4.840E-08  delta-k (pcm) =
[  NORMAL ]  ...  511 D.R. = 5.00
[  NORMAL ]  Iteration 90:  k_eff = 1.009318   res = 3.872E-08  delta-k (pcm) =
[  NORMAL ]  ...  501 D.R. = 0.80
[  NORMAL ]  Iteration 91:  k_eff = 1.014228   res = 4.840E-08  delta-k (pcm) =
[  NORMAL ]  ...  491 D.R. = 1.25
[  NORMAL ]  Iteration 92:  k_eff = 1.019037   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  480 D.R. = 1.20
[  NORMAL ]  Iteration 93:  k_eff = 1.023744   res = 2.904E-08  delta-k (pcm) =
[  NORMAL ]  ...  470 D.R. = 0.50
[  NORMAL ]  Iteration 94:  k_eff = 1.028353   res = 6.775E-08  delta-k (pcm) =
[  NORMAL ]  ...  460 D.R. = 2.33
[  NORMAL ]  Iteration 95:  k_eff = 1.032865   res = 7.743E-08  delta-k (pcm) =
[  NORMAL ]  ...  451 D.R. = 1.14
[  NORMAL ]  Iteration 96:  k_eff = 1.037281   res = 1.355E-07  delta-k (pcm) =
[  NORMAL ]  ...  441 D.R. = 1.75
[  NORMAL ]  Iteration 97:  k_eff = 1.041604   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  432 D.R. = 0.43
[  NORMAL ]  Iteration 98:  k_eff = 1.045834   res = 4.840E-08  delta-k (pcm) =
[  NORMAL ]  ...  422 D.R. = 0.83
[  NORMAL ]  Iteration 99:  k_eff = 1.049974   res = 5.807E-08  delta-k (pcm) =
[  NORMAL ]  ...  413 D.R. = 1.20
[  NORMAL ]  Iteration 100:  k_eff = 1.054024   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 405 D.R. = 1.00
[  NORMAL ]  Iteration 101:  k_eff = 1.057987   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 396 D.R. = 0.83
[  NORMAL ]  Iteration 102:  k_eff = 1.061864   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 387 D.R. = 0.20
[  NORMAL ]  Iteration 103:  k_eff = 1.065657   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 379 D.R. = 3.00
[  NORMAL ]  Iteration 104:  k_eff = 1.069367   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 370 D.R. = 1.33
[  NORMAL ]  Iteration 105:  k_eff = 1.072996   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 362 D.R. = 1.00
[  NORMAL ]  Iteration 106:  k_eff = 1.076545   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 354 D.R. = 0.50
[  NORMAL ]  Iteration 107:  k_eff = 1.080016   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 347 D.R. = 3.50
[  NORMAL ]  Iteration 108:  k_eff = 1.083411   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 339 D.R. = 1.71
[  NORMAL ]  Iteration 109:  k_eff = 1.086731   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 331 D.R. = 0.50
[  NORMAL ]  Iteration 110:  k_eff = 1.089976   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 324 D.R. = 0.33
[  NORMAL ]  Iteration 111:  k_eff = 1.093150   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 317 D.R. = 4.00
[  NORMAL ]  Iteration 112:  k_eff = 1.096253   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 310 D.R. = 1.00
[  NORMAL ]  Iteration 113:  k_eff = 1.099286   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 303 D.R. = 0.50
[  NORMAL ]  Iteration 114:  k_eff = 1.102251   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 296 D.R. = 1.25
[  NORMAL ]  Iteration 115:  k_eff = 1.105150   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 289 D.R. = 0.20
[  NORMAL ]  Iteration 116:  k_eff = 1.107983   res = 1.452E-07  delta-k (pcm)
[  NORMAL ]  ...  = 283 D.R. = 15.00
[  NORMAL ]  Iteration 117:  k_eff = 1.110753   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 276 D.R. = 0.33
[  NORMAL ]  Iteration 118:  k_eff = 1.113459   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 270 D.R. = 1.80
[  NORMAL ]  Iteration 119:  k_eff = 1.116105   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 264 D.R. = 1.22
[  NORMAL ]  Iteration 120:  k_eff = 1.118690   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 258 D.R. = 0.36
[  NORMAL ]  Iteration 121:  k_eff = 1.121216   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 252 D.R. = 0.25
[  NORMAL ]  Iteration 122:  k_eff = 1.123685   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 246 D.R. = 1.00
[  NORMAL ]  Iteration 123:  k_eff = 1.126097   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 241 D.R. = 4.00
[  NORMAL ]  Iteration 124:  k_eff = 1.128454   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 235 D.R. = 0.00
[  NORMAL ]  Iteration 125:  k_eff = 1.130758   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 230 D.R. = inf
[  NORMAL ]  Iteration 126:  k_eff = 1.133008   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 224 D.R. = 5.50
[  NORMAL ]  Iteration 127:  k_eff = 1.135207   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 219 D.R. = 0.45
[  NORMAL ]  Iteration 128:  k_eff = 1.137354   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 214 D.R. = 0.40
[  NORMAL ]  Iteration 129:  k_eff = 1.139453   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 209 D.R. = 0.00
[  NORMAL ]  Iteration 130:  k_eff = 1.141503   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 204 D.R. = inf
[  NORMAL ]  Iteration 131:  k_eff = 1.143505   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 200 D.R. = 1.50
[  NORMAL ]  Iteration 132:  k_eff = 1.145461   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 195 D.R. = 2.00
[  NORMAL ]  Iteration 133:  k_eff = 1.147372   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 191 D.R. = 1.50
[  NORMAL ]  Iteration 134:  k_eff = 1.149238   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 186 D.R. = 1.22
[  NORMAL ]  Iteration 135:  k_eff = 1.151061   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 182 D.R. = 0.18
[  NORMAL ]  Iteration 136:  k_eff = 1.152842   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 178 D.R. = 1.00
[  NORMAL ]  Iteration 137:  k_eff = 1.154581   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 173 D.R. = 2.00
[  NORMAL ]  Iteration 138:  k_eff = 1.156279   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 169 D.R. = 2.50
[  NORMAL ]  Iteration 139:  k_eff = 1.157938   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 165 D.R. = 0.80
[  NORMAL ]  Iteration 140:  k_eff = 1.159557   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 161 D.R. = 1.50
[  NORMAL ]  Iteration 141:  k_eff = 1.161139   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 158 D.R. = 0.33
[  NORMAL ]  Iteration 142:  k_eff = 1.162684   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 154 D.R. = 1.00
[  NORMAL ]  Iteration 143:  k_eff = 1.164193   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 150 D.R. = 2.75
[  NORMAL ]  Iteration 144:  k_eff = 1.165666   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 147 D.R. = 0.09
[  NORMAL ]  Iteration 145:  k_eff = 1.167105   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 143 D.R. = 2.00
[  NORMAL ]  Iteration 146:  k_eff = 1.168509   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 140 D.R. = 4.00
[  NORMAL ]  Iteration 147:  k_eff = 1.169881   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 137 D.R. = 1.25
[  NORMAL ]  Iteration 148:  k_eff = 1.171220   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 133 D.R. = 0.10
[  NORMAL ]  Iteration 149:  k_eff = 1.172528   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 130 D.R. = 6.00
[  NORMAL ]  Iteration 150:  k_eff = 1.173804   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 127 D.R. = 1.67
[  NORMAL ]  Iteration 151:  k_eff = 1.175051   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 124 D.R. = 0.80
[  NORMAL ]  Iteration 152:  k_eff = 1.176268   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 121 D.R. = 1.00
[  NORMAL ]  Iteration 153:  k_eff = 1.177456   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 118 D.R. = 1.00
[  NORMAL ]  Iteration 154:  k_eff = 1.178616   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 115 D.R. = 0.63
[  NORMAL ]  Iteration 155:  k_eff = 1.179749   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 113 D.R. = 1.20
[  NORMAL ]  Iteration 156:  k_eff = 1.180855   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 110 D.R. = 0.83
[  NORMAL ]  Iteration 157:  k_eff = 1.181935   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 107 D.R. = 0.20
[  NORMAL ]  Iteration 158:  k_eff = 1.182988   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 105 D.R. = 3.00
[  NORMAL ]  Iteration 159:  k_eff = 1.184017   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 102 D.R. = 0.67
[  NORMAL ]  Iteration 160:  k_eff = 1.185021   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 100 D.R. = 1.50
[  NORMAL ]  Iteration 161:  k_eff = 1.186002   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 98 D.R. = 0.67
[  NORMAL ]  Iteration 162:  k_eff = 1.186959   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 95 D.R. = 2.50
[  NORMAL ]  Iteration 163:  k_eff = 1.187893   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 93 D.R. = 1.40
[  NORMAL ]  Iteration 164:  k_eff = 1.188805   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 91 D.R. = 1.29
[  NORMAL ]  Iteration 165:  k_eff = 1.189695   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 89 D.R. = 0.22
[  NORMAL ]  Iteration 166:  k_eff = 1.190564   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 86 D.R. = 2.00
[  NORMAL ]  Iteration 167:  k_eff = 1.191413   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 84 D.R. = 2.00
[  NORMAL ]  Iteration 168:  k_eff = 1.192241   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 82 D.R. = 0.00
[  NORMAL ]  Iteration 169:  k_eff = 1.193049   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 80 D.R. = inf
[  NORMAL ]  Iteration 170:  k_eff = 1.193838   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 78 D.R. = 4.50
[  NORMAL ]  Iteration 171:  k_eff = 1.194607   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 76 D.R. = 0.56
[  NORMAL ]  Iteration 172:  k_eff = 1.195359   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 75 D.R. = 1.20
[  NORMAL ]  Iteration 173:  k_eff = 1.196092   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 73 D.R. = 0.67
[  NORMAL ]  Iteration 174:  k_eff = 1.196808   res = 1.355E-07  delta-k (pcm)
[  NORMAL ]  ...  = 71 D.R. = 3.50
[  NORMAL ]  Iteration 175:  k_eff = 1.197507   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 69 D.R. = 0.86
[  NORMAL ]  Iteration 176:  k_eff = 1.198190   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 68 D.R. = 0.33
[  NORMAL ]  Iteration 177:  k_eff = 1.198855   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 66 D.R. = 0.75
[  NORMAL ]  Iteration 178:  k_eff = 1.199505   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 64 D.R. = 0.67
[  NORMAL ]  Iteration 179:  k_eff = 1.200139   res = 1.258E-07  delta-k (pcm)
[  NORMAL ]  ...  = 63 D.R. = 6.50
[  NORMAL ]  Iteration 180:  k_eff = 1.200757   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 61 D.R. = 0.62
[  NORMAL ]  Iteration 181:  k_eff = 1.201361   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 60 D.R. = 0.25
[  NORMAL ]  Iteration 182:  k_eff = 1.201951   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 58 D.R. = 1.50
[  NORMAL ]  Iteration 183:  k_eff = 1.202526   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 57 D.R. = 0.33
[  NORMAL ]  Iteration 184:  k_eff = 1.203088   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 56 D.R. = 5.00
[  NORMAL ]  Iteration 185:  k_eff = 1.203636   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 54 D.R. = 0.00
[  NORMAL ]  Iteration 186:  k_eff = 1.204171   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 53 D.R. = inf
[  NORMAL ]  Iteration 187:  k_eff = 1.204692   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 52 D.R. = 2.33
[  NORMAL ]  Iteration 188:  k_eff = 1.205202   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 50 D.R. = 0.57
[  NORMAL ]  Iteration 189:  k_eff = 1.205698   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 49 D.R. = 0.50
[  NORMAL ]  Iteration 190:  k_eff = 1.206183   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 48 D.R. = 0.50
[  NORMAL ]  Iteration 191:  k_eff = 1.206656   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 47 D.R. = 6.00
[  NORMAL ]  Iteration 192:  k_eff = 1.207118   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 46 D.R. = 0.00
[  NORMAL ]  Iteration 193:  k_eff = 1.207570   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 45 D.R. = inf
[  NORMAL ]  Iteration 194:  k_eff = 1.208010   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 44 D.R. = 0.33
[  NORMAL ]  Iteration 195:  k_eff = 1.208439   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 42 D.R. = 1.33
[  NORMAL ]  Iteration 196:  k_eff = 1.208858   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 41 D.R. = 0.50
[  NORMAL ]  Iteration 197:  k_eff = 1.209267   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 40 D.R. = 1.50
[  NORMAL ]  Iteration 198:  k_eff = 1.209665   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 39 D.R. = 1.33
[  NORMAL ]  Iteration 199:  k_eff = 1.210055   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 38 D.R. = 1.50
[  NORMAL ]  Iteration 200:  k_eff = 1.210435   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 37 D.R. = 0.17
[  NORMAL ]  Iteration 201:  k_eff = 1.210805   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 37 D.R. = 9.00
[  NORMAL ]  Iteration 202:  k_eff = 1.211167   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 36 D.R. = 0.11
[  NORMAL ]  Iteration 203:  k_eff = 1.211520   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 35 D.R. = 5.00
[  NORMAL ]  Iteration 204:  k_eff = 1.211864   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 34 D.R. = 0.60
[  NORMAL ]  Iteration 205:  k_eff = 1.212200   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 33 D.R. = 0.67
[  NORMAL ]  Iteration 206:  k_eff = 1.212528   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 32 D.R. = 2.50
[  NORMAL ]  Iteration 207:  k_eff = 1.212848   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 32 D.R. = 1.60
[  NORMAL ]  Iteration 208:  k_eff = 1.213160   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 31 D.R. = 0.25
[  NORMAL ]  Iteration 209:  k_eff = 1.213466   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 30 D.R. = 5.50
[  NORMAL ]  Iteration 210:  k_eff = 1.213763   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 29 D.R. = 0.55
[  NORMAL ]  Iteration 211:  k_eff = 1.214053   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 29 D.R. = 1.67
[  NORMAL ]  Iteration 212:  k_eff = 1.214337   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 28 D.R. = 0.30
[  NORMAL ]  Iteration 213:  k_eff = 1.214614   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 27 D.R. = 2.33
[  NORMAL ]  Iteration 214:  k_eff = 1.214883   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 26 D.R. = 1.29
[  NORMAL ]  Iteration 215:  k_eff = 1.215145   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 26 D.R. = 0.56
[  NORMAL ]  Iteration 216:  k_eff = 1.215402   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 25 D.R. = 0.40
[  NORMAL ]  Iteration 217:  k_eff = 1.215653   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 25 D.R. = 3.00
[  NORMAL ]  Iteration 218:  k_eff = 1.215897   res = 1.258E-07  delta-k (pcm)
[  NORMAL ]  ...  = 24 D.R. = 2.17
[  NORMAL ]  Iteration 219:  k_eff = 1.216136   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 23 D.R. = 0.77
[  NORMAL ]  Iteration 220:  k_eff = 1.216368   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 23 D.R. = 0.80
[  NORMAL ]  Iteration 221:  k_eff = 1.216595   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 22 D.R. = 0.25
[  NORMAL ]  Iteration 222:  k_eff = 1.216817   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 22 D.R. = 3.00
[  NORMAL ]  Iteration 223:  k_eff = 1.217033   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 21 D.R. = 0.33
[  NORMAL ]  Iteration 224:  k_eff = 1.217244   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 21 D.R. = 4.00
[  NORMAL ]  Iteration 225:  k_eff = 1.217450   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 20 D.R. = 0.75
[  NORMAL ]  Iteration 226:  k_eff = 1.217651   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 20 D.R. = 0.17
[  NORMAL ]  Iteration 227:  k_eff = 1.217847   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 19 D.R. = 4.00
[  NORMAL ]  Iteration 228:  k_eff = 1.218039   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 19 D.R. = 2.50
[  NORMAL ]  Iteration 229:  k_eff = 1.218225   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 18 D.R. = 0.20
[  NORMAL ]  Iteration 230:  k_eff = 1.218407   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 18 D.R. = 2.50
[  NORMAL ]  Iteration 231:  k_eff = 1.218585   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 17 D.R. = 1.00
[  NORMAL ]  Iteration 232:  k_eff = 1.218758   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 17 D.R. = 1.40
[  NORMAL ]  Iteration 233:  k_eff = 1.218927   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 16 D.R. = 0.71
[  NORMAL ]  Iteration 234:  k_eff = 1.219092   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 16 D.R. = 1.00
[  NORMAL ]  Iteration 235:  k_eff = 1.219253   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 16 D.R. = 1.40
[  NORMAL ]  Iteration 236:  k_eff = 1.219410   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 15 D.R. = 0.57
[  NORMAL ]  Iteration 237:  k_eff = 1.219563   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 15 D.R. = 0.50
[  NORMAL ]  Iteration 238:  k_eff = 1.219712   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 14 D.R. = 0.00
[  NORMAL ]  Iteration 239:  k_eff = 1.219858   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 14 D.R. = -nan
[  NORMAL ]  Iteration 240:  k_eff = 1.220000   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 14 D.R. = inf
[  NORMAL ]  Iteration 241:  k_eff = 1.220139   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 13 D.R. = 1.13
[  NORMAL ]  Iteration 242:  k_eff = 1.220274   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 13 D.R. = 1.11
[  NORMAL ]  Iteration 243:  k_eff = 1.220407   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 13 D.R. = 1.10
[  NORMAL ]  Iteration 244:  k_eff = 1.220536   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 12 D.R. = 0.64
[  NORMAL ]  Iteration 245:  k_eff = 1.220662   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 12 D.R. = 0.43
[  NORMAL ]  Iteration 246:  k_eff = 1.220784   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 12 D.R. = 0.67
[  NORMAL ]  Iteration 247:  k_eff = 1.220904   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 1.00
[  NORMAL ]  Iteration 248:  k_eff = 1.221021   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 3.50
[  NORMAL ]  Iteration 249:  k_eff = 1.221135   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 1.43
[  NORMAL ]  Iteration 250:  k_eff = 1.221246   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 11 D.R. = 0.80
[  NORMAL ]  Iteration 251:  k_eff = 1.221355   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 0.87
[  NORMAL ]  Iteration 252:  k_eff = 1.221461   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 0.43
[  NORMAL ]  Iteration 253:  k_eff = 1.221564   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 1.67
[  NORMAL ]  Iteration 254:  k_eff = 1.221665   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 10 D.R. = 0.40
[  NORMAL ]  Iteration 255:  k_eff = 1.221763   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = 0.00
[  NORMAL ]  Iteration 256:  k_eff = 1.221859   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = inf
[  NORMAL ]  Iteration 257:  k_eff = 1.221953   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = 1.00
[  NORMAL ]  Iteration 258:  k_eff = 1.222044   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 9 D.R. = 0.40
[  NORMAL ]  Iteration 259:  k_eff = 1.222133   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 1.50
[  NORMAL ]  Iteration 260:  k_eff = 1.222220   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 0.00
[  NORMAL ]  Iteration 261:  k_eff = 1.222305   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = inf
[  NORMAL ]  Iteration 262:  k_eff = 1.222387   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 1.17
[  NORMAL ]  Iteration 263:  k_eff = 1.222468   res = 1.839E-07  delta-k (pcm)
[  NORMAL ]  ...  = 8 D.R. = 2.71
[  NORMAL ]  Iteration 264:  k_eff = 1.222547   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 0.16
[  NORMAL ]  Iteration 265:  k_eff = 1.222624   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 1.00
[  NORMAL ]  Iteration 266:  k_eff = 1.222699   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 0.33
[  NORMAL ]  Iteration 267:  k_eff = 1.222772   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 6.00
[  NORMAL ]  Iteration 268:  k_eff = 1.222844   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 7 D.R. = 1.17
[  NORMAL ]  Iteration 269:  k_eff = 1.222913   res = 1.452E-07  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 2.14
[  NORMAL ]  Iteration 270:  k_eff = 1.222981   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.40
[  NORMAL ]  Iteration 271:  k_eff = 1.223048   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.17
[  NORMAL ]  Iteration 272:  k_eff = 1.223113   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 3.00
[  NORMAL ]  Iteration 273:  k_eff = 1.223176   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 1.33
[  NORMAL ]  Iteration 274:  k_eff = 1.223238   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 1.00
[  NORMAL ]  Iteration 275:  k_eff = 1.223298   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 1.00
[  NORMAL ]  Iteration 276:  k_eff = 1.223357   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 1.00
[  NORMAL ]  Iteration 277:  k_eff = 1.223414   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.50
[  NORMAL ]  Iteration 278:  k_eff = 1.223470   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 1.50
[  NORMAL ]  Iteration 279:  k_eff = 1.223524   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 1.67
[  NORMAL ]  Iteration 280:  k_eff = 1.223577   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.20
[  NORMAL ]  Iteration 281:  k_eff = 1.223629   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 5.00
[  NORMAL ]  Iteration 282:  k_eff = 1.223680   res = 1.258E-07  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 2.60
[  NORMAL ]  Iteration 283:  k_eff = 1.223729   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.77
[  NORMAL ]  Iteration 284:  k_eff = 1.223777   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.70
[  NORMAL ]  Iteration 285:  k_eff = 1.223824   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 1.00
[  NORMAL ]  Iteration 286:  k_eff = 1.223870   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 1.00
[  NORMAL ]  Iteration 287:  k_eff = 1.223915   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.29
[  NORMAL ]  Iteration 288:  k_eff = 1.223959   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.00
[  NORMAL ]  Iteration 289:  k_eff = 1.224001   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = inf
[  NORMAL ]  Iteration 290:  k_eff = 1.224043   res = 1.355E-07  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 2.80
[  NORMAL ]  Iteration 291:  k_eff = 1.224083   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.57
[  NORMAL ]  Iteration 292:  k_eff = 1.224123   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.37
[  NORMAL ]  Iteration 293:  k_eff = 1.224161   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 1.33
[  NORMAL ]  Iteration 294:  k_eff = 1.224199   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 2.00
[  NORMAL ]  Iteration 295:  k_eff = 1.224235   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.50
[  NORMAL ]  Iteration 296:  k_eff = 1.224271   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 1.75
[  NORMAL ]  Iteration 297:  k_eff = 1.224306   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 1.71
[  NORMAL ]  Iteration 298:  k_eff = 1.224340   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.08
[  NORMAL ]  Iteration 299:  k_eff = 1.224373   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 2.00
[  NORMAL ]  Iteration 300:  k_eff = 1.224406   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 5.50
[  NORMAL ]  Iteration 301:  k_eff = 1.224438   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.82
[  NORMAL ]  Iteration 302:  k_eff = 1.224468   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.78
[  NORMAL ]  Iteration 303:  k_eff = 1.224498   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.71
[  NORMAL ]  Iteration 304:  k_eff = 1.224528   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.40
[  NORMAL ]  Iteration 305:  k_eff = 1.224557   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 3.00
[  NORMAL ]  Iteration 306:  k_eff = 1.224585   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.83
[  NORMAL ]  Iteration 307:  k_eff = 1.224612   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.20
[  NORMAL ]  Iteration 308:  k_eff = 1.224639   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 3.00
[  NORMAL ]  Iteration 309:  k_eff = 1.224665   res = 9.679E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 3.33
[  NORMAL ]  Iteration 310:  k_eff = 1.224691   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.80
[  NORMAL ]  Iteration 311:  k_eff = 1.224716   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.50
[  NORMAL ]  Iteration 312:  k_eff = 1.224740   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.75
[  NORMAL ]  Iteration 313:  k_eff = 1.224764   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 4.00
[  NORMAL ]  Iteration 314:  k_eff = 1.224786   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.17
[  NORMAL ]  Iteration 315:  k_eff = 1.224809   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 1.50
[  NORMAL ]  Iteration 316:  k_eff = 1.224831   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 3.67
[  NORMAL ]  Iteration 317:  k_eff = 1.224852   res = 1.161E-07  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 1.09
[  NORMAL ]  Iteration 318:  k_eff = 1.224873   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.00
[  NORMAL ]  Iteration 319:  k_eff = 1.224893   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = inf
[  NORMAL ]  Iteration 320:  k_eff = 1.224913   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.63
[  NORMAL ]  Iteration 321:  k_eff = 1.224932   res = 6.775E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.40
[  NORMAL ]  Iteration 322:  k_eff = 1.224951   res = 8.711E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.29
[  NORMAL ]  Iteration 323:  k_eff = 1.224969   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.11
[  NORMAL ]  Iteration 324:  k_eff = 1.224987   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 8.00
[  NORMAL ]  Iteration 325:  k_eff = 1.225005   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.25
[  NORMAL ]  Iteration 326:  k_eff = 1.225022   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.50
[  NORMAL ]  Iteration 327:  k_eff = 1.225039   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 2.00
[  NORMAL ]  Iteration 328:  k_eff = 1.225055   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 3.00
[  NORMAL ]  Iteration 329:  k_eff = 1.225071   res = 1.065E-07  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.83
[  NORMAL ]  Iteration 330:  k_eff = 1.225086   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.27
[  NORMAL ]  Iteration 331:  k_eff = 1.225102   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.00
[  NORMAL ]  Iteration 332:  k_eff = 1.225116   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.00
[  NORMAL ]  Iteration 333:  k_eff = 1.225131   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 2.00
[  NORMAL ]  Iteration 334:  k_eff = 1.225145   res = 4.840E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.83
[  NORMAL ]  Iteration 335:  k_eff = 1.225159   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.20
[  NORMAL ]  Iteration 336:  k_eff = 1.225172   res = 5.807E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 6.00
[  NORMAL ]  Iteration 337:  k_eff = 1.225185   res = 1.355E-07  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 2.33
[  NORMAL ]  Iteration 338:  k_eff = 1.225198   res = 1.258E-07  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.93
[  NORMAL ]  Iteration 339:  k_eff = 1.225210   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.00
[  NORMAL ]  Iteration 340:  k_eff = 1.225222   res = 9.679E-09  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = inf
[  NORMAL ]  Iteration 341:  k_eff = 1.225233   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 4.00
[  NORMAL ]  Iteration 342:  k_eff = 1.225245   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.50
[  NORMAL ]  Iteration 343:  k_eff = 1.225256   res = 0.000E+00  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.00
[  NORMAL ]  Iteration 344:  k_eff = 1.225267   res = 7.743E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = inf
[  NORMAL ]  Iteration 345:  k_eff = 1.225278   res = 2.904E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.38
[  NORMAL ]  Iteration 346:  k_eff = 1.225288   res = 3.872E-08  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 1.33
[  NORMAL ]  Iteration 347:  k_eff = 1.225298   res = 1.936E-08  delta-k (pcm)
[  NORMAL ]  ...  = 0 D.R. = 0.50
[27]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined.n
bias = (openmoc_keff - openmc_keff) * 1e5

print('openmc keff = {0:1.6f}'.format(openmc_keff))
print('openmoc keff = {0:1.6f}'.format(openmoc_keff))
print('bias [pcm]: {0:1.1f}'.format(bias))
openmc keff = 1.224484
openmoc keff = 1.225298
bias [pcm]: 81.4

There is a non-trivial bias in both the 2-group and 8-group cases. In the case of a pin cell, one can show that these biases do not converge to <100 pcm with more particle histories. For heterogeneous geometries, additional measures must be taken to address the following three sources of bias:

  • Appropriate transport-corrected cross sections
  • Spatial discretization of OpenMOC’s mesh
  • Constant-in-angle multi-group cross sections
Visualizing MGXS Data

It is often insightful to generate visual depictions of multi-group cross sections. There are many different types of plots which may be useful for multi-group cross section visualization, only a few of which will be shown here for enrichment and inspiration.

One particularly useful visualization is a comparison of the continuous-energy and multi-group cross sections for a particular nuclide and reaction type. We illustrate one option for generating such plots with the use of the openmc.plotter module to plot continuous-energy cross sections from the openly available cross section library distributed by NNDC.

The MGXS data can also be plotted using the openmc.plot_xs command, however we will do this manually here to show how the openmc.Mgxs.get_xs method can be used to obtain data.

[28]:
# Create a figure of the U-235 continuous-energy fission cross section
fig = openmc.plot_xs('U235', ['fission'])

# Get the axis to use for plotting the MGXS
ax = fig.gca()

# Extract energy group bounds and MGXS values to plot
fission = xs_library[fuel_cell.id]['fission']
energy_groups = fission.energy_groups
x = energy_groups.group_edges
y = fission.get_xs(nuclides=['U235'], order_groups='decreasing', xs_type='micro')
y = np.squeeze(y)

# Fix low energy bound
x[0] = 1.e-5

# Extend the mgxs values array for matplotlib's step plot
y = np.insert(y, 0, y[0])

# Create a step plot for the MGXS
ax.plot(x, y, drawstyle='steps', color='r', linewidth=3)

ax.set_title('U-235 Fission Cross Section')
ax.legend(['Continuous', 'Multi-Group'])
ax.set_xlim((x.min(), x.max()))
[28]:
(1e-05, 20000000.0)
_images/examples_mgxs-part-ii_59_1.png

Another useful type of illustration is scattering matrix sparsity structures. First, we extract Pandas DataFrames for the H-1 and O-16 scattering matrices.

[29]:
# Construct a Pandas DataFrame for the microscopic nu-scattering matrix
nuscatter = xs_library[moderator_cell.id]['nu-scatter']
df = nuscatter.get_pandas_dataframe(xs_type='micro')

# Slice DataFrame in two for each nuclide's mean values
h1 = df[df['nuclide'] == 'H1']['mean']
o16 = df[df['nuclide'] == 'O16']['mean']

# Cast DataFrames as NumPy arrays
h1 = h1.values
o16 = o16.values

# Reshape arrays to 2D matrix for plotting
h1.shape = (fine_groups.num_groups, fine_groups.num_groups)
o16.shape = (fine_groups.num_groups, fine_groups.num_groups)

Matplotlib’s imshow routine can be used to plot the matrices to illustrate their sparsity structures.

[30]:
# Create plot of the H-1 scattering matrix
fig = plt.subplot(121)
fig.imshow(h1, interpolation='nearest', cmap='jet')
plt.title('H-1 Scattering Matrix')
plt.xlabel('Group Out')
plt.ylabel('Group In')

# Create plot of the O-16 scattering matrix
fig2 = plt.subplot(122)
fig2.imshow(o16, interpolation='nearest', cmap='jet')
plt.title('O-16 Scattering Matrix')
plt.xlabel('Group Out')
plt.ylabel('Group In')

# Show the plot on screen
plt.show()
_images/examples_mgxs-part-ii_63_0.png

Multigroup Cross Section Generation Part III: Libraries

This IPython Notebook illustrates the use of the ``openmc.mgxs.Library`` class. The Library class is designed to automate the calculation of multi-group cross sections for use cases with one or more domains, cross section types, and/or nuclides. In particular, this Notebook illustrates the following features:

  • Calculation of multi-group cross sections for a fuel assembly
  • Automated creation, manipulation and storage of MGXS with ``openmc.mgxs.Library``
  • Validation of multi-group cross sections with `OpenMOC <https://mit-crpg.github.io/OpenMOC/>`__
  • Steady-state pin-by-pin fission rates comparison between OpenMC and OpenMOC

Note: This Notebook was created using OpenMOC to verify the multi-group cross-sections generated by OpenMC. You must install OpenMOC on your system to run this Notebook in its entirety. In addition, this Notebook illustrates the use of Pandas DataFrames to containerize multi-group cross section data.

Generate Input Files
[1]:
import math
import pickle

from IPython.display import Image
import matplotlib.pyplot as plt
import numpy as np

import openmc
import openmc.mgxs
from openmc.openmoc_compatible import get_openmoc_geometry
import openmoc
import openmoc.process
from openmoc.materialize import load_openmc_mgxs_lib

%matplotlib inline

First we need to define materials that will be used in the problem. We’ll create three materials for the fuel, water, and cladding of the fuel pins.

[2]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)
water.add_nuclide('B10', 8.0042e-6)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our three materials, we can now create a Materials object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials object
materials_file = openmc.Materials([fuel, water, zircaloy])

# Export to "materials.xml"
materials_file.export_to_xml()

Now let’s move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC’s lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, r=0.45720)

# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='reflective')
min_y = openmc.YPlane(y0=-10.71, boundary_type='reflective')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10., boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10., boundary_type='reflective')

With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
fuel_pin_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
fuel_pin_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
fuel_pin_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
fuel_pin_universe.add_cell(moderator_cell)

Likewise, we can construct a control rod guide tube with the same surfaces.

[6]:
# Create a Universe to encapsulate a control rod guide tube
guide_tube_universe = openmc.Universe(name='Guide Tube')

# Create guide tube Cell
guide_tube_cell = openmc.Cell(name='Guide Tube Water')
guide_tube_cell.fill = water
guide_tube_cell.region = -fuel_outer_radius
guide_tube_universe.add_cell(guide_tube_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='Guide Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
guide_tube_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='Guide Tube Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
guide_tube_universe.add_cell(moderator_cell)

Using the pin cell universe, we can construct a 17x17 rectangular lattice with a 1.26 cm pitch.

[7]:
# Create fuel assembly Lattice
assembly = openmc.RectLattice(name='1.6% Fuel Assembly')
assembly.pitch = (1.26, 1.26)
assembly.lower_left = [-1.26 * 17. / 2.0] * 2

Next, we create a NumPy array of fuel pin and guide tube universes for the lattice.

[8]:
# Create array indices for guide tube locations in lattice
template_x = np.array([5, 8, 11, 3, 13, 2, 5, 8, 11, 14, 2, 5, 8,
                       11, 14, 2, 5, 8, 11, 14, 3, 13, 5, 8, 11])
template_y = np.array([2, 2, 2, 3, 3, 5, 5, 5, 5, 5, 8, 8, 8, 8,
                       8, 11, 11, 11, 11, 11, 13, 13, 14, 14, 14])

# Initialize an empty 17x17 array of the lattice universes
universes = np.empty((17, 17), dtype=openmc.Universe)

# Fill the array with the fuel pin and guide tube universes
universes[:,:] = fuel_pin_universe
universes[template_x, template_y] = guide_tube_universe

# Store the array of universes in the lattice
assembly.universes = universes

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the assembly and then assign it to the root universe.

[9]:
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = assembly

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(root_cell)

We now must create a geometry that is assigned a root universe and export it to XML.

[10]:
# Create Geometry and set root Universe
geometry = openmc.Geometry(root_universe)
[11]:
# Export to "geometry.xml"
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 2500 particles.

[12]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 10000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()

Let us also create a plot to verify that our fuel assembly geometry was created successfully.

[13]:
# Instantiate a Plot
plot = openmc.Plot.from_geometry(geometry)
plot.pixels = (250, 250)
plot.color_by = 'material'
plot.to_ipython_image()
[13]:
_images/examples_mgxs-part-iii_25_0.png

As we can see from the plot, we have a nice array of fuel and guide tube pin cells with fuel, cladding, and water!

Create an MGXS Library

Now we are ready to generate multi-group cross sections! First, let’s define a 2-group structure using the built-in EnergyGroups class.

[14]:
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])

Next, we will instantiate an openmc.mgxs.Library for the energy groups with the fuel assembly geometry.

[15]:
# Initialize a 2-group MGXS Library for OpenMOC
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = groups

Now, we must specify to the Library which types of cross sections to compute. In particular, the following are the multi-group cross section MGXS subclasses that are mapped to string codes accepted by the Library class:

  • TotalXS ("total")
  • TransportXS ("transport" or "nu-transport with nu set to True)
  • AbsorptionXS ("absorption")
  • CaptureXS ("capture")
  • FissionXS ("fission" or "nu-fission" with nu set to True)
  • KappaFissionXS ("kappa-fission")
  • ScatterXS ("scatter" or "nu-scatter" with nu set to True)
  • ScatterMatrixXS ("scatter matrix" or "nu-scatter matrix" with nu set to True)
  • Chi ("chi")
  • ChiPrompt ("chi prompt")
  • InverseVelocity ("inverse-velocity")
  • PromptNuFissionXS ("prompt-nu-fission")
  • DelayedNuFissionXS ("delayed-nu-fission")
  • ChiDelayed ("chi-delayed")
  • Beta ("beta")

In this case, let’s create the multi-group cross sections needed to run an OpenMOC simulation to verify the accuracy of our cross sections. In particular, we will define "nu-transport", "nu-fission", '"fission", "nu-scatter matrix" and "chi" cross sections for our Library.

Note: A variety of different approximate transport-corrected total multi-group cross sections (and corresponding scattering matrices) can be found in the literature. At the present time, the openmc.mgxs module only supports the "P0" transport correction. This correction can be turned on and off through the boolean Library.correction property which may take values of "P0" (default) or None.

[16]:
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['nu-transport', 'nu-fission', 'fission', 'nu-scatter matrix', 'chi']

Now we must specify the type of domain over which we would like the Library to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library supports "material", "cell", "universe", and "mesh" domain types. We will use a "cell" domain type here to compute cross sections in each of the cells in the fuel assembly geometry.

Note: By default, the Library class will instantiate MGXS objects for each and every domain (material, cell or universe) in the geometry of interest. However, one may specify a subset of these domains to the Library.domains property. In our case, we wish to compute multi-group cross sections in each and every cell since they will be needed in our downstream OpenMOC calculation on the identical combinatorial geometry mesh.

[17]:
# Specify a "cell" domain type for the cross section tally filters
mgxs_lib.domain_type = 'cell'

# Specify the cell domains over which to compute multi-group cross sections
mgxs_lib.domains = geometry.get_all_material_cells().values()

We can easily instruct the Library to compute multi-group cross sections on a nuclide-by-nuclide basis with the boolean Library.by_nuclide property. By default, by_nuclide is set to False, but we will set it to True here.

[18]:
# Compute cross sections on a nuclide-by-nuclide basis
mgxs_lib.by_nuclide = True

Lastly, we use the Library to construct the tallies needed to compute all of the requested multi-group cross sections in each domain and nuclide.

[19]:
# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()

The tallies can now be export to a “tallies.xml” input file for OpenMC.

NOTE: At this point the Library has constructed nearly 100 distinct Tally objects. The overhead to tally in OpenMC scales as \(O(N)\) for \(N\) tallies, which can become a bottleneck for large tally datasets. To compensate for this, the Python API’s Tally, Filter and Tallies classes allow for the smart merging of tallies when possible. The Library class supports this runtime optimization with the use of the optional merge paramter (False by default) for the Library.add_to_tallies_file(...) method, as shown below.

[20]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)

In addition, we instantiate a fission rate mesh tally to compare with OpenMOC.

[21]:
# Instantiate a tally Mesh
mesh = openmc.RegularMesh(mesh_id=1)
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.upper_right = [+10.71, +10.71]

# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)

# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission', 'nu-fission']

# Add tally to collection
tallies_file.append(tally)
[22]:
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=126.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=21.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=3.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=4.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=96.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=15.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=114.
  warn(msg, IDWarning)
[23]:
# Run OpenMC
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-19 07:12:55
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading B10 from /opt/data/hdf5/nndc_hdf5_v15/B10.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.03784
        2/1    1.02297
        3/1    1.02244
        4/1    1.02344
        5/1    1.02057
        6/1    1.04077
        7/1    1.00775
        8/1    1.03892
        9/1    1.01606
       10/1    1.02209
       11/1    1.03259
       12/1    1.03331    1.03295 +/- 0.00036
       13/1    1.02027    1.02872 +/- 0.00423
       14/1    1.03901    1.03130 +/- 0.00395
       15/1    1.02000    1.02904 +/- 0.00380
       16/1    1.04469    1.03164 +/- 0.00405
       17/1    1.01862    1.02978 +/- 0.00390
       18/1    1.03265    1.03014 +/- 0.00340
       19/1    1.00489    1.02734 +/- 0.00410
       20/1    1.04533    1.02914 +/- 0.00409
       21/1    1.01534    1.02788 +/- 0.00390
       22/1    1.02204    1.02739 +/- 0.00360
       23/1    1.02181    1.02696 +/- 0.00334
       24/1    0.99207    1.02447 +/- 0.00397
       25/1    1.03041    1.02487 +/- 0.00372
       26/1    1.03652    1.02560 +/- 0.00355
       27/1    1.03793    1.02632 +/- 0.00341
       28/1    1.02099    1.02603 +/- 0.00323
       29/1    1.01953    1.02568 +/- 0.00308
       30/1    1.01690    1.02525 +/- 0.00295
       31/1    1.01938    1.02497 +/- 0.00282
       32/1    1.01800    1.02465 +/- 0.00271
       33/1    1.01598    1.02427 +/- 0.00262
       34/1    1.01735    1.02398 +/- 0.00252
       35/1    1.01080    1.02346 +/- 0.00247
       36/1    1.01267    1.02304 +/- 0.00241
       37/1    1.01907    1.02289 +/- 0.00233
       38/1    1.02333    1.02291 +/- 0.00224
       39/1    1.01516    1.02264 +/- 0.00218
       40/1    1.02797    1.02282 +/- 0.00211
       41/1    1.03949    1.02336 +/- 0.00211
       42/1    1.01456    1.02308 +/- 0.00207
       43/1    1.02376    1.02310 +/- 0.00200
       44/1    1.01917    1.02299 +/- 0.00195
       45/1    1.01631    1.02280 +/- 0.00190
       46/1    1.02381    1.02282 +/- 0.00185
       47/1    1.04002    1.02329 +/- 0.00185
       48/1    1.01059    1.02296 +/- 0.00184
       49/1    1.02647    1.02305 +/- 0.00179
       50/1    1.02451    1.02308 +/- 0.00175
 Creating state point statepoint.50.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 5.7635e-01 seconds
   Reading cross sections          = 5.4002e-01 seconds
 Total time in simulation          = 7.0174e+01 seconds
   Time in transport only          = 6.9687e+01 seconds
   Time in inactive batches        = 7.1832e+00 seconds
   Time in active batches          = 6.2991e+01 seconds
   Time synchronizing fission bank = 3.9991e-02 seconds
     Sampling source sites         = 3.4633e-02 seconds
     SEND/RECV source sites        = 5.2616e-03 seconds
   Time accumulating tallies       = 4.9801e-04 seconds
 Total time for finalization       = 1.3501e-05 seconds
 Total time elapsed                = 7.0791e+01 seconds
 Calculation Rate (inactive)       = 13921.3 particles/second
 Calculation Rate (active)         = 6350.11 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.02434 +/- 0.00173
 k-effective (Track-length)  = 1.02308 +/- 0.00175
 k-effective (Absorption)    = 1.02494 +/- 0.00175
 Combined k-effective        = 1.02408 +/- 0.00144
 Leakage Fraction            = 0.00000 +/- 0.00000

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

[24]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.50.h5')

The statepoint is now ready to be analyzed by the Library. We simply have to load the tallies from the statepoint into the Library and our MGXS objects will compute the cross sections for us under-the-hood.

[25]:
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)

Voila! Our multi-group cross sections are now ready to rock ‘n roll!

Extracting and Storing MGXS Data

The Library supports a rich API to automate a variety of tasks, including multi-group cross section data retrieval and storage. We will highlight a few of these features here. First, the Library.get_mgxs(...) method allows one to extract an MGXS object from the Library for a particular domain and cross section type. The following cell illustrates how one may extract the NuFissionXS object for the fuel cell.

Note: The MGXS.get_mgxs(...) method will accept either the domain or the integer domain ID of interest.

[26]:
# Retrieve the NuFissionXS object for the fuel cell from the library
fuel_mgxs = mgxs_lib.get_mgxs(fuel_cell, 'nu-fission')

The NuFissionXS object supports all of the methods described previously in the openmc.mgxs tutorials, such as Pandas DataFrames: Note that since so few histories were simulated, we should expect a few division-by-error errors as some tallies have not yet scored any results.

[27]:
df = fuel_mgxs.get_pandas_dataframe()
df
[27]:
cell group in nuclide mean std. dev.
3 1 1 U235 8.089079e-03 1.461462e-05
4 1 1 U238 7.358661e-03 2.302063e-05
5 1 1 O16 0.000000e+00 0.000000e+00
0 1 2 U235 3.617174e-01 9.467633e-04
1 1 2 U238 6.744743e-07 1.750450e-09
2 1 2 O16 0.000000e+00 0.000000e+00

Similarly, we can use the MGXS.print_xs(...) method to view a string representation of the multi-group cross section data.

[28]:
fuel_mgxs.print_xs()
Multi-Group XS
        Reaction Type  =        nu-fission
        Domain Type    =        cell
        Domain ID      =        1
        Nuclide        =        U235
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        8.09e-03 +/- 1.81e-01%
            Group 2 [0.0        - 0.625     eV]:        3.62e-01 +/- 2.62e-01%

        Nuclide        =        U238
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        7.36e-03 +/- 3.13e-01%
            Group 2 [0.0        - 0.625     eV]:        6.74e-07 +/- 2.60e-01%

        Nuclide        =        O16
        Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:        0.00e+00 +/- 0.00e+00%
            Group 2 [0.0        - 0.625     eV]:        0.00e+00 +/- 0.00e+00%



/home/romano/openmc/openmc/tallies.py:1269: RuntimeWarning: invalid value encountered in true_divide
  data = self.std_dev[indices] / self.mean[indices]

One can export the entire Library to HDF5 with the Library.build_hdf5_store(...) method as follows:

[29]:
# Store the cross section data in an "mgxs/mgxs.h5" HDF5 binary file
mgxs_lib.build_hdf5_store(filename='mgxs.h5', directory='mgxs')

The HDF5 store will contain the numerical multi-group cross section data indexed by domain, nuclide and cross section type. Some data workflows may be optimized by storing and retrieving binary representations of the MGXS objects in the Library. This feature is supported through the Library.dump_to_file(...) and Library.load_from_file(...) routines which use Python’s `pickle <https://docs.python.org/3/library/pickle.html>`__ module. This is illustrated as follows.

[30]:
# Store a Library and its MGXS objects in a pickled binary file "mgxs/mgxs.pkl"
mgxs_lib.dump_to_file(filename='mgxs', directory='mgxs')
[31]:
# Instantiate a new MGXS Library from the pickled binary file "mgxs/mgxs.pkl"
mgxs_lib = openmc.mgxs.Library.load_from_file(filename='mgxs', directory='mgxs')

The Library class may be used to leverage the energy condensation features supported by the MGXS class. In particular, one can use the Library.get_condensed_library(...) with a coarse group structure which is a subset of the original “fine” group structure as shown below.

[32]:
# Create a 1-group structure
coarse_groups = openmc.mgxs.EnergyGroups(group_edges=[0., 20.0e6])

# Create a new MGXS Library on the coarse 1-group structure
coarse_mgxs_lib = mgxs_lib.get_condensed_library(coarse_groups)
[33]:
# Retrieve the NuFissionXS object for the fuel cell from the 1-group library
coarse_fuel_mgxs = coarse_mgxs_lib.get_mgxs(fuel_cell, 'nu-fission')

# Show the Pandas DataFrame for the 1-group MGXS
coarse_fuel_mgxs.get_pandas_dataframe()
[33]:
cell group in nuclide mean std. dev.
0 1 1 U235 0.074556 0.000144
1 1 1 U238 0.005976 0.000019
2 1 1 O16 0.000000 0.000000
Verification with OpenMOC

Of course it is always a good idea to verify that one’s cross sections are accurate. We can easily do so here with the deterministic transport code OpenMOC. We first construct an equivalent OpenMOC geometry.

[34]:
# Create an OpenMOC Geometry from the OpenMC Geometry
openmoc_geometry = get_openmoc_geometry(mgxs_lib.geometry)

Now, we can inject the multi-group cross sections into the equivalent fuel assembly OpenMOC geometry. The openmoc.materialize module supports the loading of Library objects from OpenMC as illustrated below.

[35]:
# Load the library into the OpenMOC geometry
materials = load_openmc_mgxs_lib(mgxs_lib, openmoc_geometry)

We are now ready to run OpenMOC to verify our cross-sections from OpenMC.

[36]:
# Generate tracks for OpenMOC
track_generator = openmoc.TrackGenerator(openmoc_geometry, num_azim=32, azim_spacing=0.1)
track_generator.generateTracks()

# Run OpenMOC
solver = openmoc.CPUSolver(track_generator)
solver.computeEigenvalue()
[  NORMAL ]  Initializing a default angular quadrature...
[  NORMAL ]  Initializing 2D tracks...
[  NORMAL ]  Initializing 2D tracks reflections...
[  NORMAL ]  Initializing 2D tracks array...
[  NORMAL ]  Ray tracing for 2D track segmentation...
[ WARNING ]  The Geometry was set with non-infinite z-boundaries and supplied
[ WARNING ]  ...  to a 2D TrackGenerator. The min-z boundary was set to -10.00
[ WARNING ]  ... and the max-z boundary was set to 10.00. Z-boundaries are
[ WARNING ]  ... assumed to be infinite in 2D TrackGenerators.
[  NORMAL ]  Progress Segmenting 2D tracks: 0.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 10.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 20.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 30.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 40.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 50.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 60.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 70.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 80.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 90.02 %
[  NORMAL ]  Progress Segmenting 2D tracks: 100.00 %
[  NORMAL ]  Initializing FSR lookup vectors
[  NORMAL ]  Total number of FSRs 867
[  RESULT ]  Total Track Generation & Segmentation Time...........4.2139E-01 sec
[  NORMAL ]  Initializing MOC eigenvalue solver...
[  NORMAL ]  Initializing solver arrays...
[  NORMAL ]  Centering segments around FSR centroid...
[  NORMAL ]  Max boundary angular flux storage per domain =   0.42 MB
[  NORMAL ]  Max scalar flux storage per domain =   0.01 MB
[  NORMAL ]  Max source storage per domain =   0.01 MB
[  NORMAL ]  Number of azimuthal angles = 32
[  NORMAL ]  Azimuthal ray spacing = 0.100000
[  NORMAL ]  Number of polar angles = 6
[  NORMAL ]  Source type = Flat
[  NORMAL ]  MOC transport undamped
[  NORMAL ]  CMFD acceleration: OFF
[  NORMAL ]  Using 1 threads
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:  k_eff = 0.823216   res = 9.828E-02  delta-k (pcm) =
[  NORMAL ]  ...  -17678 D.R. = 0.10
[  NORMAL ]  Iteration 1:  k_eff = 0.779788   res = 4.642E-02  delta-k (pcm) =
[  NORMAL ]  ...  -4342 D.R. = 0.47
[  NORMAL ]  Iteration 2:  k_eff = 0.738779   res = 9.633E-03  delta-k (pcm) =
[  NORMAL ]  ...  -4100 D.R. = 0.21
[  NORMAL ]  Iteration 3:  k_eff = 0.710046   res = 8.556E-03  delta-k (pcm) =
[  NORMAL ]  ...  -2873 D.R. = 0.89
[  NORMAL ]  Iteration 4:  k_eff = 0.688781   res = 5.190E-03  delta-k (pcm) =
[  NORMAL ]  ...  -2126 D.R. = 0.61
[  NORMAL ]  Iteration 5:  k_eff = 0.674128   res = 3.585E-03  delta-k (pcm) =
[  NORMAL ]  ...  -1465 D.R. = 0.69
[  NORMAL ]  Iteration 6:  k_eff = 0.664928   res = 2.516E-03  delta-k (pcm) =
[  NORMAL ]  ...  -919 D.R. = 0.70
[  NORMAL ]  Iteration 7:  k_eff = 0.660304   res = 1.866E-03  delta-k (pcm) =
[  NORMAL ]  ...  -462 D.R. = 0.74
[  NORMAL ]  Iteration 8:  k_eff = 0.659481   res = 1.471E-03  delta-k (pcm) =
[  NORMAL ]  ...  -82 D.R. = 0.79
[  NORMAL ]  Iteration 9:  k_eff = 0.661799   res = 1.248E-03  delta-k (pcm) =
[  NORMAL ]  ...  231 D.R. = 0.85
[  NORMAL ]  Iteration 10:  k_eff = 0.666690   res = 1.123E-03  delta-k (pcm) =
[  NORMAL ]  ...  489 D.R. = 0.90
[  NORMAL ]  Iteration 11:  k_eff = 0.673664   res = 1.049E-03  delta-k (pcm) =
[  NORMAL ]  ...  697 D.R. = 0.93
[  NORMAL ]  Iteration 12:  k_eff = 0.682301   res = 1.001E-03  delta-k (pcm) =
[  NORMAL ]  ...  863 D.R. = 0.95
[  NORMAL ]  Iteration 13:  k_eff = 0.692239   res = 9.638E-04  delta-k (pcm) =
[  NORMAL ]  ...  993 D.R. = 0.96
[  NORMAL ]  Iteration 14:  k_eff = 0.703171   res = 9.329E-04  delta-k (pcm) =
[  NORMAL ]  ...  1093 D.R. = 0.97
[  NORMAL ]  Iteration 15:  k_eff = 0.714835   res = 9.055E-04  delta-k (pcm) =
[  NORMAL ]  ...  1166 D.R. = 0.97
[  NORMAL ]  Iteration 16:  k_eff = 0.727008   res = 8.803E-04  delta-k (pcm) =
[  NORMAL ]  ...  1217 D.R. = 0.97
[  NORMAL ]  Iteration 17:  k_eff = 0.739503   res = 8.566E-04  delta-k (pcm) =
[  NORMAL ]  ...  1249 D.R. = 0.97
[  NORMAL ]  Iteration 18:  k_eff = 0.752162   res = 8.335E-04  delta-k (pcm) =
[  NORMAL ]  ...  1265 D.R. = 0.97
[  NORMAL ]  Iteration 19:  k_eff = 0.764855   res = 8.108E-04  delta-k (pcm) =
[  NORMAL ]  ...  1269 D.R. = 0.97
[  NORMAL ]  Iteration 20:  k_eff = 0.777472   res = 7.879E-04  delta-k (pcm) =
[  NORMAL ]  ...  1261 D.R. = 0.97
[  NORMAL ]  Iteration 21:  k_eff = 0.789924   res = 7.647E-04  delta-k (pcm) =
[  NORMAL ]  ...  1245 D.R. = 0.97
[  NORMAL ]  Iteration 22:  k_eff = 0.802140   res = 7.410E-04  delta-k (pcm) =
[  NORMAL ]  ...  1221 D.R. = 0.97
[  NORMAL ]  Iteration 23:  k_eff = 0.814061   res = 7.168E-04  delta-k (pcm) =
[  NORMAL ]  ...  1192 D.R. = 0.97
[  NORMAL ]  Iteration 24:  k_eff = 0.825643   res = 6.922E-04  delta-k (pcm) =
[  NORMAL ]  ...  1158 D.R. = 0.97
[  NORMAL ]  Iteration 25:  k_eff = 0.836850   res = 6.672E-04  delta-k (pcm) =
[  NORMAL ]  ...  1120 D.R. = 0.96
[  NORMAL ]  Iteration 26:  k_eff = 0.847658   res = 6.419E-04  delta-k (pcm) =
[  NORMAL ]  ...  1080 D.R. = 0.96
[  NORMAL ]  Iteration 27:  k_eff = 0.858047   res = 6.165E-04  delta-k (pcm) =
[  NORMAL ]  ...  1038 D.R. = 0.96
[  NORMAL ]  Iteration 28:  k_eff = 0.868008   res = 5.911E-04  delta-k (pcm) =
[  NORMAL ]  ...  996 D.R. = 0.96
[  NORMAL ]  Iteration 29:  k_eff = 0.877535   res = 5.658E-04  delta-k (pcm) =
[  NORMAL ]  ...  952 D.R. = 0.96
[  NORMAL ]  Iteration 30:  k_eff = 0.886625   res = 5.409E-04  delta-k (pcm) =
[  NORMAL ]  ...  909 D.R. = 0.96
[  NORMAL ]  Iteration 31:  k_eff = 0.895281   res = 5.163E-04  delta-k (pcm) =
[  NORMAL ]  ...  865 D.R. = 0.95
[  NORMAL ]  Iteration 32:  k_eff = 0.903509   res = 4.921E-04  delta-k (pcm) =
[  NORMAL ]  ...  822 D.R. = 0.95
[  NORMAL ]  Iteration 33:  k_eff = 0.911317   res = 4.685E-04  delta-k (pcm) =
[  NORMAL ]  ...  780 D.R. = 0.95
[  NORMAL ]  Iteration 34:  k_eff = 0.918715   res = 4.456E-04  delta-k (pcm) =
[  NORMAL ]  ...  739 D.R. = 0.95
[  NORMAL ]  Iteration 35:  k_eff = 0.925715   res = 4.232E-04  delta-k (pcm) =
[  NORMAL ]  ...  699 D.R. = 0.95
[  NORMAL ]  Iteration 36:  k_eff = 0.932329   res = 4.016E-04  delta-k (pcm) =
[  NORMAL ]  ...  661 D.R. = 0.95
[  NORMAL ]  Iteration 37:  k_eff = 0.938571   res = 3.807E-04  delta-k (pcm) =
[  NORMAL ]  ...  624 D.R. = 0.95
[  NORMAL ]  Iteration 38:  k_eff = 0.944455   res = 3.606E-04  delta-k (pcm) =
[  NORMAL ]  ...  588 D.R. = 0.95
[  NORMAL ]  Iteration 39:  k_eff = 0.949996   res = 3.413E-04  delta-k (pcm) =
[  NORMAL ]  ...  554 D.R. = 0.95
[  NORMAL ]  Iteration 40:  k_eff = 0.955210   res = 3.227E-04  delta-k (pcm) =
[  NORMAL ]  ...  521 D.R. = 0.95
[  NORMAL ]  Iteration 41:  k_eff = 0.960110   res = 3.049E-04  delta-k (pcm) =
[  NORMAL ]  ...  490 D.R. = 0.94
[  NORMAL ]  Iteration 42:  k_eff = 0.964713   res = 2.880E-04  delta-k (pcm) =
[  NORMAL ]  ...  460 D.R. = 0.94
[  NORMAL ]  Iteration 43:  k_eff = 0.969032   res = 2.717E-04  delta-k (pcm) =
[  NORMAL ]  ...  431 D.R. = 0.94
[  NORMAL ]  Iteration 44:  k_eff = 0.973082   res = 2.562E-04  delta-k (pcm) =
[  NORMAL ]  ...  405 D.R. = 0.94
[  NORMAL ]  Iteration 45:  k_eff = 0.976877   res = 2.414E-04  delta-k (pcm) =
[  NORMAL ]  ...  379 D.R. = 0.94
[  NORMAL ]  Iteration 46:  k_eff = 0.980431   res = 2.274E-04  delta-k (pcm) =
[  NORMAL ]  ...  355 D.R. = 0.94
[  NORMAL ]  Iteration 47:  k_eff = 0.983757   res = 2.140E-04  delta-k (pcm) =
[  NORMAL ]  ...  332 D.R. = 0.94
[  NORMAL ]  Iteration 48:  k_eff = 0.986869   res = 2.014E-04  delta-k (pcm) =
[  NORMAL ]  ...  311 D.R. = 0.94
[  NORMAL ]  Iteration 49:  k_eff = 0.989777   res = 1.894E-04  delta-k (pcm) =
[  NORMAL ]  ...  290 D.R. = 0.94
[  NORMAL ]  Iteration 50:  k_eff = 0.992495   res = 1.780E-04  delta-k (pcm) =
[  NORMAL ]  ...  271 D.R. = 0.94
[  NORMAL ]  Iteration 51:  k_eff = 0.995033   res = 1.672E-04  delta-k (pcm) =
[  NORMAL ]  ...  253 D.R. = 0.94
[  NORMAL ]  Iteration 52:  k_eff = 0.997402   res = 1.569E-04  delta-k (pcm) =
[  NORMAL ]  ...  236 D.R. = 0.94
[  NORMAL ]  Iteration 53:  k_eff = 0.999613   res = 1.473E-04  delta-k (pcm) =
[  NORMAL ]  ...  221 D.R. = 0.94
[  NORMAL ]  Iteration 54:  k_eff = 1.001675   res = 1.382E-04  delta-k (pcm) =
[  NORMAL ]  ...  206 D.R. = 0.94
[  NORMAL ]  Iteration 55:  k_eff = 1.003597   res = 1.296E-04  delta-k (pcm) =
[  NORMAL ]  ...  192 D.R. = 0.94
[  NORMAL ]  Iteration 56:  k_eff = 1.005388   res = 1.215E-04  delta-k (pcm) =
[  NORMAL ]  ...  179 D.R. = 0.94
[  NORMAL ]  Iteration 57:  k_eff = 1.007057   res = 1.138E-04  delta-k (pcm) =
[  NORMAL ]  ...  166 D.R. = 0.94
[  NORMAL ]  Iteration 58:  k_eff = 1.008612   res = 1.066E-04  delta-k (pcm) =
[  NORMAL ]  ...  155 D.R. = 0.94
[  NORMAL ]  Iteration 59:  k_eff = 1.010059   res = 9.980E-05  delta-k (pcm) =
[  NORMAL ]  ...  144 D.R. = 0.94
[  NORMAL ]  Iteration 60:  k_eff = 1.011406   res = 9.342E-05  delta-k (pcm) =
[  NORMAL ]  ...  134 D.R. = 0.94
[  NORMAL ]  Iteration 61:  k_eff = 1.012659   res = 8.740E-05  delta-k (pcm) =
[  NORMAL ]  ...  125 D.R. = 0.94
[  NORMAL ]  Iteration 62:  k_eff = 1.013825   res = 8.175E-05  delta-k (pcm) =
[  NORMAL ]  ...  116 D.R. = 0.94
[  NORMAL ]  Iteration 63:  k_eff = 1.014909   res = 7.642E-05  delta-k (pcm) =
[  NORMAL ]  ...  108 D.R. = 0.93
[  NORMAL ]  Iteration 64:  k_eff = 1.015917   res = 7.142E-05  delta-k (pcm) =
[  NORMAL ]  ...  100 D.R. = 0.93
[  NORMAL ]  Iteration 65:  k_eff = 1.016853   res = 6.675E-05  delta-k (pcm) =
[  NORMAL ]  ...  93 D.R. = 0.93
[  NORMAL ]  Iteration 66:  k_eff = 1.017724   res = 6.235E-05  delta-k (pcm) =
[  NORMAL ]  ...  87 D.R. = 0.93
[  NORMAL ]  Iteration 67:  k_eff = 1.018533   res = 5.822E-05  delta-k (pcm) =
[  NORMAL ]  ...  80 D.R. = 0.93
[  NORMAL ]  Iteration 68:  k_eff = 1.019284   res = 5.436E-05  delta-k (pcm) =
[  NORMAL ]  ...  75 D.R. = 0.93
[  NORMAL ]  Iteration 69:  k_eff = 1.019982   res = 5.074E-05  delta-k (pcm) =
[  NORMAL ]  ...  69 D.R. = 0.93
[  NORMAL ]  Iteration 70:  k_eff = 1.020630   res = 4.734E-05  delta-k (pcm) =
[  NORMAL ]  ...  64 D.R. = 0.93
[  NORMAL ]  Iteration 71:  k_eff = 1.021232   res = 4.417E-05  delta-k (pcm) =
[  NORMAL ]  ...  60 D.R. = 0.93
[  NORMAL ]  Iteration 72:  k_eff = 1.021790   res = 4.119E-05  delta-k (pcm) =
[  NORMAL ]  ...  55 D.R. = 0.93
[  NORMAL ]  Iteration 73:  k_eff = 1.022308   res = 3.841E-05  delta-k (pcm) =
[  NORMAL ]  ...  51 D.R. = 0.93
[  NORMAL ]  Iteration 74:  k_eff = 1.022789   res = 3.579E-05  delta-k (pcm) =
[  NORMAL ]  ...  48 D.R. = 0.93
[  NORMAL ]  Iteration 75:  k_eff = 1.023235   res = 3.336E-05  delta-k (pcm) =
[  NORMAL ]  ...  44 D.R. = 0.93
[  NORMAL ]  Iteration 76:  k_eff = 1.023648   res = 3.107E-05  delta-k (pcm) =
[  NORMAL ]  ...  41 D.R. = 0.93
[  NORMAL ]  Iteration 77:  k_eff = 1.024032   res = 2.897E-05  delta-k (pcm) =
[  NORMAL ]  ...  38 D.R. = 0.93
[  NORMAL ]  Iteration 78:  k_eff = 1.024388   res = 2.696E-05  delta-k (pcm) =
[  NORMAL ]  ...  35 D.R. = 0.93
[  NORMAL ]  Iteration 79:  k_eff = 1.024718   res = 2.510E-05  delta-k (pcm) =
[  NORMAL ]  ...  32 D.R. = 0.93
[  NORMAL ]  Iteration 80:  k_eff = 1.025024   res = 2.338E-05  delta-k (pcm) =
[  NORMAL ]  ...  30 D.R. = 0.93
[  NORMAL ]  Iteration 81:  k_eff = 1.025307   res = 2.175E-05  delta-k (pcm) =
[  NORMAL ]  ...  28 D.R. = 0.93
[  NORMAL ]  Iteration 82:  k_eff = 1.025570   res = 2.025E-05  delta-k (pcm) =
[  NORMAL ]  ...  26 D.R. = 0.93
[  NORMAL ]  Iteration 83:  k_eff = 1.025814   res = 1.886E-05  delta-k (pcm) =
[  NORMAL ]  ...  24 D.R. = 0.93
[  NORMAL ]  Iteration 84:  k_eff = 1.026039   res = 1.752E-05  delta-k (pcm) =
[  NORMAL ]  ...  22 D.R. = 0.93
[  NORMAL ]  Iteration 85:  k_eff = 1.026249   res = 1.628E-05  delta-k (pcm) =
[  NORMAL ]  ...  20 D.R. = 0.93
[  NORMAL ]  Iteration 86:  k_eff = 1.026442   res = 1.517E-05  delta-k (pcm) =
[  NORMAL ]  ...  19 D.R. = 0.93
[  NORMAL ]  Iteration 87:  k_eff = 1.026622   res = 1.408E-05  delta-k (pcm) =
[  NORMAL ]  ...  17 D.R. = 0.93
[  NORMAL ]  Iteration 88:  k_eff = 1.026788   res = 1.308E-05  delta-k (pcm) =
[  NORMAL ]  ...  16 D.R. = 0.93
[  NORMAL ]  Iteration 89:  k_eff = 1.026942   res = 1.218E-05  delta-k (pcm) =
[  NORMAL ]  ...  15 D.R. = 0.93
[  NORMAL ]  Iteration 90:  k_eff = 1.027085   res = 1.132E-05  delta-k (pcm) =
[  NORMAL ]  ...  14 D.R. = 0.93
[  NORMAL ]  Iteration 91:  k_eff = 1.027217   res = 1.049E-05  delta-k (pcm) =
[  NORMAL ]  ...  13 D.R. = 0.93
[  NORMAL ]  Iteration 92:  k_eff = 1.027339   res = 9.760E-06  delta-k (pcm) =
[  NORMAL ]  ...  12 D.R. = 0.93
[  NORMAL ]  Iteration 93:  k_eff = 1.027453   res = 9.076E-06  delta-k (pcm) =
[  NORMAL ]  ...  11 D.R. = 0.93
[  NORMAL ]  Iteration 94:  k_eff = 1.027557   res = 8.434E-06  delta-k (pcm) =
[  NORMAL ]  ...  10 D.R. = 0.93
[  NORMAL ]  Iteration 95:  k_eff = 1.027655   res = 7.827E-06  delta-k (pcm) =
[  NORMAL ]  ...  9 D.R. = 0.93
[  NORMAL ]  Iteration 96:  k_eff = 1.027744   res = 7.266E-06  delta-k (pcm) =
[  NORMAL ]  ...  8 D.R. = 0.93
[  NORMAL ]  Iteration 97:  k_eff = 1.027828   res = 6.737E-06  delta-k (pcm) =
[  NORMAL ]  ...  8 D.R. = 0.93
[  NORMAL ]  Iteration 98:  k_eff = 1.027905   res = 6.255E-06  delta-k (pcm) =
[  NORMAL ]  ...  7 D.R. = 0.93
[  NORMAL ]  Iteration 99:  k_eff = 1.027976   res = 5.803E-06  delta-k (pcm) =
[  NORMAL ]  ...  7 D.R. = 0.93
[  NORMAL ]  Iteration 100:  k_eff = 1.028042   res = 5.383E-06  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.93
[  NORMAL ]  Iteration 101:  k_eff = 1.028103   res = 5.017E-06  delta-k (pcm)
[  NORMAL ]  ...  = 6 D.R. = 0.93
[  NORMAL ]  Iteration 102:  k_eff = 1.028160   res = 4.618E-06  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.92
[  NORMAL ]  Iteration 103:  k_eff = 1.028212   res = 4.306E-06  delta-k (pcm)
[  NORMAL ]  ...  = 5 D.R. = 0.93
[  NORMAL ]  Iteration 104:  k_eff = 1.028260   res = 3.999E-06  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.93
[  NORMAL ]  Iteration 105:  k_eff = 1.028305   res = 3.706E-06  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.93
[  NORMAL ]  Iteration 106:  k_eff = 1.028347   res = 3.429E-06  delta-k (pcm)
[  NORMAL ]  ...  = 4 D.R. = 0.93
[  NORMAL ]  Iteration 107:  k_eff = 1.028385   res = 3.213E-06  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.94
[  NORMAL ]  Iteration 108:  k_eff = 1.028420   res = 2.943E-06  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.92
[  NORMAL ]  Iteration 109:  k_eff = 1.028453   res = 2.740E-06  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.93
[  NORMAL ]  Iteration 110:  k_eff = 1.028484   res = 2.531E-06  delta-k (pcm)
[  NORMAL ]  ...  = 3 D.R. = 0.92
[  NORMAL ]  Iteration 111:  k_eff = 1.028512   res = 2.369E-06  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.94
[  NORMAL ]  Iteration 112:  k_eff = 1.028538   res = 2.186E-06  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.92
[  NORMAL ]  Iteration 113:  k_eff = 1.028562   res = 2.026E-06  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.93
[  NORMAL ]  Iteration 114:  k_eff = 1.028584   res = 1.858E-06  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.92
[  NORMAL ]  Iteration 115:  k_eff = 1.028604   res = 1.760E-06  delta-k (pcm)
[  NORMAL ]  ...  = 2 D.R. = 0.95
[  NORMAL ]  Iteration 116:  k_eff = 1.028623   res = 1.612E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.92
[  NORMAL ]  Iteration 117:  k_eff = 1.028641   res = 1.496E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.93
[  NORMAL ]  Iteration 118:  k_eff = 1.028657   res = 1.382E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.92
[  NORMAL ]  Iteration 119:  k_eff = 1.028672   res = 1.293E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.94
[  NORMAL ]  Iteration 120:  k_eff = 1.028686   res = 1.191E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.92
[  NORMAL ]  Iteration 121:  k_eff = 1.028699   res = 1.112E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.93
[  NORMAL ]  Iteration 122:  k_eff = 1.028711   res = 1.005E-06  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.90
[  NORMAL ]  Iteration 123:  k_eff = 1.028722   res = 9.443E-07  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.94
[  NORMAL ]  Iteration 124:  k_eff = 1.028732   res = 8.583E-07  delta-k (pcm)
[  NORMAL ]  ...  = 1 D.R. = 0.91
[  NORMAL ]  Iteration 125:  k_eff = 1.028742   res = 8.028E-07  delta-k (pcm)
[  NORMAL ]  ...  = 0 D.R. = 0.94

We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.

[37]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined.nominal_value
bias = (openmoc_keff - openmc_keff) * 1e5

print('openmc keff = {0:1.6f}'.format(openmc_keff))
print('openmoc keff = {0:1.6f}'.format(openmoc_keff))
print('bias [pcm]: {0:1.1f}'.format(bias))
openmc keff = 1.024078
openmoc keff = 1.028742
bias [pcm]: 466.4

There is a non-trivial bias between the eigenvalues computed by OpenMC and OpenMOC. One can show that these biases do not converge to <100 pcm with more particle histories. For heterogeneous geometries, additional measures must be taken to address the following three sources of bias:

  • Appropriate transport-corrected cross sections
  • Spatial discretization of OpenMOC’s mesh
  • Constant-in-angle multi-group cross sections
Flux and Pin Power Visualizations

We will conclude this tutorial by illustrating how to visualize the fission rates computed by OpenMOC and OpenMC. First, we extract volume-integrated fission rates from OpenMC’s mesh fission rate tally for each pin cell in the fuel assembly.

[38]:
# Get the OpenMC fission rate mesh tally data
mesh_tally = sp.get_tally(name='mesh tally')
openmc_fission_rates = mesh_tally.get_values(scores=['nu-fission'])

# Reshape array to 2D for plotting
openmc_fission_rates.shape = (17,17)

# Normalize to the average pin power
openmc_fission_rates /= np.mean(openmc_fission_rates[openmc_fission_rates > 0.])

Next, we extract OpenMOC’s volume-averaged fission rates into a 2D 17x17 NumPy array.

[39]:
# Create OpenMOC Mesh on which to tally fission rates
openmoc_mesh = openmoc.process.Mesh()
openmoc_mesh.dimension = np.array(mesh.dimension)
openmoc_mesh.lower_left = np.array(mesh.lower_left)
openmoc_mesh.upper_right = np.array(mesh.upper_right)
openmoc_mesh.width = openmoc_mesh.upper_right - openmoc_mesh.lower_left
openmoc_mesh.width /= openmoc_mesh.dimension

# Tally OpenMOC fission rates on the Mesh
openmoc_fission_rates = openmoc_mesh.tally_fission_rates(solver)
openmoc_fission_rates = np.squeeze(openmoc_fission_rates)
openmoc_fission_rates = np.fliplr(openmoc_fission_rates)

# Normalize to the average pin fission rate
openmoc_fission_rates /= np.mean(openmoc_fission_rates[openmoc_fission_rates > 0.])

Now we can easily use Matplotlib to visualize the fission rates from OpenMC and OpenMOC side-by-side.

[40]:
# Ignore zero fission rates in guide tubes with Matplotlib color scheme
openmc_fission_rates[openmc_fission_rates == 0] = np.nan
openmoc_fission_rates[openmoc_fission_rates == 0] = np.nan

# Plot OpenMC's fission rates in the left subplot
fig = plt.subplot(121)
plt.imshow(openmc_fission_rates, interpolation='none', cmap='jet')
plt.title('OpenMC Fission Rates')

# Plot OpenMOC's fission rates in the right subplot
fig2 = plt.subplot(122)
plt.imshow(openmoc_fission_rates, interpolation='none', cmap='jet')
plt.title('OpenMOC Fission Rates')
[40]:
Text(0.5, 1.0, 'OpenMOC Fission Rates')
_images/examples_mgxs-part-iii_83_1.png

Multigroup (Delayed) Cross Section Generation Part I: Introduction

This IPython Notebook introduces the use of the openmc.mgxs module to calculate multi-energy-group and multi-delayed-group cross sections for an infinite homogeneous medium. In particular, this Notebook introduces the the following features:

  • Creation of multi-delayed-group cross sections for an infinite homogeneous medium
  • Calculation of delayed neutron precursor concentrations
Introduction to Multi-Delayed-Group Cross Sections (MDGXS)

Many Monte Carlo particle transport codes, including OpenMC, use continuous-energy nuclear cross section data. However, most deterministic neutron transport codes use multi-group cross sections defined over discretized energy bins or energy groups. Furthermore, kinetics calculations typically separate out parameters that involve delayed neutrons into prompt and delayed components and further subdivide delayed components by delayed groups. An example is the energy spectrum for prompt and delayed neutrons for U-235 and Pu-239 computed for a light water reactor spectrum.

[1]:
from IPython.display import Image
Image(filename='images/mdgxs.png', width=350)
[1]:
_images/examples_mdgxs-part-i_3_0.png

A variety of tools employing different methodologies have been developed over the years to compute multi-group cross sections for certain applications, including NJOY (LANL), MC\(^2\)-3 (ANL), and Serpent (VTT). The openmc.mgxs Python module is designed to leverage OpenMC’s tally system to calculate multi-group cross sections with arbitrary energy discretizations and different delayed group models (e.g. 6, 7, or 8 delayed group models) for fine-mesh heterogeneous deterministic neutron transport applications.

Before proceeding to illustrate how one may use the openmc.mgxs module, it is worthwhile to define the general equations used to calculate multi-energy-group and multi-delayed-group cross sections. This is only intended as a brief overview of the methodology used by openmc.mgxs - we refer the interested reader to the large body of literature on the subject for a more comprehensive understanding of this complex topic.

Introductory Notation

The continuous real-valued microscopic cross section may be denoted \(\sigma_{n,x}(\mathbf{r}, E)\) for position vector \(\mathbf{r}\), energy \(E\), nuclide \(n\) and interaction type \(x\). Similarly, the scalar neutron flux may be denoted by \(\Phi(\mathbf{r},E)\) for position \(\mathbf{r}\) and energy \(E\). Note: Although nuclear cross sections are dependent on the temperature \(T\) of the interacting medium, the temperature variable is neglected here for brevity.

Spatial and Energy Discretization

The energy domain for critical systems such as thermal reactors spans more than 10 orders of magnitude of neutron energies from 10\(^{-5}\) - 10\(^7\) eV. The multi-group approximation discretization divides this energy range into one or more energy groups. In particular, for \(G\) total groups, we denote an energy group index \(g\) such that \(g \in \{1, 2, ..., G\}\). The energy group indices are defined such that the smaller group the higher the energy, and vice versa. The integration over neutron energies across a discrete energy group is commonly referred to as energy condensation.

The delayed neutrons created from fissions are created from > 30 delayed neutron precursors. Modeling each of the delayed neutron precursors is possible, but this approach has not recieved much attention due to large uncertainties in certain precursors. Therefore, the delayed neutrons are often combined into “delayed groups” that have a set time constant, \(\lambda_d\). Some cross section libraries use the same group time constants for all nuclides (e.g. JEFF 3.1) while other libraries use different time constants for all nuclides (e.g. ENDF/B-VII.1). Multi-delayed-group cross sections can either be created with the entire delayed group set, a subset of delayed groups, or integrated over all delayed groups.

Multi-group cross sections are computed for discretized spatial zones in the geometry of interest. The spatial zones may be defined on a structured and regular fuel assembly or pin cell mesh, an arbitrary unstructured mesh or the constructive solid geometry used by OpenMC. For a geometry with \(K\) distinct spatial zones, we designate each spatial zone an index \(k\) such that \(k \in \{1, 2, ..., K\}\). The volume of each spatial zone is denoted by \(V_{k}\). The integration over discrete spatial zones is commonly referred to as spatial homogenization.

General Scalar-Flux Weighted MDGXS

The multi-group cross sections computed by openmc.mgxs are defined as a scalar flux-weighted average of the microscopic cross sections across each discrete energy group. This formulation is employed in order to preserve the reaction rates within each energy group and spatial zone. In particular, spatial homogenization and energy condensation are used to compute the general multi-group cross section. For instance, the delayed-nu-fission multi-energy-group and multi-delayed-group cross section, \(\nu_d \sigma_{f,x,k,g}\), can be computed as follows:

\[\nu_d \sigma_{n,x,k,g} = \frac{\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r} \nu_d \sigma_{f,x}(\mathbf{r},E')\Phi(\mathbf{r},E')}{\int_{E_{g}}^{E_{g-1}}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\Phi(\mathbf{r},E')} \]

This scalar flux-weighted average microscopic cross section is computed by openmc.mgxs for only the delayed-nu-fission and delayed neutron fraction reaction type at the moment. These double integrals are stochastically computed with OpenMC’s tally system - in particular, filters on the energy range and spatial zone (material, cell, universe, or mesh) define the bounds of integration for both numerator and denominator.

Multi-Group Prompt and Delayed Fission Spectrum

The energy spectrum of neutrons emitted from fission is denoted by \(\chi_{n}(\mathbf{r},E' \rightarrow E'')\) for incoming and outgoing energies \(E'\) and \(E''\), respectively. Unlike the multi-group cross sections \(\sigma_{n,x,k,g}\) considered up to this point, the fission spectrum is a probability distribution and must sum to unity. The outgoing energy is typically much less dependent on the incoming energy for fission than for scattering interactions. As a result, it is common practice to integrate over the incoming neutron energy when computing the multi-group fission spectrum. The fission spectrum may be simplified as \(\chi_{n}(\mathbf{r},E)\) with outgoing energy \(E\).

Computing the cumulative energy spectrum of emitted neutrons, \(\chi_{n}(\mathbf{r},E)\), has been presented in the mgxs-part-i.ipynb notebook. Here, we will present the energy spectrum of prompt and delayed emission neutrons, \(\chi_{n,p}(\mathbf{r},E)\) and \(\chi_{n,d}(\mathbf{r},E)\), respectively. Unlike the multi-group cross sections defined up to this point, the multi-group fission spectrum is weighted by the fission production rate rather than the scalar flux. This formulation is intended to preserve the total fission production rate in the multi-group deterministic calculation. In order to mathematically define the multi-group fission spectrum, we denote the microscopic fission cross section as \(\sigma_{n,f}(\mathbf{r},E)\) and the average number of neutrons emitted from fission interactions with nuclide \(n\) as \(\nu_{n,p}(\mathbf{r},E)\) and \(\nu_{n,d}(\mathbf{r},E)\) for prompt and delayed neutrons, respectively. The multi-group fission spectrum \(\chi_{n,k,g,d}\) is then the probability of fission neutrons emitted into energy group \(g\) and delayed group \(d\). There are not prompt groups, so inserting \(p\) in place of \(d\) just denotes all prompt neutrons.

Similar to before, spatial homogenization and energy condensation are used to find the multi-energy-group and multi-delayed-group fission spectrum \(\chi_{n,k,g,d}\) as follows:

\[\chi_{n,k,g',d} = \frac{\int_{E_{g'}}^{E_{g'-1}}\mathrm{d}E''\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\chi_{n,d}(\mathbf{r},E'\rightarrow E'')\nu_{n,d}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')}{\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\nu_{n,d}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')} \]

The fission production-weighted multi-energy-group and multi-delayed-group fission spectrum for delayed neutrons is computed using OpenMC tallies with energy in, energy out, and delayed group filters. Alternatively, the delayed group filter can be omitted to compute the fission spectrum integrated over all delayed groups.

This concludes our brief overview on the methodology to compute multi-energy-group and multi-delayed-group cross sections. The following sections detail more concretely how users may employ the openmc.mgxs module to power simulation workflows requiring multi-group cross sections for downstream deterministic calculations.

Generate Input Files
[2]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

import openmc
import openmc.mgxs as mgxs

First we need to define materials that will be used in the problem. Let’s create a material for the homogeneous medium.

[3]:
# Instantiate a Material and register the Nuclides
inf_medium = openmc.Material(name='moderator')
inf_medium.set_density('g/cc', 5.)
inf_medium.add_nuclide('H1',  0.03)
inf_medium.add_nuclide('O16', 0.015)
inf_medium.add_nuclide('U235', 0.0001)
inf_medium.add_nuclide('U238', 0.007)
inf_medium.add_nuclide('Pu239', 0.00003)
inf_medium.add_nuclide('Zr90', 0.002)

With our material, we can now create a Materials object that can be exported to an actual XML file.

[4]:
# Instantiate a Materials collection and export to XML
materials_file = openmc.Materials([inf_medium])
materials_file.export_to_xml()

Now let’s move on to the geometry. This problem will be a simple square cell with reflective boundary conditions to simulate an infinite homogeneous medium. The first step is to create the outer bounding surfaces of the problem.

[5]:
# Instantiate boundary Planes
min_x = openmc.XPlane(boundary_type='reflective', x0=-0.63)
max_x = openmc.XPlane(boundary_type='reflective', x0=0.63)
min_y = openmc.YPlane(boundary_type='reflective', y0=-0.63)
max_y = openmc.YPlane(boundary_type='reflective', y0=0.63)

With the surfaces defined, we can now create a cell that is defined by intersections of half-spaces created by the surfaces.

[6]:
# Instantiate a Cell
cell = openmc.Cell(cell_id=1, name='cell')

# Register bounding Surfaces with the Cell
cell.region = +min_x & -max_x & +min_y & -max_y

# Fill the Cell with the Material
cell.fill = inf_medium

We now must create a geometry and export it to XML.

[7]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry([cell])

# Export to "geometry.xml"
openmc_geometry.export_to_xml()

Next, we must define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 2500 particles.

[8]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 5000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()

Now we are ready to generate multi-group cross sections! First, let’s define a 100-energy-group structure and 1-energy-group structure using the built-in EnergyGroups class. We will also create a 6-delayed-group list.

[9]:
# Instantiate a 100-group EnergyGroups object
energy_groups = mgxs.EnergyGroups()
energy_groups.group_edges = np.logspace(-3, 7.3, 101)

# Instantiate a 1-group EnergyGroups object
one_group = mgxs.EnergyGroups()
one_group.group_edges = np.array([energy_groups.group_edges[0], energy_groups.group_edges[-1]])

delayed_groups = list(range(1,7))

We can now use the EnergyGroups object and delayed group list, along with our previously created materials and geometry, to instantiate some MGXS objects from the openmc.mgxs module. In particular, the following are subclasses of the generic and abstract MGXS class:

  • TotalXS
  • TransportXS
  • AbsorptionXS
  • CaptureXS
  • FissionXS
  • NuFissionMatrixXS
  • KappaFissionXS
  • ScatterXS
  • ScatterMatrixXS
  • Chi
  • InverseVelocity

A separate abstract MDGXS class is used for cross-sections and parameters that involve delayed neutrons. The subclasses of MDGXS include:

  • DelayedNuFissionXS
  • ChiDelayed
  • Beta
  • DecayRate

These classes provide us with an interface to generate the tally inputs as well as perform post-processing of OpenMC’s tally data to compute the respective multi-group cross sections.

In this case, let’s create the multi-group chi-prompt, chi-delayed, and prompt-nu-fission cross sections with our 100-energy-group structure and multi-group delayed-nu-fission and beta cross sections with our 100-energy-group and 6-delayed-group structures.

The prompt chi and nu-fission data can actually be gathered using the Chi and FissionXS classes, respectively, by passing in a value of True for the optional prompt parameter upon initialization.

[10]:
# Instantiate a few different sections
chi_prompt = mgxs.Chi(domain=cell, groups=energy_groups, by_nuclide=True, prompt=True)
prompt_nu_fission = mgxs.FissionXS(domain=cell, groups=energy_groups, by_nuclide=True, nu=True, prompt=True)
chi_delayed = mgxs.ChiDelayed(domain=cell, energy_groups=energy_groups, by_nuclide=True)
delayed_nu_fission = mgxs.DelayedNuFissionXS(domain=cell, energy_groups=energy_groups, delayed_groups=delayed_groups, by_nuclide=True)
beta = mgxs.Beta(domain=cell, energy_groups=energy_groups, delayed_groups=delayed_groups, by_nuclide=True)
decay_rate = mgxs.DecayRate(domain=cell, energy_groups=one_group, delayed_groups=delayed_groups, by_nuclide=True)

chi_prompt.nuclides = ['U235', 'Pu239']
prompt_nu_fission.nuclides = ['U235', 'Pu239']
chi_delayed.nuclides = ['U235', 'Pu239']
delayed_nu_fission.nuclides = ['U235', 'Pu239']
beta.nuclides = ['U235', 'Pu239']
decay_rate.nuclides = ['U235', 'Pu239']

Each multi-group cross section object stores its tallies in a Python dictionary called tallies. We can inspect the tallies in the dictionary for our Decay Rate object as follows.

[11]:
decay_rate.tallies
[11]:
OrderedDict([('delayed-nu-fission', Tally
                ID             =        1
                Name           =
                Filters        =        CellFilter, DelayedGroupFilter, EnergyFilter
                Nuclides       =        U235 Pu239
                Scores         =        ['delayed-nu-fission']
                Estimator      =        tracklength), ('decay-rate', Tally
                ID             =        2
                Name           =
                Filters        =        CellFilter, DelayedGroupFilter, EnergyFilter
                Nuclides       =        U235 Pu239
                Scores         =        ['decay-rate']
                Estimator      =        tracklength)])

The Beta object includes tracklength tallies for the ‘nu-fission’ and ‘delayed-nu-fission’ scores in the 100-energy-group and 6-delayed-group structure in cell 1. Now that each MGXS and MDGXS object contains the tallies that it needs, we must add these tallies to a Tallies object to generate the “tallies.xml” input file for OpenMC.

[12]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()

# Add chi-prompt tallies to the tallies file
tallies_file += chi_prompt.tallies.values()

# Add prompt-nu-fission tallies to the tallies file
tallies_file += prompt_nu_fission.tallies.values()

# Add chi-delayed tallies to the tallies file
tallies_file += chi_delayed.tallies.values()

# Add delayed-nu-fission tallies to the tallies file
tallies_file += delayed_nu_fission.tallies.values()

# Add beta tallies to the tallies file
tallies_file += beta.tallies.values()

# Add decay rate tallies to the tallies file
tallies_file += decay_rate.tallies.values()

# Export to "tallies.xml"
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=4.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=6.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=5.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=8.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=14.
  warn(msg, IDWarning)

Now we a have a complete set of inputs, so we can go ahead and run our simulation.

[13]:
# Run OpenMC
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-19 06:56:34
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading Pu239 from /opt/data/hdf5/nndc_hdf5_v15/Pu239.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for H1
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.21670
        2/1    1.24155
        3/1    1.21924
        4/1    1.22486
        5/1    1.21719
        6/1    1.24330
        7/1    1.22322
        8/1    1.24133
        9/1    1.21840
       10/1    1.25141
       11/1    1.21217
       12/1    1.25625    1.23421 +/- 0.02204
       13/1    1.22056    1.22966 +/- 0.01351
       14/1    1.21757    1.22664 +/- 0.01002
       15/1    1.24571    1.23045 +/- 0.00865
       16/1    1.26489    1.23619 +/- 0.00910
       17/1    1.22323    1.23434 +/- 0.00791
       18/1    1.26108    1.23768 +/- 0.00762
       19/1    1.23145    1.23699 +/- 0.00676
       20/1    1.23548    1.23684 +/- 0.00605
       21/1    1.20446    1.23390 +/- 0.00621
       22/1    1.20533    1.23152 +/- 0.00615
       23/1    1.22520    1.23103 +/- 0.00568
       24/1    1.18367    1.22765 +/- 0.00625
       25/1    1.23614    1.22821 +/- 0.00585
       26/1    1.23746    1.22879 +/- 0.00550
       27/1    1.23626    1.22923 +/- 0.00518
       28/1    1.21334    1.22835 +/- 0.00497
       29/1    1.25169    1.22958 +/- 0.00486
       30/1    1.25579    1.23089 +/- 0.00479
       31/1    1.23828    1.23124 +/- 0.00457
       32/1    1.26911    1.23296 +/- 0.00468
       33/1    1.20090    1.23157 +/- 0.00469
       34/1    1.28606    1.23384 +/- 0.00503
       35/1    1.23129    1.23374 +/- 0.00483
       36/1    1.22535    1.23341 +/- 0.00465
       37/1    1.20367    1.23231 +/- 0.00461
       38/1    1.22886    1.23219 +/- 0.00444
       39/1    1.24056    1.23248 +/- 0.00429
       40/1    1.25038    1.23307 +/- 0.00419
       41/1    1.21504    1.23249 +/- 0.00410
       42/1    1.20762    1.23171 +/- 0.00404
       43/1    1.20597    1.23093 +/- 0.00399
       44/1    1.24424    1.23133 +/- 0.00389
       45/1    1.24767    1.23179 +/- 0.00381
       46/1    1.22998    1.23174 +/- 0.00370
       47/1    1.26195    1.23256 +/- 0.00369
       48/1    1.23146    1.23253 +/- 0.00359
       49/1    1.22059    1.23222 +/- 0.00351
       50/1    1.24724    1.23260 +/- 0.00345
 Creating state point statepoint.50.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 4.7388e-01 seconds
   Reading cross sections          = 4.4709e-01 seconds
 Total time in simulation          = 3.9290e+01 seconds
   Time in transport only          = 3.9005e+01 seconds
   Time in inactive batches        = 1.4079e+00 seconds
   Time in active batches          = 3.7882e+01 seconds
   Time synchronizing fission bank = 1.8814e-02 seconds
     Sampling source sites         = 1.6376e-02 seconds
     SEND/RECV source sites        = 2.3626e-03 seconds
   Time accumulating tallies       = 8.3299e-04 seconds
 Total time for finalization       = 1.1533e-02 seconds
 Total time elapsed                = 3.9783e+01 seconds
 Calculation Rate (inactive)       = 35514.2 particles/second
 Calculation Rate (active)         = 5279.54 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.23256 +/- 0.00308
 k-effective (Track-length)  = 1.23260 +/- 0.00345
 k-effective (Absorption)    = 1.23111 +/- 0.00186
 Combined k-effective        = 1.23135 +/- 0.00184
 Leakage Fraction            = 0.00000 +/- 0.00000

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

[14]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.50.h5')

In addition to the statepoint file, our simulation also created a summary file which encapsulates information about the materials and geometry. By default, a Summary object is automatically linked when a StatePoint is loaded. This is necessary for the openmc.mgxs module to properly process the tally data.

The statepoint is now ready to be analyzed by our multi-group cross sections. We simply have to load the tallies from the StatePoint into each object as follows and our MGXS objects will compute the cross sections for us under-the-hood.

[15]:
# Load the tallies from the statepoint into each MGXS object
chi_prompt.load_from_statepoint(sp)
prompt_nu_fission.load_from_statepoint(sp)
chi_delayed.load_from_statepoint(sp)
delayed_nu_fission.load_from_statepoint(sp)
beta.load_from_statepoint(sp)
decay_rate.load_from_statepoint(sp)

Voila! Our multi-group cross sections are now ready to rock ‘n roll!

Extracting and Storing MGXS Data

Let’s first inspect our delayed-nu-fission section by printing it to the screen after condensing the cross section down to one group.

[16]:
delayed_nu_fission.get_condensed_xs(one_group).get_xs()
[16]:
array([[[5.14223507e-06, 1.16426087e-06]],

       [[2.65426350e-05, 7.58220468e-06]],

       [[2.53399053e-05, 5.73796202e-06]],

       [[5.68141581e-05, 1.04757933e-05]],

       [[2.32930026e-05, 5.45658817e-06]],

       [[9.75735783e-06, 1.65150949e-06]]])

Since the openmc.mgxs module uses tally arithmetic under-the-hood, the cross section is stored as a “derived” Tally object. This means that it can be queried and manipulated using all of the same methods supported for the Tally class in the OpenMC Python API. For example, we can construct a Pandas DataFrame of the multi-group cross section data.

[17]:
df = delayed_nu_fission.get_pandas_dataframe()
df.head(10)
[17]:
cell delayedgroup group in nuclide mean std. dev.
198 1 1 1 U235 9.345817e-08 6.616216e-08
199 1 1 1 Pu239 1.574816e-08 1.114955e-08
398 1 2 1 U235 4.824023e-07 3.415087e-07
399 1 2 1 Pu239 1.025593e-07 7.261101e-08
598 1 3 1 U235 4.605432e-07 3.260339e-07
599 1 3 1 Pu239 7.761348e-08 5.494962e-08
798 1 4 1 U235 1.032576e-06 7.309948e-07
799 1 4 1 Pu239 1.416989e-07 1.003215e-07
998 1 5 1 U235 4.233415e-07 2.996976e-07
999 1 5 1 Pu239 7.380753e-08 5.225504e-08
[18]:
df = decay_rate.get_pandas_dataframe()
df.head(12)
[18]:
cell delayedgroup group in nuclide mean std. dev.
0 1 1 1 U235 0.013336 0.000061
1 1 1 1 Pu239 0.013271 0.000053
2 1 2 1 U235 0.032739 0.000149
3 1 2 1 Pu239 0.030881 0.000123
4 1 3 1 U235 0.120780 0.000549
5 1 3 1 Pu239 0.113370 0.000451
6 1 4 1 U235 0.302780 0.001378
7 1 4 1 Pu239 0.292500 0.001163
8 1 5 1 U235 0.849490 0.003865
9 1 5 1 Pu239 0.857490 0.003411
10 1 6 1 U235 2.853000 0.012980
11 1 6 1 Pu239 2.729700 0.010858

Each multi-group cross section object can be easily exported to a variety of file formats, including CSV, Excel, and LaTeX for storage or data processing.

[19]:
beta.export_xs_data(filename='beta', format='excel')

The following code snippet shows how to export the chi-prompt and chi-delayed MGXS to the same HDF5 binary data store.

[20]:
chi_prompt.build_hdf5_store(filename='mdgxs', append=True)
chi_delayed.build_hdf5_store(filename='mdgxs', append=True)
Using Tally Arithmetic to Compute the Delayed Neutron Precursor Concentrations

Finally, we illustrate how one can leverage OpenMC’s tally arithmetic data processing feature with MGXS objects. The openmc.mgxs module uses tally arithmetic to compute multi-group cross sections with automated uncertainty propagation. Each MGXS object includes an xs_tally attribute which is a “derived” Tally based on the tallies needed to compute the cross section type of interest. These derived tallies can be used in subsequent tally arithmetic operations. For example, we can use tally artithmetic to compute the delayed neutron precursor concentrations using the Beta, DelayedNuFissionXS, and DecayRate objects. The delayed neutron precursor concentrations are modeled using the following equations:

\[\frac{\partial}{\partial t} C_{k,d} (t) = \int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r} \beta_{k,d} (t) \nu_d \sigma_{f,x}(\mathbf{r},E',t)\Phi(\mathbf{r},E',t) - \lambda_{d} C_{k,d} (t) \]
\[C_{k,d} (t=0) = \frac{1}{\lambda_{d}} \int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r} \beta_{k,d} (t=0) \nu_d \sigma_{f,x}(\mathbf{r},E',t=0)\Phi(\mathbf{r},E',t=0) \]

First, let’s investigate the decay rates for U235 and Pu235. The fraction of the delayed neutron precursors remaining as a function of time after fission for each delayed group and fissioning isotope have been plotted below.

[21]:
# Get the decay rate data
dr_tally = decay_rate.xs_tally
dr_u235 = dr_tally.get_values(nuclides=['U235']).flatten()
dr_pu239 = dr_tally.get_values(nuclides=['Pu239']).flatten()

# Compute the exponential decay of the precursors
time = np.logspace(-3,3)
dr_u235_points = np.exp(-np.outer(dr_u235, time))
dr_pu239_points = np.exp(-np.outer(dr_pu239, time))

# Create a plot of the fraction of the precursors remaining as a f(time)
colors = ['b', 'g', 'r', 'c', 'm', 'k']
legend = []
fig = plt.figure(figsize=(8,6))
for g,c in enumerate(colors):
    plt.semilogx(time, dr_u235_points [g,:], color=c, linestyle='--', linewidth=3)
    plt.semilogx(time, dr_pu239_points[g,:], color=c, linestyle=':' , linewidth=3)
    legend.append('U-235 $t_{1/2}$ = ' + '{0:1.2f} seconds'.format(np.log(2) / dr_u235[g]))
    legend.append('Pu-239 $t_{1/2}$ = ' + '{0:1.2f} seconds'.format(np.log(2) / dr_pu239[g]))

plt.title('Delayed Neutron Precursor Decay Rates')
plt.xlabel('Time (s)')
plt.ylabel('Fraction Remaining')
plt.legend(legend, loc=1, bbox_to_anchor=(1.55, 0.95))
[21]:
<matplotlib.legend.Legend at 0x14ac6a8999b0>
_images/examples_mdgxs-part-i_52_1.png

Now let’s compute the initial concentration of the delayed neutron precursors:

[22]:
# Use tally arithmetic to compute the precursor concentrations
precursor_conc = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) * \
    delayed_nu_fission.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) / \
    decay_rate.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True)

# Get the Pandas DataFrames for inspection
precursor_conc.get_pandas_dataframe()
[22]:
cell delayedgroup nuclide score mean std. dev.
0 1 1 U235 (((delayed-nu-fission / nu-fission) * (delayed... 8.779139e-08 4.658590e-10
1 1 1 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 7.149814e-09 3.559010e-11
2 1 2 U235 (((delayed-nu-fission / nu-fission) * (delayed... 9.527880e-07 5.055905e-09
3 1 2 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 1.303159e-07 6.486820e-10
4 1 3 U235 (((delayed-nu-fission / nu-fission) * (delayed... 2.353903e-07 1.249083e-09
5 1 3 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 2.032895e-08 1.011928e-10
6 1 4 U235 (((delayed-nu-fission / nu-fission) * (delayed... 4.720191e-07 2.504737e-09
7 1 4 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 2.626309e-08 1.307315e-10
8 1 5 U235 (((delayed-nu-fission / nu-fission) * (delayed... 2.827915e-08 1.500614e-10
9 1 5 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 2.430587e-09 1.209889e-11
10 1 6 U235 (((delayed-nu-fission / nu-fission) * (delayed... 1.477530e-09 7.840413e-12
11 1 6 Pu239 (((delayed-nu-fission / nu-fission) * (delayed... 6.994312e-11 3.481605e-13

We can plot the delayed neutron fractions for each nuclide.

[23]:
energy_filter = [f for f in beta.xs_tally.filters if type(f) is openmc.EnergyFilter]
beta_integrated = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True)
beta_u235  = beta_integrated.get_values(nuclides=['U235'])
beta_pu239 = beta_integrated.get_values(nuclides=['Pu239'])

# Reshape the betas
beta_u235.shape = (beta_u235.shape[0])
beta_pu239.shape = (beta_pu239.shape[0])

df = beta_integrated.summation(filter_type=openmc.DelayedGroupFilter, remove_filter=True).get_pandas_dataframe()
print('Beta (U-235) : {:.6f} +/- {:.6f}'.format(df[df['nuclide'] == 'U235']['mean'][0], df[df['nuclide'] == 'U235']['std. dev.'][0]))
print('Beta (Pu-239): {:.6f} +/- {:.6f}'.format(df[df['nuclide'] == 'Pu239']['mean'][1], df[df['nuclide'] == 'Pu239']['std. dev.'][1]))

beta_u235 = np.append(beta_u235[0], beta_u235)
beta_pu239 = np.append(beta_pu239[0], beta_pu239)

# Create a step plot for the MGXS
plt.plot(np.arange(0.5, 7.5, 1), beta_u235, drawstyle='steps', color='b', linewidth=3)
plt.plot(np.arange(0.5, 7.5, 1), beta_pu239, drawstyle='steps', color='g', linewidth=3)

plt.title('Delayed Neutron Fraction (beta)')
plt.xlabel('Delayed Group')
plt.ylabel('Beta(fraction total neutrons)')
plt.legend(['U-235', 'Pu-239'])
plt.xlim([0,7])
Beta (U-235) : 0.006504 +/- 0.000007
Beta (Pu-239): 0.002245 +/- 0.000002
[23]:
(0, 7)
_images/examples_mdgxs-part-i_56_2.png

We can also plot the energy spectrum for fission emission of prompt and delayed neutrons.

[24]:
chi_d_u235  = np.squeeze(chi_delayed.get_xs(nuclides=['U235'], order_groups='decreasing'))
chi_d_pu239 = np.squeeze(chi_delayed.get_xs(nuclides=['Pu239'], order_groups='decreasing'))
chi_p_u235  = np.squeeze(chi_prompt.get_xs(nuclides=['U235'], order_groups='decreasing'))
chi_p_pu239 = np.squeeze(chi_prompt.get_xs(nuclides=['Pu239'], order_groups='decreasing'))

chi_d_u235  = np.append(chi_d_u235 , chi_d_u235[0])
chi_d_pu239 = np.append(chi_d_pu239, chi_d_pu239[0])
chi_p_u235  = np.append(chi_p_u235 , chi_p_u235[0])
chi_p_pu239 = np.append(chi_p_pu239, chi_p_pu239[0])

# Create a step plot for the MGXS
plt.semilogx(energy_groups.group_edges, chi_d_u235 , drawstyle='steps', color='b', linestyle='--', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_d_pu239, drawstyle='steps', color='g', linestyle='--', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_p_u235 , drawstyle='steps', color='b', linestyle=':', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_p_pu239, drawstyle='steps', color='g', linestyle=':', linewidth=3)

plt.title('Energy Spectrum for Fission Neutrons')
plt.xlabel('Energy (eV)')
plt.ylabel('Fraction on emitted neutrons')
plt.legend(['U-235 delayed', 'Pu-239 delayed', 'U-235 prompt', 'Pu-239 prompt'],loc=2)
plt.xlim(1.0e3, 20.0e6)
[24]:
(1000.0, 20000000.0)
_images/examples_mdgxs-part-i_58_1.png

Multigroup (Delayed) Cross Section Generation Part II: Advanced Features

This IPython Notebook illustrates the use of the ``openmc.mgxs.Library`` class. The Library class is designed to automate the calculation of multi-group cross sections for use cases with one or more domains, cross section types, and/or nuclides. In particular, this Notebook illustrates the following features:

  • Calculation of multi-energy-group and multi-delayed-group cross sections for a fuel assembly
  • Automated creation, manipulation and storage of MGXS with ``openmc.mgxs.Library``
  • Steady-state pin-by-pin delayed neutron fractions (beta) for each delayed group.
  • Generation of surface currents on the interfaces and surfaces of a Mesh.
Generate Input Files
[1]:
%matplotlib inline
import math

import matplotlib.pyplot as plt
import numpy as np

import openmc
import openmc.mgxs

First we need to define materials that will be used in the problem: fuel, water, and cladding.

[2]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide('U235', 3.7503e-4)
fuel.add_nuclide('U238', 2.2625e-2)
fuel.add_nuclide('O16', 4.6007e-2)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_nuclide('H1', 4.9457e-2)
water.add_nuclide('O16', 2.4732e-2)
water.add_nuclide('B10', 8.0042e-6)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_nuclide('Zr90', 7.2758e-3)

With our three materials, we can now create a Materials object that can be exported to an actual XML file.

[3]:
# Create a materials collection and export to XML
materials = openmc.Materials((fuel, water, zircaloy))
materials.export_to_xml()

Now let’s move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC’s lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.

[4]:
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(r=0.39218)
clad_outer_radius = openmc.ZCylinder(r=0.45720)

# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='reflective')
min_y = openmc.YPlane(y0=-10.71, boundary_type='reflective')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10., boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10., boundary_type='reflective')

With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
fuel_pin_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
fuel_pin_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
fuel_pin_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
fuel_pin_universe.add_cell(moderator_cell)

Likewise, we can construct a control rod guide tube with the same surfaces.

[6]:
# Create a Universe to encapsulate a control rod guide tube
guide_tube_universe = openmc.Universe(name='Guide Tube')

# Create guide tube Cell
guide_tube_cell = openmc.Cell(name='Guide Tube Water')
guide_tube_cell.fill = water
guide_tube_cell.region = -fuel_outer_radius
guide_tube_universe.add_cell(guide_tube_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='Guide Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
guide_tube_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='Guide Tube Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
guide_tube_universe.add_cell(moderator_cell)

Using the pin cell universe, we can construct a 17x17 rectangular lattice with a 1.26 cm pitch.

[7]:
# Create fuel assembly Lattice
assembly = openmc.RectLattice(name='1.6% Fuel Assembly')
assembly.pitch = (1.26, 1.26)
assembly.lower_left = [-1.26 * 17. / 2.0] * 2

Next, we create a NumPy array of fuel pin and guide tube universes for the lattice.

[8]:
# Create array indices for guide tube locations in lattice
template_x = np.array([5, 8, 11, 3, 13, 2, 5, 8, 11, 14, 2, 5, 8,
                       11, 14, 2, 5, 8, 11, 14, 3, 13, 5, 8, 11])
template_y = np.array([2, 2, 2, 3, 3, 5, 5, 5, 5, 5, 8, 8, 8, 8,
                       8, 11, 11, 11, 11, 11, 13, 13, 14, 14, 14])

# Create universes array with the fuel pin and guide tube universes
universes = np.tile(fuel_pin_universe, (17,17))
universes[template_x, template_y] = guide_tube_universe

# Store the array of universes in the lattice
assembly.universes = universes

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.

[9]:
# Create root Cell
root_cell = openmc.Cell(name='root cell', fill=assembly)

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(root_cell)

We now must create a geometry that is assigned a root universe and export it to XML.

[10]:
# Create Geometry and export to XML
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 2500 particles.

[11]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 2500

# Instantiate a Settings object
settings = openmc.Settings()
settings.batches = batches
settings.inactive = inactive
settings.particles = particles
settings.output = {'tallies': False}

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings.export_to_xml()

Let us also create a plot to verify that our fuel assembly geometry was created successfully.

[12]:
# Plot our geometry
plot = openmc.Plot.from_geometry(geometry)
plot.pixels = (250, 250)
plot.color_by = 'material'
openmc.plot_inline(plot)
_images/examples_mdgxs-part-ii_24_0.png

As we can see from the plot, we have a nice array of fuel and guide tube pin cells with fuel, cladding, and water!

Create an MGXS Library

Now we are ready to generate multi-group cross sections! First, let’s define a 20-energy-group and 1-energy-group.

[13]:
# Instantiate a 20-group EnergyGroups object
energy_groups = openmc.mgxs.EnergyGroups()
energy_groups.group_edges = np.logspace(-3, 7.3, 21)

# Instantiate a 1-group EnergyGroups object
one_group = openmc.mgxs.EnergyGroups()
one_group.group_edges = np.array([energy_groups.group_edges[0], energy_groups.group_edges[-1]])

Next, we will instantiate an openmc.mgxs.Library for the energy and delayed groups with our the fuel assembly geometry.

[14]:
# Instantiate a tally mesh
mesh = openmc.RegularMesh(mesh_id=1)
mesh.dimension = [17, 17, 1]
mesh.lower_left = [-10.71, -10.71, -10000.]
mesh.width = [1.26, 1.26, 20000.]

# Initialize an 20-energy-group and 6-delayed-group MGXS Library
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups  = energy_groups
mgxs_lib.num_delayed_groups = 6

# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['total', 'transport', 'nu-scatter matrix', 'kappa-fission', 'inverse-velocity', 'chi-prompt',
                      'prompt-nu-fission', 'chi-delayed', 'delayed-nu-fission', 'beta']

# Specify a "mesh" domain type for the cross section tally filters
mgxs_lib.domain_type = 'mesh'

# Specify the mesh domain over which to compute multi-group cross sections
mgxs_lib.domains = [mesh]

# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()

# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)

# Instantiate a current tally
mesh_filter = openmc.MeshSurfaceFilter(mesh)
current_tally = openmc.Tally(name='current tally')
current_tally.scores = ['current']
current_tally.filters = [mesh_filter]

# Add current tally to the tallies file
tallies_file.append(current_tally)

# Export to "tallies.xml"
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=1.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=5.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=6.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=17.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=23.
  warn(msg, IDWarning)

Now, we can run OpenMC to generate the cross sections.

[15]:
# Run OpenMC
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | 61c911cffdae2406f9f4bc667a9a6954748bb70c
         Date/Time | 2019-07-18 22:07:58
    OpenMP Threads | 4

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U235 from /opt/data/hdf5/nndc_hdf5_v15/U235.h5
 Reading U238 from /opt/data/hdf5/nndc_hdf5_v15/U238.h5
 Reading O16 from /opt/data/hdf5/nndc_hdf5_v15/O16.h5
 Reading H1 from /opt/data/hdf5/nndc_hdf5_v15/H1.h5
 Reading B10 from /opt/data/hdf5/nndc_hdf5_v15/B10.h5
 Reading Zr90 from /opt/data/hdf5/nndc_hdf5_v15/Zr90.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Reading tallies XML file...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    1.03852
        2/1    0.99743
        3/1    1.02987
        4/1    1.04397
        5/1    1.06262
        6/1    1.06657
        7/1    0.98574
        8/1    1.04364
        9/1    1.01253
       10/1    1.02094
       11/1    0.99586
       12/1    1.00508    1.00047 +/- 0.00461
       13/1    1.05292    1.01795 +/- 0.01769
       14/1    1.04732    1.02530 +/- 0.01450
       15/1    1.04886    1.03001 +/- 0.01218
       16/1    1.00948    1.02659 +/- 0.01052
       17/1    1.02644    1.02657 +/- 0.00889
       18/1    1.03080    1.02710 +/- 0.00772
       19/1    1.00018    1.02411 +/- 0.00743
       20/1    1.05668    1.02736 +/- 0.00740
       21/1    1.01160    1.02593 +/- 0.00685
       22/1    1.04334    1.02738 +/- 0.00642
       23/1    1.03105    1.02766 +/- 0.00591
       24/1    1.01174    1.02653 +/- 0.00559
       25/1    0.99844    1.02465 +/- 0.00553
       26/1    1.02241    1.02451 +/- 0.00517
       27/1    1.02904    1.02478 +/- 0.00487
       28/1    1.02132    1.02459 +/- 0.00459
       29/1    1.01384    1.02402 +/- 0.00438
       30/1    1.03891    1.02477 +/- 0.00422
       31/1    1.04092    1.02553 +/- 0.00409
       32/1    1.00058    1.02440 +/- 0.00406
       33/1    0.99940    1.02331 +/- 0.00403
       34/1    0.98362    1.02166 +/- 0.00420
       35/1    1.05358    1.02294 +/- 0.00422
       36/1    0.99923    1.02202 +/- 0.00416
       37/1    1.08491    1.02435 +/- 0.00463
       38/1    1.01838    1.02414 +/- 0.00447
       39/1    0.98567    1.02281 +/- 0.00451
       40/1    1.05047    1.02374 +/- 0.00445
       41/1    1.01993    1.02361 +/- 0.00431
       42/1    1.01223    1.02326 +/- 0.00419
       43/1    1.06259    1.02445 +/- 0.00423
       44/1    1.01993    1.02432 +/- 0.00411
       45/1    0.99233    1.02340 +/- 0.00409
       46/1    0.98532    1.02234 +/- 0.00411
       47/1    1.02513    1.02242 +/- 0.00400
       48/1    1.01637    1.02226 +/- 0.00390
       49/1    1.03215    1.02251 +/- 0.00381
       50/1    1.01826    1.02241 +/- 0.00371
 Creating state point statepoint.50.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 4.2397e-01 seconds
   Reading cross sections          = 4.0321e-01 seconds
 Total time in simulation          = 2.0407e+01 seconds
   Time in transport only          = 2.0154e+01 seconds
   Time in inactive batches        = 1.0937e+00 seconds
   Time in active batches          = 1.9314e+01 seconds
   Time synchronizing fission bank = 7.8056e-03 seconds
     Sampling source sites         = 6.7223e-03 seconds
     SEND/RECV source sites        = 9.5783e-04 seconds
   Time accumulating tallies       = 9.2006e-02 seconds
 Total time for finalization       = 1.0890e-02 seconds
 Total time elapsed                = 2.0869e+01 seconds
 Calculation Rate (inactive)       = 22858.4 particles/second
 Calculation Rate (active)         = 5177.70 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.02207 +/- 0.00343
 k-effective (Track-length)  = 1.02241 +/- 0.00371
 k-effective (Absorption)    = 1.02408 +/- 0.00356
 Combined k-effective        = 1.02306 +/- 0.00307
 Leakage Fraction            = 0.00000 +/- 0.00000

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

[16]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.50.h5')

The statepoint is now ready to be analyzed by the Library. We simply have to load the tallies from the statepoint into the Library and our MGXS objects will compute the cross sections for us under-the-hood.

[17]:
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)

# Extrack the current tally separately
current_tally = sp.get_tally(name='current tally')
Using Tally Arithmetic to Compute the Delayed Neutron Precursor Concentrations

Finally, we illustrate how one can leverage OpenMC’s tally arithmetic data processing feature with MGXS objects. The openmc.mgxs module uses tally arithmetic to compute multi-group cross sections with automated uncertainty propagation. Each MGXS object includes an xs_tally attribute which is a “derived” Tally based on the tallies needed to compute the cross section type of interest. These derived tallies can be used in subsequent tally arithmetic operations. For example, we can use tally artithmetic to compute the delayed neutron precursor concentrations using the Beta and DelayedNuFissionXS objects. The delayed neutron precursor concentrations are modeled using the following equations:

\[\frac{\partial}{\partial t} C_{k,d} (t) = \int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r} \beta_{k,d} (t) \nu_d \sigma_{f,x}(\mathbf{r},E',t)\Phi(\mathbf{r},E',t) - \lambda_{d} C_{k,d} (t) \]
\[C_{k,d} (t=0) = \frac{1}{\lambda_{d}} \int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r} \beta_{k,d} (t=0) \nu_d \sigma_{f,x}(\mathbf{r},E',t=0)\Phi(\mathbf{r},E',t=0) \]
[18]:
# Set the time constants for the delayed precursors (in seconds^-1)
precursor_halflife = np.array([55.6, 24.5, 16.3, 2.37, 0.424, 0.195])
precursor_lambda = math.log(2.0) / precursor_halflife

beta = mgxs_lib.get_mgxs(mesh, 'beta')

# Create a tally object with only the delayed group filter for the time constants
beta_filters = [f for f in beta.xs_tally.filters if type(f) is not openmc.DelayedGroupFilter]
lambda_tally = beta.xs_tally.summation(nuclides=beta.xs_tally.nuclides)
for f in beta_filters:
    lambda_tally = lambda_tally.summation(filter_type=type(f), remove_filter=True) * 0. + 1.

# Set the mean of the lambda tally and reshape to account for nuclides and scores
lambda_tally._mean = precursor_lambda
lambda_tally._mean.shape = lambda_tally.std_dev.shape

# Set a total nuclide and lambda score
lambda_tally.nuclides = [openmc.Nuclide(name='total')]
lambda_tally.scores = ['lambda']

delayed_nu_fission = mgxs_lib.get_mgxs(mesh, 'delayed-nu-fission')

# Use tally arithmetic to compute the precursor concentrations
precursor_conc = beta.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) * \
    delayed_nu_fission.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) / lambda_tally

# The difference is a derived tally which can generate Pandas DataFrames for inspection
precursor_conc.get_pandas_dataframe().head(10)
[18]:
mesh 1 delayedgroup nuclide score mean std. dev.
x y z
0 1 1 1 1 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000099 2.275247e-05
1 1 1 1 2 total (((delayed-nu-fission / nu-fission) * (delayed... 0.001260 2.852271e-04
2 1 1 1 3 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000800 1.795615e-04
3 1 1 1 4 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000630 1.397151e-04
4 1 1 1 5 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000023 4.861639e-06
5 1 1 1 6 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000002 3.879558e-07
6 2 1 1 1 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000091 2.062544e-05
7 2 1 1 2 total (((delayed-nu-fission / nu-fission) * (delayed... 0.001162 2.584797e-04
8 2 1 1 3 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000737 1.626991e-04
9 2 1 1 4 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000581 1.265708e-04

Another useful feature of the Python API is the ability to extract the surface currents for the interfaces and surfaces of a mesh. We can inspect the currents for the mesh by getting the pandas dataframe.

[19]:
current_tally.get_pandas_dataframe().head(10)
[19]:
mesh 1 nuclide score mean std. dev.
x y z surf
0 1 1 1 x-min out total current 0.00000 0.000000
1 1 1 1 x-min in total current 0.00000 0.000000
2 1 1 1 x-max out total current 0.03245 0.000677
3 1 1 1 x-max in total current 0.03180 0.000659
4 1 1 1 y-min out total current 0.00000 0.000000
5 1 1 1 y-min in total current 0.00000 0.000000
6 1 1 1 y-max out total current 0.03072 0.000677
7 1 1 1 y-max in total current 0.03104 0.000652
8 1 1 1 z-min out total current 0.00000 0.000000
9 1 1 1 z-min in total current 0.00000 0.000000
Cross Section Visualizations

In addition to inspecting the data in the tallies by getting the pandas dataframe, we can also plot the tally data on the domain mesh. Below is the delayed neutron fraction tallied in each mesh cell for each delayed group.

[20]:
# Extract the energy-condensed delayed neutron fraction tally
beta_by_group = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type='energy', remove_filter=True)
beta_by_group.mean.shape = (17, 17, 6)
beta_by_group.mean[beta_by_group.mean == 0] = np.nan

# Plot the betas
plt.figure(figsize=(18,9))
fig = plt.subplot(231)
plt.imshow(beta_by_group.mean[:,:,0], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 1')

fig = plt.subplot(232)
plt.imshow(beta_by_group.mean[:,:,1], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 2')

fig = plt.subplot(233)
plt.imshow(beta_by_group.mean[:,:,2], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 3')

fig = plt.subplot(234)
plt.imshow(beta_by_group.mean[:,:,3], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 4')

fig = plt.subplot(235)
plt.imshow(beta_by_group.mean[:,:,4], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 5')

fig = plt.subplot(236)
plt.imshow(beta_by_group.mean[:,:,5], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 6')
[20]:
Text(0.5, 1.0, 'Beta - delayed group 6')
_images/examples_mdgxs-part-ii_45_1.png

Multigroup Mode

Multigroup Mode Part I: Introduction

This Notebook illustrates the usage of OpenMC’s multi-group calculational mode with the Python API. This example notebook creates and executes the 2-D C5G7 benchmark model using the openmc.MGXSLibrary class to create the supporting data library on the fly.

Generate MGXS Library
[1]:
import os

import matplotlib.pyplot as plt
import matplotlib.colors as colors
import numpy as np

import openmc

%matplotlib inline

We will now create the multi-group library using data directly from Appendix A of the C5G7 benchmark documentation. All of the data below will be created at 294K, consistent with the benchmark.

This notebook will first begin by setting the group structure and building the groupwise data for UO2. As you can see, the cross sections are input in the order of increasing groups (or decreasing energy).

Note: The C5G7 benchmark uses transport-corrected cross sections. So the total cross section we input here will technically be the transport cross section.

[2]:
# Create a 7-group structure with arbitrary boundaries (the specific boundaries are unimportant)
groups = openmc.mgxs.EnergyGroups(np.logspace(-5, 7, 8))

uo2_xsdata = openmc.XSdata('uo2', groups)
uo2_xsdata.order = 0

# When setting the data let the object know you are setting the data for a temperature of 294K.
uo2_xsdata.set_total([1.77949E-1, 3.29805E-1, 4.80388E-1, 5.54367E-1,
                      3.11801E-1, 3.95168E-1, 5.64406E-1], temperature=294.)

uo2_xsdata.set_absorption([8.0248E-03, 3.7174E-3, 2.6769E-2, 9.6236E-2,
                           3.0020E-02, 1.1126E-1, 2.8278E-1], temperature=294.)
uo2_xsdata.set_fission([7.21206E-3, 8.19301E-4, 6.45320E-3, 1.85648E-2,
                        1.78084E-2, 8.30348E-2, 2.16004E-1], temperature=294.)

uo2_xsdata.set_nu_fission([2.005998E-2, 2.027303E-3, 1.570599E-2, 4.518301E-2,
                           4.334208E-2, 2.020901E-1, 5.257105E-1], temperature=294.)

uo2_xsdata.set_chi([5.87910E-1, 4.11760E-1, 3.39060E-4, 1.17610E-7,
                    0.00000E-0, 0.00000E-0, 0.00000E-0], temperature=294.)

We will now add the scattering matrix data.

Note: Most users familiar with deterministic transport libraries are already familiar with the idea of entering one scattering matrix for every order (i.e. scattering order as the outer dimension). However, the shape of OpenMC’s scattering matrix entry is instead [Incoming groups, Outgoing Groups, Scattering Order] to best enable other scattering representations. We will follow the more familiar approach in this notebook, and then use numpy’s numpy.rollaxis function to change the ordering to what we need (scattering order on the inner dimension).

[3]:
# The scattering matrix is ordered with incoming groups as rows and outgoing groups as columns
# (i.e., below the diagonal is up-scattering).
scatter_matrix = \
    [[[1.27537E-1, 4.23780E-2, 9.43740E-6, 5.51630E-9, 0.00000E-0, 0.00000E-0, 0.00000E-0],
      [0.00000E-0, 3.24456E-1, 1.63140E-3, 3.14270E-9, 0.00000E-0, 0.00000E-0, 0.00000E-0],
      [0.00000E-0, 0.00000E-0, 4.50940E-1, 2.67920E-3, 0.00000E-0, 0.00000E-0, 0.00000E-0],
      [0.00000E-0, 0.00000E-0, 0.00000E-0, 4.52565E-1, 5.56640E-3, 0.00000E-0, 0.00000E-0],
      [0.00000E-0, 0.00000E-0, 0.00000E-0, 1.25250E-4, 2.71401E-1, 1.02550E-2, 1.00210E-8],
      [0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 1.29680E-3, 2.65802E-1, 1.68090E-2],
      [0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 8.54580E-3, 2.73080E-1]]]
scatter_matrix = np.array(scatter_matrix)
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
uo2_xsdata.set_scatter_matrix(scatter_matrix, temperature=294.)

Now that the UO2 data has been created, we can move on to the remaining materials using the same process.

However, we will actually skip repeating the above for now. Our simulation will instead use the c5g7.h5 file that has already been created using exactly the same logic as above, but for the remaining materials in the benchmark problem.

For now we will show how you would use the uo2_xsdata information to create an openmc.MGXSLibrary object and write to disk.

[4]:
# Initialize the library
mg_cross_sections_file = openmc.MGXSLibrary(groups)

# Add the UO2 data to it
mg_cross_sections_file.add_xsdata(uo2_xsdata)

# And write to disk
mg_cross_sections_file.export_to_hdf5('mgxs.h5')
Generate 2-D C5G7 Problem Input Files

To build the actual 2-D model, we will first begin by creating the materials.xml file.

First we need to define materials that will be used in the problem. In other notebooks, either nuclides or elements were added to materials at the equivalent stage. We can do that in multi-group mode as well. However, multi-group cross-sections are sometimes provided as macroscopic cross-sections; the C5G7 benchmark data are macroscopic. In this case, we can instead use the Material.add_macroscopic method to specify a macroscopic object. Unlike for nuclides and elements, we do not need provide information on atom/weight percents as no number densities are needed.

When assigning macroscopic objects to a material, the density can still be scaled by setting the density to a value that is not 1.0. This would be useful, for example, when slightly perturbing the density of water due to a small change in temperature (while of course ignoring any resultant spectral shift). The density of a macroscopic dataset is set to 1.0 in the openmc.Material object by default when a macroscopic dataset is used; so we will show its use the first time and then afterwards it will not be required.

Aside from these differences, the following code is very similar to similar code in other OpenMC example Notebooks.

[5]:
# For every cross section data set in the library, assign an openmc.Macroscopic object to a material
materials = {}
for xs in ['uo2', 'mox43', 'mox7', 'mox87', 'fiss_chamber', 'guide_tube', 'water']:
    materials[xs] = openmc.Material(name=xs)
    materials[xs].set_density('macro', 1.)
    materials[xs].add_macroscopic(xs)

Now we can go ahead and produce a materials.xml file for use by OpenMC

[6]:
# Instantiate a Materials collection, register all Materials, and export to XML
materials_file = openmc.Materials(materials.values())

# Set the location of the cross sections file to our pre-written set
materials_file.cross_sections = 'c5g7.h5'

materials_file.export_to_xml()

Our next step will be to create the geometry information needed for our assembly and to write that to the geometry.xml file.

We will begin by defining the surfaces, cells, and universes needed for each of the individual fuel pins, guide tubes, and fission chambers.

[7]:
# Create the surface used for each pin
pin_surf = openmc.ZCylinder(x0=0, y0=0, R=0.54, name='pin_surf')

# Create the cells which will be used to represent each pin type.
cells = {}
universes = {}
for material in materials.values():
    # Create the cell for the material inside the cladding
    cells[material.name] = openmc.Cell(name=material.name)
    # Assign the half-spaces to the cell
    cells[material.name].region = -pin_surf
    # Register the material with this cell
    cells[material.name].fill = material

    # Repeat the above for the material outside the cladding (i.e., the moderator)
    cell_name = material.name + '_moderator'
    cells[cell_name] = openmc.Cell(name=cell_name)
    cells[cell_name].region = +pin_surf
    cells[cell_name].fill = materials['water']

    # Finally add the two cells we just made to a Universe object
    universes[material.name] = openmc.Universe(name=material.name)
    universes[material.name].add_cells([cells[material.name], cells[cell_name]])

The next step is to take our universes (representing the different pin types) and lay them out in a lattice to represent the assembly types

[8]:
lattices = {}

# Instantiate the UO2 Lattice
lattices['UO2 Assembly'] = openmc.RectLattice(name='UO2 Assembly')
lattices['UO2 Assembly'].dimension = [17, 17]
lattices['UO2 Assembly'].lower_left = [-10.71, -10.71]
lattices['UO2 Assembly'].pitch = [1.26, 1.26]
u = universes['uo2']
g = universes['guide_tube']
f = universes['fiss_chamber']
lattices['UO2 Assembly'].universes = \
    [[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, u, u, g, u, u, g, u, u, g, u, u, u, u, u],
     [u, u, u, g, u, u, u, u, u, u, u, u, u, g, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, g, u, u, g, u, u, g, u, u, g, u, u, g, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, g, u, u, g, u, u, f, u, u, g, u, u, g, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, g, u, u, g, u, u, g, u, u, g, u, u, g, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, g, u, u, u, u, u, u, u, u, u, g, u, u, u],
     [u, u, u, u, u, g, u, u, g, u, u, g, u, u, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
     [u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u]]

# Create a containing cell and universe
cells['UO2 Assembly'] = openmc.Cell(name='UO2 Assembly')
cells['UO2 Assembly'].fill = lattices['UO2 Assembly']
universes['UO2 Assembly'] = openmc.Universe(name='UO2 Assembly')
universes['UO2 Assembly'].add_cell(cells['UO2 Assembly'])

# Instantiate the MOX Lattice
lattices['MOX Assembly'] = openmc.RectLattice(name='MOX Assembly')
lattices['MOX Assembly'].dimension = [17, 17]
lattices['MOX Assembly'].lower_left = [-10.71, -10.71]
lattices['MOX Assembly'].pitch = [1.26, 1.26]
m = universes['mox43']
n = universes['mox7']
o = universes['mox87']
g = universes['guide_tube']
f = universes['fiss_chamber']
lattices['MOX Assembly'].universes = \
    [[m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m],
     [m, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, m],
     [m, n, n, n, n, g, n, n, g, n, n, g, n, n, n, n, m],
     [m, n, n, g, n, o, o, o, o, o, o, o, n, g, n, n, m],
     [m, n, n, n, o, o, o, o, o, o, o, o, o, n, n, n, m],
     [m, n, g, o, o, g, o, o, g, o, o, g, o, o, g, n, m],
     [m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
     [m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
     [m, n, g, o, o, g, o, o, f, o, o, g, o, o, g, n, m],
     [m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
     [m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
     [m, n, g, o, o, g, o, o, g, o, o, g, o, o, g, n, m],
     [m, n, n, n, o, o, o, o, o, o, o, o, o, n, n, n, m],
     [m, n, n, g, n, o, o, o, o, o, o, o, n, g, n, n, m],
     [m, n, n, n, n, g, n, n, g, n, n, g, n, n, n, n, m],
     [m, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, m],
     [m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m]]

# Create a containing cell and universe
cells['MOX Assembly'] = openmc.Cell(name='MOX Assembly')
cells['MOX Assembly'].fill = lattices['MOX Assembly']
universes['MOX Assembly'] = openmc.Universe(name='MOX Assembly')
universes['MOX Assembly'].add_cell(cells['MOX Assembly'])

# Instantiate the reflector Lattice
lattices['Reflector Assembly'] = openmc.RectLattice(name='Reflector Assembly')
lattices['Reflector Assembly'].dimension = [1,1]
lattices['Reflector Assembly'].lower_left = [-10.71, -10.71]
lattices['Reflector Assembly'].pitch = [21.42, 21.42]
lattices['Reflector Assembly'].universes = [[universes['water']]]

# Create a containing cell and universe
cells['Reflector Assembly'] = openmc.Cell(name='Reflector Assembly')
cells['Reflector Assembly'].fill = lattices['Reflector Assembly']
universes['Reflector Assembly'] = openmc.Universe(name='Reflector Assembly')
universes['Reflector Assembly'].add_cell(cells['Reflector Assembly'])

Let’s now create the core layout in a 3x3 lattice where each lattice position is one of the assemblies we just defined.

After that we can create the final cell to contain the entire core.

[9]:
lattices['Core'] = openmc.RectLattice(name='3x3 core lattice')
lattices['Core'].dimension= [3, 3]
lattices['Core'].lower_left = [-32.13, -32.13]
lattices['Core'].pitch = [21.42, 21.42]
r = universes['Reflector Assembly']
u = universes['UO2 Assembly']
m = universes['MOX Assembly']
lattices['Core'].universes = [[u, m, r],
                              [m, u, r],
                              [r, r, r]]

# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-32.13, boundary_type='reflective')
max_x = openmc.XPlane(x0=+32.13, boundary_type='vacuum')
min_y = openmc.YPlane(y0=-32.13, boundary_type='vacuum')
max_y = openmc.YPlane(y0=+32.13, boundary_type='reflective')

# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = lattices['Core']

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y

# Create root Universe
root_universe = openmc.Universe(name='root universe', universe_id=0)
root_universe.add_cell(root_cell)

Before we commit to the geometry, we should view it using the Python API’s plotting capability

[10]:
root_universe.plot(origin=(0., 0., 0.), width=(3 * 21.42, 3 * 21.42), pixels=(500, 500),
                   color_by='material')
_images/examples_mg-mode-part-i_21_0.png

OK, it looks pretty good, let’s go ahead and write the file

[11]:
# Create Geometry and set root Universe
geometry = openmc.Geometry(root_universe)

# Export to "geometry.xml"
geometry.export_to_xml()

We can now create the tally file information. The tallies will be set up to give us the pin powers in this notebook. We will do this with a mesh filter, with one mesh cell per pin.

[12]:
tallies_file = openmc.Tallies()

# Instantiate a tally Mesh
mesh = openmc.RegularMesh()
mesh.dimension = [17 * 2, 17 * 2]
mesh.lower_left = [-32.13, -10.71]
mesh.upper_right = [+10.71, +32.13]

# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)

# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']

# Add tally to collection
tallies_file.append(tally)

# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters for the settings.xml file. Note the use of the energy_mode attribute of our settings_file object. This is used to tell OpenMC that we intend to run in multi-group mode instead of the default continuous-energy mode. If we didn’t specify this but our cross sections file was not a continuous-energy data set, then OpenMC would complain.

This will be a relatively coarse calculation with only 500,000 active histories. A benchmark-fidelity run would of course require many more!

[13]:
# OpenMC simulation parameters
batches = 150
inactive = 50
particles = 5000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles

# Tell OpenMC this is a multi-group problem
settings_file.energy_mode = 'multi-group'

# Set the verbosity to 6 so we dont see output for every batch
settings_file.verbosity = 6

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-32.13, -10.71, -1e50, 10.71, 32.13, 1e50]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Tell OpenMC we want to run in eigenvalue mode
settings_file.run_mode = 'eigenvalue'

# Export to "settings.xml"
settings_file.export_to_xml()

Let’s go ahead and execute the simulation! You’ll notice that the output for multi-group mode is exactly the same as for continuous-energy. The differences are all under the hood.

[14]:
# Run OpenMC
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2017 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.8.0
          Git SHA1 | 966169de084fcfda3a5aaca3edc0065c8caf6bbc
         Date/Time | 2017-03-09 08:18:02
    OpenMP Threads | 8

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections HDF5 file...
 Reading tallies XML file...
 Loading cross section data...
 Loading uo2 data...
 Loading mox43 data...
 Loading mox7 data...
 Loading mox87 data...
 Loading fiss_chamber data...
 Loading guide_tube data...
 Loading water data...
 Building neighboring cells lists for each surface...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

 Creating state point statepoint.150.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     =  1.3630E-01 seconds
   Reading cross sections          =  3.0827E-02 seconds
 Total time in simulation          =  8.9648E+00 seconds
   Time in transport only          =  8.2752E+00 seconds
   Time in inactive batches        =  2.4798E+00 seconds
   Time in active batches          =  6.4849E+00 seconds
   Time synchronizing fission bank =  1.4553E-02 seconds
     Sampling source sites         =  1.0318E-02 seconds
     SEND/RECV source sites        =  4.0840E-03 seconds
   Time accumulating tallies       =  4.2427E-04 seconds
 Total time for finalization       =  1.7081E-02 seconds
 Total time elapsed                =  9.1340E+00 seconds
 Calculation Rate (inactive)       =  1.00813E+05 neutrons/second
 Calculation Rate (active)         =  77101.8 neutrons/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.18880 +/-  0.00179
 k-effective (Track-length)  =  1.18853 +/-  0.00244
 k-effective (Absorption)    =  1.18601 +/-  0.00111
 Combined k-effective        =  1.18628 +/-  0.00111
 Leakage Fraction            =  0.00175 +/-  0.00006

[14]:
0
Results Visualization

Now that we have run the simulation, let’s look at the fission rate and flux tallies that we tallied.

[15]:
# Load the last statepoint file and keff value
sp = openmc.StatePoint('statepoint.' + str(batches) + '.h5')

# Get the OpenMC pin power tally data
mesh_tally = sp.get_tally(name='mesh tally')
fission_rates = mesh_tally.get_values(scores=['fission'])

# Reshape array to 2D for plotting
fission_rates.shape = mesh.dimension

# Normalize to the average pin power
fission_rates /= np.mean(fission_rates[fission_rates > 0.])

# Force zeros to be NaNs so their values are not included when matplotlib calculates
# the color scale
fission_rates[fission_rates == 0.] = np.nan

# Plot the pin powers and the fluxes
plt.figure()
plt.imshow(fission_rates, interpolation='none', cmap='jet', origin='lower')
plt.colorbar()
plt.title('Pin Powers')
plt.show()

_images/examples_mg-mode-part-i_31_0.png

There we have it! We have just successfully run the C5G7 benchmark model!

Multigroup Mode Part II: MGXS Library Generation with OpenMC

The previous Notebook in this series used multi-group mode to perform a calculation with previously defined cross sections. However, in many circumstances the multi-group data is not given and one must instead generate the cross sections for the specific application (or at least verify the use of cross sections from another application).

This Notebook illustrates the use of the openmc.mgxs.Library class specifically for the calculation of MGXS to be used in OpenMC’s multi-group mode. This example notebook is therefore very similar to the MGXS Part III notebook, except OpenMC is used as the multi-group solver instead of OpenMOC.

During this process, this notebook will illustrate the following features:

  • Calculation of multi-group cross sections for a fuel assembly
  • Automated creation and storage of MGXS with openmc.mgxs.Library
  • Steady-state pin-by-pin fission rates comparison between continuous-energy and multi-group OpenMC.
  • Modification of the scattering data in the library to show the flexibility of the multi-group solver
Generate Input Files
[1]:
import matplotlib.pyplot as plt
import numpy as np
import os

import openmc

%matplotlib inline

We will begin by creating three materials for the fuel, water, and cladding of the fuel pins.

[2]:
# 1.6% enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_element('U', 1., enrichment=1.6)
fuel.add_element('O', 2.)

# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_element('Zr', 1.)

# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_element('H', 4.9457e-2)
water.add_element('O', 2.4732e-2)
water.add_element('B', 8.0042e-6)

With our three materials, we can now create a Materials object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials object
materials_file = openmc.Materials((fuel, zircaloy, water))

# Export to "materials.xml"
materials_file.export_to_xml()

Now let’s move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC’s lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.

[4]:
# Create cylinders for the fuel and clad
# The x0 and y0 parameters (0. and 0.) are the default values for an
# openmc.ZCylinder object. We could therefore leave them out to no effect
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.45720)

# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='reflective')
min_y = openmc.YPlane(y0=-10.71, boundary_type='reflective')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10., boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10., boundary_type='reflective')

With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.

[5]:
# Create a Universe to encapsulate a fuel pin
fuel_pin_universe = openmc.Universe(name='1.6% Fuel Pin')

# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
fuel_pin_universe.add_cell(fuel_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
fuel_pin_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
fuel_pin_universe.add_cell(moderator_cell)

Likewise, we can construct a control rod guide tube with the same surfaces.

[6]:
# Create a Universe to encapsulate a control rod guide tube
guide_tube_universe = openmc.Universe(name='Guide Tube')

# Create guide tube Cell
guide_tube_cell = openmc.Cell(name='Guide Tube Water')
guide_tube_cell.fill = water
guide_tube_cell.region = -fuel_outer_radius
guide_tube_universe.add_cell(guide_tube_cell)

# Create a clad Cell
clad_cell = openmc.Cell(name='Guide Clad')
clad_cell.fill = zircaloy
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
guide_tube_universe.add_cell(clad_cell)

# Create a moderator Cell
moderator_cell = openmc.Cell(name='Guide Tube Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
guide_tube_universe.add_cell(moderator_cell)

Using the pin cell universe, we can construct a 17x17 rectangular lattice with a 1.26 cm pitch.

[7]:
# Create fuel assembly Lattice
assembly = openmc.RectLattice(name='1.6% Fuel Assembly')
assembly.pitch = (1.26, 1.26)
assembly.lower_left = [-1.26 * 17. / 2.0] * 2

Next, we create a NumPy array of fuel pin and guide tube universes for the lattice.

[8]:
# Create array indices for guide tube locations in lattice
template_x = np.array([5, 8, 11, 3, 13, 2, 5, 8, 11, 14, 2, 5, 8,
                       11, 14, 2, 5, 8, 11, 14, 3, 13, 5, 8, 11])
template_y = np.array([2, 2, 2, 3, 3, 5, 5, 5, 5, 5, 8, 8, 8, 8,
                       8, 11, 11, 11, 11, 11, 13, 13, 14, 14, 14])

# Initialize an empty 17x17 array of the lattice universes
universes = np.empty((17, 17), dtype=openmc.Universe)

# Fill the array with the fuel pin and guide tube universes
universes[:, :] = fuel_pin_universe
universes[template_x, template_y] = guide_tube_universe

# Store the array of universes in the lattice
assembly.universes = universes

OpenMC requires that there is a “root” universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.

[9]:
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = assembly

# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z

# Create root Universe
root_universe = openmc.Universe(name='root universe', universe_id=0)
root_universe.add_cell(root_cell)

Before proceeding lets check the geometry.

[10]:
root_universe.plot(origin=(0., 0., 0.), width=(21.42, 21.42), pixels=(500, 500), color_by='material')
[10]:
<matplotlib.image.AxesImage at 0x7f6d864d44a8>
_images/examples_mg-mode-part-ii_20_1.png

Looks good!

We now must create a geometry that is assigned a root universe and export it to XML.

[11]:
# Create Geometry and set root universe
geometry = openmc.Geometry(root_universe)

# Export to "geometry.xml"
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters.

[12]:
# OpenMC simulation parameters
batches = 600
inactive = 50
particles = 3000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}
settings_file.run_mode = 'eigenvalue'
settings_file.verbosity = 4

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()
Create an MGXS Library

Now we are ready to generate multi-group cross sections! First, let’s define a 2-group structure using the built-in EnergyGroups class.

[13]:
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups([0., 0.625, 20.0e6])

Next, we will instantiate an openmc.mgxs.Library for the energy groups with our the fuel assembly geometry.

[14]:
# Initialize a 2-group MGXS Library for OpenMC
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = groups

Now, we must specify to the Library which types of cross sections to compute. OpenMC’s multi-group mode can accept isotropic flux-weighted cross sections or angle-dependent cross sections, as well as supporting anisotropic scattering represented by either Legendre polynomials, histogram, or tabular angular distributions. We will create the following multi-group cross sections needed to run an OpenMC simulation to verify the accuracy of our cross sections: “total”, “absorption”, “nu-fission”, ‘“fission”, “nu-scatter matrix”, “multiplicity matrix”, and “chi”.

The “multiplicity matrix” type is a relatively rare cross section type. This data is needed to provide OpenMC’s multi-group mode with additional information needed to accurately treat scattering multiplication (i.e., (n,xn) reactions)), including how this multiplication varies depending on both incoming and outgoing neutron energies.

[15]:
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
                       'nu-scatter matrix', 'multiplicity matrix', 'chi']

Now we must specify the type of domain over which we would like the Library to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library supports “material”, “cell”, “universe”, and “mesh” domain types. In this simple example, we wish to compute multi-group cross sections only for each material and therefore will use a “material” domain type.

NOTE: By default, the Library class will instantiate MGXS objects for each and every domain (material, cell, universe, or mesh) in the geometry of interest. However, one may specify a subset of these domains to the Library.domains property.

[16]:
# Specify a "cell" domain type for the cross section tally filters
mgxs_lib.domain_type = "material"

# Specify the cell domains over which to compute multi-group cross sections
mgxs_lib.domains = geometry.get_all_materials().values()

We will instruct the library to not compute cross sections on a nuclide-by-nuclide basis, and instead to focus on generating material-specific macroscopic cross sections.

NOTE: The default value of the by_nuclide parameter is False, so the following step is not necessary but is included for illustrative purposes.

[17]:
# Do not compute cross sections on a nuclide-by-nuclide basis
mgxs_lib.by_nuclide = False

Now we will set the scattering order that we wish to use. For this problem we will use P3 scattering. A warning is expected telling us that the default behavior (a P0 correction on the scattering data) is over-ridden by our choice of using a Legendre expansion to treat anisotropic scattering.

[18]:
# Set the Legendre order to 3 for P3 scattering
mgxs_lib.legendre_order = 3
/home/nelsonag/git/openmc/openmc/mgxs/library.py:412: RuntimeWarning: The P0 correction will be ignored since the scattering order 0 is greater than zero
  warn(msg, RuntimeWarning)

Now that the Library has been setup let’s verify that it contains the types of cross sections which meet the needs of OpenMC’s multi-group solver. Note that this step is done automatically when writing the Multi-Group Library file later in the process (as part of mgxs_lib.write_mg_library()), but it is a good practice to also run this before spending all the time running OpenMC to generate the cross sections.

If no error is raised, then we have a good set of data.

[19]:
# Check the library - if no errors are raised, then the library is satisfactory.
mgxs_lib.check_library_for_openmc_mgxs()

Great, now we can use the Library to construct the tallies needed to compute all of the requested multi-group cross sections in each domain.

[20]:
# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()

The tallies can now be exported to a “tallies.xml” input file for OpenMC.

NOTE: At this point the Library has constructed nearly 100 distinct Tally objects. The overhead to tally in OpenMC scales as O(N) for N tallies, which can become a bottleneck for large tally datasets. To compensate for this, the Python API’s Tally, Filter and Tallies classes allow for the smart merging of tallies when possible. The Library class supports this runtime optimization with the use of the optional merge parameter (False by default) for the Library.add_to_tallies_file(...) method, as shown below.

[21]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)

In addition, we instantiate a fission rate mesh tally that we will eventually use to compare with the corresponding multi-group results.

[22]:
# Instantiate a tally Mesh
mesh = openmc.RegularMesh()
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.upper_right = [+10.71, +10.71]

# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)

# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']

# Add tally to collection
tallies_file.append(tally, merge=True)

# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=66.
  warn(msg, IDWarning)
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=11.
  warn(msg, IDWarning)

Time to run the calculation and get our results!

[23]:
# Run OpenMC
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2018 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.10.0
          Git SHA1 | 6c2d82a4d7dfe10312329d5969568fc03a698416
         Date/Time | 2018-04-22 15:02:43
    OpenMP Threads | 8


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.16513 +/-  0.00090
 k-effective (Track-length)  =  1.16337 +/-  0.00104
 k-effective (Absorption)    =  1.16479 +/-  0.00080
 Combined k-effective        =  1.16460 +/-  0.00068
 Leakage Fraction            =  0.00000 +/-  0.00000

To make sure the results we need are available after running the multi-group calculation, we will now rename the statepoint and summary files.

[24]:
# Move the statepoint File
ce_spfile = './statepoint_ce.h5'
os.rename('statepoint.' + str(batches) + '.h5', ce_spfile)
# Move the Summary file
ce_sumfile = './summary_ce.h5'
os.rename('summary.h5', ce_sumfile)
Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. Let’s begin by loading the StatePoint file.

[25]:
# Load the statepoint file
sp = openmc.StatePoint(ce_spfile, autolink=False)

# Load the summary file in its new location
su = openmc.Summary(ce_sumfile)
sp.link_with_summary(su)

The statepoint is now ready to be analyzed by the Library. We simply have to load the tallies from the statepoint into the Library and our MGXS objects will compute the cross sections for us under-the-hood.

[26]:
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)

The next step will be to prepare the input for OpenMC to use our newly created multi-group data.

Multi-Group OpenMC Calculation
We will now use the Library to produce a multi-group cross section data set for use by the OpenMC multi-group solver.
Note that since this simulation included so few histories, it is reasonable to expect some data has not had any scores, and thus we could see division by zero errors. This will show up as a runtime warning in the following step. The Library class is designed to gracefully handle these scenarios.
[27]:
# Create a MGXS File which can then be written to disk
mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=['fuel', 'zircaloy', 'water'])

# Write the file to disk using the default filename of "mgxs.h5"
mgxs_file.export_to_hdf5()

OpenMC’s multi-group mode uses the same input files as does the continuous-energy mode (materials, geometry, settings, plots, and tallies file). Differences would include the use of a flag to tell the code to use multi-group transport, a location of the multi-group library file, and any changes needed in the materials.xml and geometry.xml files to re-define materials as necessary. The materials and geometry file changes could be necessary if materials or their nuclide/element/macroscopic constituents need to be renamed.

In this example we have created macroscopic cross sections (by material), and thus we will need to change the material definitions accordingly.

First we will create the new materials.xml file.

[28]:
# Re-define our materials to use the multi-group macroscopic data
# instead of the continuous-energy data.

# 1.6% enriched fuel UO2
fuel_mg = openmc.Material(name='UO2', material_id=1)
fuel_mg.add_macroscopic('fuel')

# cladding
zircaloy_mg = openmc.Material(name='Clad', material_id=2)
zircaloy_mg.add_macroscopic('zircaloy')

# moderator
water_mg = openmc.Material(name='Water', material_id=3)
water_mg.add_macroscopic('water')

# Finally, instantiate our Materials object
materials_file = openmc.Materials((fuel_mg, zircaloy_mg, water_mg))

# Set the location of the cross sections file
materials_file.cross_sections = 'mgxs.h5'

# Export to "materials.xml"
materials_file.export_to_xml()
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Material instance already exists with id=1.
  warn(msg, IDWarning)
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Material instance already exists with id=2.
  warn(msg, IDWarning)
/home/nelsonag/git/openmc/openmc/mixin.py:71: IDWarning: Another Material instance already exists with id=3.
  warn(msg, IDWarning)

No geometry file neeeds to be written as the continuous-energy file is correctly defined for the multi-group case as well.

Next, we can make the changes we need to the simulation parameters. These changes are limited to telling OpenMC to run a multi-group vice contrinuous-energy calculation.

[29]:
# Set the energy mode
settings_file.energy_mode = 'multi-group'

# Export to "settings.xml"
settings_file.export_to_xml()

Lets clear the tallies file so it doesn’t include tallies for re-generating a multi-group library, but then put back in a tally for the fission mesh.

[30]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()

# Add fission and flux mesh to tally for plotting using the same mesh we've already defined
mesh_tally = openmc.Tally(name='mesh tally')
mesh_tally.filters = [openmc.MeshFilter(mesh)]
mesh_tally.scores = ['fission']
tallies_file.append(mesh_tally)

# Export to "tallies.xml"
tallies_file.export_to_xml()

Before running the calculation let’s visually compare a subset of the newly-generated multi-group cross section data to the continuous-energy data. We will do this using the cross section plotting functionality built-in to the OpenMC Python API.

[31]:
# First lets plot the fuel data
# We will first add the continuous-energy data
fig = openmc.plot_xs(fuel, ['total'])

# We will now add in the corresponding multi-group data and show the result
openmc.plot_xs(fuel_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()

# Then repeat for the zircaloy data
fig = openmc.plot_xs(zircaloy, ['total'])
openmc.plot_xs(zircaloy_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()

# And finally repeat for the water data
fig = openmc.plot_xs(water, ['total'])
openmc.plot_xs(water_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()
_images/examples_mg-mode-part-ii_65_0.png
_images/examples_mg-mode-part-ii_65_1.png
_images/examples_mg-mode-part-ii_65_2.png

At this point, the problem is set up and we can run the multi-group calculation.

[32]:
# Run the Multi-Group OpenMC Simulation
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2018 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.10.0
          Git SHA1 | 6c2d82a4d7dfe10312329d5969568fc03a698416
         Date/Time | 2018-04-22 15:04:03
    OpenMP Threads | 8


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.16541 +/-  0.00086
 k-effective (Track-length)  =  1.16590 +/-  0.00096
 k-effective (Absorption)    =  1.16469 +/-  0.00046
 Combined k-effective        =  1.16480 +/-  0.00045
 Leakage Fraction            =  0.00000 +/-  0.00000

Results Comparison

Now we can compare the multi-group and continuous-energy results.

We will begin by loading the multi-group statepoint file we just finished writing and extracting the calculated keff.

[33]:
# Move the StatePoint File
mg_spfile = './statepoint_mg.h5'
os.rename('statepoint.' + str(batches) + '.h5', mg_spfile)
# Move the Summary file
mg_sumfile = './summary_mg.h5'
os.rename('summary.h5', mg_sumfile)

# Rename and then load the last statepoint file and keff value
mgsp = openmc.StatePoint(mg_spfile, autolink=False)

# Load the summary file in its new location
mgsu = openmc.Summary(mg_sumfile)
mgsp.link_with_summary(mgsu)

# Get keff
mg_keff = mgsp.k_combined

Next, we can load the continuous-energy eigenvalue for comparison.

[34]:
ce_keff = sp.k_combined

Lets compare the two eigenvalues, including their bias

[35]:
bias = 1.0E5 * (ce_keff - mg_keff)

print('Continuous-Energy keff = {0:1.6f}'.format(ce_keff))
print('Multi-Group keff = {0:1.6f}'.format(mg_keff))
print('bias [pcm]: {0:1.1f}'.format(bias.nominal_value))
Continuous-Energy keff = 1.164600+/-0.000677
Multi-Group keff = 1.164805+/-0.000448
bias [pcm]: -20.4

This shows a small but nontrivial pcm bias between the two methods. Some degree of mismatch is expected simply to the very few histories being used in these example problems. An additional mismatch is always inherent in the practical application of multi-group theory due to the high degree of approximations inherent in that method.

Pin Power Visualizations

Next we will visualize the pin power results obtained from both the Continuous-Energy and Multi-Group OpenMC calculations.

First, we extract volume-integrated fission rates from the Multi-Group calculation’s mesh fission rate tally for each pin cell in the fuel assembly.

[36]:
# Get the OpenMC fission rate mesh tally data
mg_mesh_tally = mgsp.get_tally(name='mesh tally')
mg_fission_rates = mg_mesh_tally.get_values(scores=['fission'])

# Reshape array to 2D for plotting
mg_fission_rates.shape = (17,17)

# Normalize to the average pin power
mg_fission_rates /= np.mean(mg_fission_rates[mg_fission_rates > 0.])

We can now do the same for the Continuous-Energy results.

[37]:
# Get the OpenMC fission rate mesh tally data
ce_mesh_tally = sp.get_tally(name='mesh tally')
ce_fission_rates = ce_mesh_tally.get_values(scores=['fission'])

# Reshape array to 2D for plotting
ce_fission_rates.shape = (17,17)

# Normalize to the average pin power
ce_fission_rates /= np.mean(ce_fission_rates[ce_fission_rates > 0.])

Now we can easily use Matplotlib to visualize the two fission rates side-by-side.

[38]:
# Force zeros to be NaNs so their values are not included when matplotlib calculates
# the color scale
ce_fission_rates[ce_fission_rates == 0.] = np.nan
mg_fission_rates[mg_fission_rates == 0.] = np.nan

# Plot the CE fission rates in the left subplot
fig = plt.subplot(121)
plt.imshow(ce_fission_rates, interpolation='none', cmap='jet')
plt.title('Continuous-Energy Fission Rates')

# Plot the MG fission rates in the right subplot
fig2 = plt.subplot(122)
plt.imshow(mg_fission_rates, interpolation='none', cmap='jet')
plt.title('Multi-Group Fission Rates')

[38]:
<matplotlib.text.Text at 0x7f6db0a885c0>
_images/examples_mg-mode-part-ii_81_1.png

These figures really indicate that more histories are probably necessary when trying to achieve a fully converged solution, but hey, this is good enough for our example!

Scattering Anisotropy Treatments

We will next show how we can work with the scattering angular distributions. OpenMC’s MG solver has the capability to use group-to-group angular distributions which are represented as any of the following: a truncated Legendre series of up to the 10th order, a histogram distribution, and a tabular distribution. Any combination of these representations can be used by OpenMC during the transport process, so long as all constituents of a given material use the same representation. This means it is possible to have water represented by a tabular distribution and fuel represented by a Legendre if so desired.

Note: To have the highest runtime performance OpenMC natively converts Legendre series to a tabular distribution before the transport begins. This default functionality can be turned off with the tabular_legendre element of the settings.xml file (or for the Python API, the openmc.Settings.tabular_legendre attribute).

This section will examine the following: - Re-run the MG-mode calculation with P0 scattering everywhere using the openmc.Settings.max_order attribute - Re-run the problem with only the water represented with P3 scattering and P0 scattering for the remaining materials using the Python API’s ability to convert between formats.

Global P0 Scattering

First we begin by re-running with P0 scattering (i.e., isotropic) everywhere. If a global maximum order is requested, the most effective way to do this is to use the max_order attribute of our openmc.Settings object.

[39]:
# Set the maximum scattering order to 0 (i.e., isotropic scattering)
settings_file.max_order = 0

# Export to "settings.xml"
settings_file.export_to_xml()

Now we can re-run OpenMC to obtain our results

[40]:
# Run the Multi-Group OpenMC Simulation
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2018 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.10.0
          Git SHA1 | 6c2d82a4d7dfe10312329d5969568fc03a698416
         Date/Time | 2018-04-22 15:04:39
    OpenMP Threads | 8


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.16379 +/-  0.00090
 k-effective (Track-length)  =  1.16469 +/-  0.00101
 k-effective (Absorption)    =  1.16315 +/-  0.00052
 Combined k-effective        =  1.16335 +/-  0.00050
 Leakage Fraction            =  0.00000 +/-  0.00000

And then get the eigenvalue differences from the Continuous-Energy and P3 MG solution

[41]:
# Move the statepoint File
mgp0_spfile = './statepoint_mg_p0.h5'
os.rename('statepoint.' + str(batches) + '.h5', mgp0_spfile)
# Move the Summary file
mgp0_sumfile = './summary_mg_p0.h5'
os.rename('summary.h5', mgp0_sumfile)

# Load the last statepoint file and keff value
mgsp_p0 = openmc.StatePoint(mgp0_spfile, autolink=False)

# Get keff
mg_p0_keff = mgsp_p0.k_combined

bias_p0 = 1.0E5 * (ce_keff - mg_p0_keff)

print('P3 bias [pcm]: {0:1.1f}'.format(bias.nominal_value))
print('P0 bias [pcm]: {0:1.1f}'.format(bias_p0.nominal_value))
P3 bias [pcm]: -20.4
P0 bias [pcm]: 125.1
Mixed Scattering Representations

OpenMC’s Multi-Group mode also includes a feature where not every data in the library is required to have the same scattering treatment. For example, we could represent the water with P3 scattering, and the fuel and cladding with P0 scattering. This series will show how this can be done.

First we will convert the data to P0 scattering, unless its water, then we will leave that as P3 data.

[42]:
# Convert the zircaloy and fuel data to P0 scattering
for i, xsdata in enumerate(mgxs_file.xsdatas):
    if xsdata.name != 'water':
        mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('legendre', 0)

We can also use whatever scattering format that we want for the materials in the library. As an example, we will take this P0 data and convert zircaloy to a histogram anisotropic scattering format and the fuel to a tabular anisotropic scattering format

[43]:
# Convert the formats as discussed
for i, xsdata in enumerate(mgxs_file.xsdatas):
    if xsdata.name == 'zircaloy':
        mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('histogram', 2)
    elif xsdata.name == 'fuel':
        mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('tabular', 2)

mgxs_file.export_to_hdf5('mgxs.h5')

Finally we will re-set our max_order parameter of our openmc.Settings object to our maximum order so that OpenMC will use whatever scattering data is available in the library.

After we do this we can re-run the simulation.

[44]:
settings_file.max_order = None

# Export to "settings.xml"
settings_file.export_to_xml()

# Run the Multi-Group OpenMC Simulation
openmc.run()

                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2018 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.10.0
          Git SHA1 | 6c2d82a4d7dfe10312329d5969568fc03a698416
         Date/Time | 2018-04-22 15:05:16
    OpenMP Threads | 8


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     =  1.16471 +/-  0.00093
 k-effective (Track-length)  =  1.16412 +/-  0.00106
 k-effective (Absorption)    =  1.16449 +/-  0.00050
 Combined k-effective        =  1.16441 +/-  0.00049
 Leakage Fraction            =  0.00000 +/-  0.00000

For a final step we can again obtain the eigenvalue differences from this case and compare with the same from the P3 MG solution

[45]:
# Load the last statepoint file and keff value
mgsp_mixed = openmc.StatePoint('./statepoint.' + str(batches) + '.h5')

mg_mixed_keff = mgsp_mixed.k_combined
bias_mixed = 1.0E5 * (ce_keff - mg_mixed_keff)

print('P3 bias [pcm]: {0:1.1f}'.format(bias.nominal_value))
print('Mixed Scattering bias [pcm]: {0:1.1f}'.format(bias_mixed.nominal_value))
P3 bias [pcm]: -20.4
Mixed Scattering bias [pcm]: 19.5

Our tests in this section showed the flexibility of data formatting within OpenMC’s multi-group mode: every material can be represented with its own format with the approximations that make the most sense. Now, as you’ll see above, the runtimes from our P3, P0, and mixed cases are not significantly different and therefore this might not be a useful strategy for multi-group Monte Carlo. However, this capability provides a useful benchmark for the accuracy hit one may expect due to these scattering approximations before implementing this generality in a deterministic solver where the runtime savings are more significant.

NOTE: The biases obtained above with P3, P0, and mixed representations do not necessarily reflect the inherent accuracies of the options. These cases were not run with a sufficient number of histories to truly differentiate methods improvement from statistical noise.

Multigroup Mode Part III: Advanced Feature Showcase

This Notebook illustrates the use of the the more advanced features of OpenMC’s multi-group mode and the openmc.mgxs.Library class. During this process, this notebook will illustrate the following features:

  • Calculation of multi-group cross sections for a simplified BWR 8x8 assembly with isotropic and angle-dependent MGXS.
  • Automated creation and storage of MGXS with openmc.mgxs.Library
  • Fission rate comparison between continuous-energy and the two multi-group OpenMC cases.

To avoid focusing on unimportant details, the BWR assembly in this notebook is greatly simplified. The descriptions which follow will point out some areas of simplification.

Generate Input Files
[1]:
import os

import matplotlib.pyplot as plt
import numpy as np
import openmc

%matplotlib inline

We will be running a rodded 8x8 assembly with Gadolinia fuel pins. Let’s start by creating the materials that we will use later.

Material Definition Simplifications:

  • This model will be run at room temperature so the NNDC ENDF-B/VII.1 data set can be used but the water density will be representative of a module with around 20% voiding. This water density will be non-physically used in all regions of the problem.
  • Steel is composed of more than just iron, but we will only treat it as such here.
[2]:
materials = {}

# Fuel
materials['Fuel'] = openmc.Material(name='Fuel')
materials['Fuel'].set_density('g/cm3', 10.32)
materials['Fuel'].add_element('O', 2)
materials['Fuel'].add_element('U', 1, enrichment=3.)

# Gadolinia bearing fuel
materials['Gad'] = openmc.Material(name='Gad')
materials['Gad'].set_density('g/cm3', 10.23)
materials['Gad'].add_element('O', 2)
materials['Gad'].add_element('U', 1, enrichment=3.)
materials['Gad'].add_element('Gd', .02)

# Zircaloy
materials['Zirc2'] = openmc.Material(name='Zirc2')
materials['Zirc2'].set_density('g/cm3', 6.55)
materials['Zirc2'].add_element('Zr', 1)

# Boiling Water
materials['Water'] = openmc.Material(name='Water')
materials['Water'].set_density('g/cm3', 0.6)
materials['Water'].add_element('H', 2)
materials['Water'].add_element('O', 1)

# Boron Carbide for the Control Rods
materials['B4C'] = openmc.Material(name='B4C')
materials['B4C'].set_density('g/cm3', 0.7 * 2.52)
materials['B4C'].add_element('B', 4)
materials['B4C'].add_element('C', 1)

# Steel
materials['Steel'] = openmc.Material(name='Steel')
materials['Steel'].set_density('g/cm3', 7.75)
materials['Steel'].add_element('Fe', 1)

We can now create a Materials object that can be exported to an actual XML file.

[3]:
# Instantiate a Materials object
materials_file = openmc.Materials(materials.values())

# Export to "materials.xml"
materials_file.export_to_xml()

Now let’s move on to the geometry. The first step is to define some constants which will be used to set our dimensions and then we can start creating the surfaces and regions for the problem, the 8x8 lattice, the rods and the control blade.

Before proceeding let’s discuss some simplifications made to the problem geometry: - To enable the use of an equal-width mesh for running the multi-group calculations, the intra-assembly gap was increased to the same size as the pitch of the 8x8 fuel lattice - The can is neglected - The pin-in-water geometry for the control blade is ignored and instead the blade is a solid block of B4C - Rounded corners are ignored - There is no cladding for the water rod

[4]:
# Set constants for the problem and assembly dimensions
fuel_rad = 0.53213
clad_rad = 0.61341
Np = 8
pin_pitch = 1.6256
length = float(Np + 2) * pin_pitch
assembly_width = length - 2. * pin_pitch
rod_thick = 0.47752 / 2. + 0.14224
rod_span = 7. * pin_pitch

surfaces = {}

# Create boundary planes to surround the geometry
surfaces['Global x-'] = openmc.XPlane(0., boundary_type='reflective')
surfaces['Global x+'] = openmc.XPlane(length, boundary_type='reflective')
surfaces['Global y-'] = openmc.YPlane(0., boundary_type='reflective')
surfaces['Global y+'] = openmc.YPlane(length, boundary_type='reflective')

# Create cylinders for the fuel and clad
surfaces['Fuel Radius'] = openmc.ZCylinder(r=fuel_rad)
surfaces['Clad Radius'] = openmc.ZCylinder(r=clad_rad)

surfaces['Assembly x-'] = openmc.XPlane(pin_pitch)
surfaces['Assembly x+'] = openmc.XPlane(length - pin_pitch)
surfaces['Assembly y-'] = openmc.YPlane(pin_pitch)
surfaces['Assembly y+'] = openmc.YPlane(length - pin_pitch)

# Set surfaces for the control blades
surfaces['Top Blade y-'] = openmc.YPlane(length - rod_thick)
surfaces['Top Blade x-'] = openmc.XPlane(pin_pitch)
surfaces['Top Blade x+'] = openmc.XPlane(rod_span)
surfaces['Left Blade x+'] = openmc.XPlane(rod_thick)
surfaces['Left Blade y-'] = openmc.YPlane(length - rod_span)
surfaces['Left Blade y+'] = openmc.YPlane(9. * pin_pitch)

With the surfaces defined, we can now construct regions with these surfaces before we use those to create cells

[5]:
# Set regions for geometry building
regions = {}
regions['Global'] = \
    (+surfaces['Global x-'] & -surfaces['Global x+'] &
     +surfaces['Global y-'] & -surfaces['Global y+'])
regions['Assembly'] = \
    (+surfaces['Assembly x-'] & -surfaces['Assembly x+'] &
     +surfaces['Assembly y-'] & -surfaces['Assembly y+'])
regions['Fuel'] = -surfaces['Fuel Radius']
regions['Clad'] = +surfaces['Fuel Radius'] & -surfaces['Clad Radius']
regions['Water'] = +surfaces['Clad Radius']
regions['Top Blade'] = \
    (+surfaces['Top Blade y-'] & -surfaces['Global y+']) & \
    (+surfaces['Top Blade x-'] & -surfaces['Top Blade x+'])
regions['Top Steel'] = \
    (+surfaces['Global x-'] & -surfaces['Top Blade x-']) & \
    (+surfaces['Top Blade y-'] & -surfaces['Global y+'])
regions['Left Blade'] = \
    (+surfaces['Left Blade y-'] & -surfaces['Left Blade y+']) & \
    (+surfaces['Global x-'] & -surfaces['Left Blade x+'])
regions['Left Steel'] = \
    (+surfaces['Left Blade y+'] & -surfaces['Top Blade y-']) & \
    (+surfaces['Global x-'] & -surfaces['Left Blade x+'])
regions['Corner Blade'] = \
    regions['Left Steel'] | regions['Top Steel']
regions['Water Fill'] = \
    regions['Global'] & ~regions['Assembly'] & \
    ~regions['Top Blade'] & ~regions['Left Blade'] &\
    ~regions['Corner Blade']

We will begin building the 8x8 assembly. To do that we will have to build the cells and universe for each pin type (fuel, gadolinia-fuel, and water).

[6]:
universes = {}
cells = {}

for name, mat, in zip(['Fuel Pin', 'Gd Pin'],
                      [materials['Fuel'], materials['Gad']]):
    universes[name] = openmc.Universe(name=name)
    cells[name] = openmc.Cell(name=name)
    cells[name].fill = mat
    cells[name].region = regions['Fuel']
    universes[name].add_cell(cells[name])

    cells[name + ' Clad'] = openmc.Cell(name=name + ' Clad')
    cells[name + ' Clad'].fill = materials['Zirc2']
    cells[name + ' Clad'].region = regions['Clad']
    universes[name].add_cell(cells[name + ' Clad'])

    cells[name + ' Water'] = openmc.Cell(name=name + ' Water')
    cells[name + ' Water'].fill = materials['Water']
    cells[name + ' Water'].region = regions['Water']
    universes[name].add_cell(cells[name + ' Water'])

universes['Hole'] = openmc.Universe(name='Hole')
cells['Hole'] = openmc.Cell(name='Hole')
cells['Hole'].fill = materials['Water']
universes['Hole'].add_cell(cells['Hole'])

Let’s use this pin information to create our 8x8 assembly.

[7]:
# Create fuel assembly Lattice
universes['Assembly'] = openmc.RectLattice(name='Assembly')
universes['Assembly'].pitch = (pin_pitch, pin_pitch)
universes['Assembly'].lower_left = [pin_pitch, pin_pitch]

f = universes['Fuel Pin']
g = universes['Gd Pin']
h = universes['Hole']

lattices = [[f, f, f, f, f, f, f, f],
            [f, f, f, f, f, f, f, f],
            [f, f, f, g, f, g, f, f],
            [f, f, g, h, h, f, g, f],
            [f, f, f, h, h, f, f, f],
            [f, f, g, f, f, f, g, f],
            [f, f, f, g, f, g, f, f],
            [f, f, f, f, f, f, f, f]]

# Store the array of lattice universes
universes['Assembly'].universes = lattices

cells['Assembly'] = openmc.Cell(name='Assembly')
cells['Assembly'].fill = universes['Assembly']
cells['Assembly'].region = regions['Assembly']

So far we have the rods and water within the assembly , but we still need the control blade and the water which fills the rest of the space. We will create those cells now

[8]:
# The top portion of the blade, poisoned with B4C
cells['Top Blade'] = openmc.Cell(name='Top Blade')
cells['Top Blade'].fill = materials['B4C']
cells['Top Blade'].region = regions['Top Blade']

# The left portion of the blade, poisoned with B4C
cells['Left Blade'] = openmc.Cell(name='Left Blade')
cells['Left Blade'].fill = materials['B4C']
cells['Left Blade'].region = regions['Left Blade']

# The top-left corner portion of the blade, with no poison
cells['Corner Blade'] = openmc.Cell(name='Corner Blade')
cells['Corner Blade'].fill = materials['Steel']
cells['Corner Blade'].region = regions['Corner Blade']

# Water surrounding all other cells and our assembly
cells['Water Fill'] = openmc.Cell(name='Water Fill')
cells['Water Fill'].fill = materials['Water']
cells['Water Fill'].region = regions['Water Fill']

OpenMC requires that there is a “root” universe. Let us create our root universe and fill it with the cells just defined.

[9]:
# Create root Universe
universes['Root'] = openmc.Universe(name='root universe', universe_id=0)
universes['Root'].add_cells([cells['Assembly'], cells['Top Blade'],
                             cells['Corner Blade'], cells['Left Blade'],
                             cells['Water Fill']])

What do you do after you create your model? Check it! We will use the plotting capabilities of the Python API to do this for us.

When doing so, we will coloring by material with fuel being red, gadolinia-fuel as yellow, zirc cladding as a light grey, water as blue, B4C as black and steel as a darker gray.

[10]:
universes['Root'].plot(origin=(length / 2., length / 2., 0.),
                       pixels=(500, 500), width=(length, length),
                       color_by='material',
                       colors={materials['Fuel']: (1., 0., 0.),
                               materials['Gad']: (1., 1., 0.),
                               materials['Zirc2']: (0.5, 0.5, 0.5),
                               materials['Water']: (0.0, 0.0, 1.0),
                               materials['B4C']: (0.0, 0.0, 0.0),
                               materials['Steel']: (0.4, 0.4, 0.4)})
[10]:
<matplotlib.image.AxesImage at 0x14a9643a2ef0>
_images/examples_mg-mode-part-iii_20_1.png

Looks pretty good to us!

We now must create a geometry that is assigned a root universe and export it to XML.

[11]:
# Create Geometry and set root universe
geometry = openmc.Geometry(universes['Root'])

# Export to "geometry.xml"
geometry.export_to_xml()

With the geometry and materials finished, we now just need to define simulation parameters, including how to run the model and what we want to learn from the model (i.e., define the tallies). We will start with our simulation parameters in the next block.

This will include setting the run strategy, telling OpenMC not to bother creating a tallies.out file, and limiting the verbosity of our output to just the header and results to not clog up our notebook with results from each batch.

[12]:
# OpenMC simulation parameters
batches = 1000
inactive = 20
particles = 1000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}
settings_file.verbosity = 4

# Create an initial uniform spatial source distribution over fissionable zones
bounds = [pin_pitch, pin_pitch, 10, length - pin_pitch, length - pin_pitch, 10]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.Source(space=uniform_dist)

# Export to "settings.xml"
settings_file.export_to_xml()
Create an MGXS Library

Now we are ready to generate multi-group cross sections! First, let’s define a 2-group structure using the built-in EnergyGroups class.

[13]:
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])

Next, we will instantiate an openmc.mgxs.Library for the energy groups with our the problem geometry. This library will use the default setting of isotropically-weighting the multi-group cross sections.

[14]:
# Initialize a 2-group Isotropic MGXS Library for OpenMC
iso_mgxs_lib = openmc.mgxs.Library(geometry)
iso_mgxs_lib.energy_groups = groups

Now, we must specify to the Library which types of cross sections to compute. OpenMC’s multi-group mode can accept isotropic flux-weighted cross sections or angle-dependent cross sections, as well as supporting anisotropic scattering represented by either Legendre polynomials, histogram, or tabular angular distributions.

Just like before, we will create the following multi-group cross sections needed to run an OpenMC simulation to verify the accuracy of our cross sections: “total”, “absorption”, “nu-fission”, ‘“fission”, “nu-scatter matrix”, “multiplicity matrix”, and “chi”. “multiplicity matrix” is needed to provide OpenMC’s multi-group mode with additional information needed to accurately treat scattering multiplication (i.e., (n,xn) reactions)) explicitly.

[15]:
# Specify multi-group cross section types to compute
iso_mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
                           'nu-scatter matrix', 'multiplicity matrix', 'chi']

Now we must specify the type of domain over which we would like the Library to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library supports “material” “cell”, “universe”, and “mesh” domain types.

For the sake of example we will use a mesh to gather our cross sections. This mesh will be set up so there is one mesh bin for every pin cell.

[16]:
# Instantiate a tally Mesh
mesh = openmc.RegularMesh()
mesh.dimension = [10, 10]
mesh.lower_left = [0., 0.]
mesh.upper_right = [length, length]

# Specify a "mesh" domain type for the cross section tally filters
iso_mgxs_lib.domain_type = "mesh"

# Specify the mesh over which to compute multi-group cross sections
iso_mgxs_lib.domains = [mesh]

Now we will set the scattering treatment that we wish to use.

In the mg-mode-part-ii notebook, the cross sections were generated with a typical P3 scattering expansion in mind. Now, however, we will use a more advanced technique: OpenMC will directly provide us a histogram of the change-in-angle (i.e., \(\mu\)) distribution.

Where as in the mg-mode-part-ii notebook, all that was required was to set the legendre_order attribute of mgxs_lib, here we have only slightly more work: we have to tell the Library that we want to use a histogram distribution (as it is not the default), and then tell it the number of bins.

For this problem we will use 11 bins.

[17]:
# Set the scattering format to histogram and then define the number of bins

# Avoid a warning that corrections don't make sense with histogram data
iso_mgxs_lib.correction = None
# Set the histogram data
iso_mgxs_lib.scatter_format = 'histogram'
iso_mgxs_lib.histogram_bins = 11

Ok, we made our isotropic library with histogram-scattering!

Now why don’t we go ahead and create a library to do the same, but with angle-dependent MGXS. That is, we will avoid making the isotropic flux weighting approximation and instead just store a cross section for every polar and azimuthal angle pair.

To do this with the Python API and OpenMC, all we have to do is set the number of polar and azimuthal bins. Here we only need to set the number of bins, the API will convert all of angular space into equal-width bins for us.

Since this problem is symmetric in the z-direction, we only need to concern ourselves with the azimuthal variation here. We will use eight angles.

Ok, we will repeat all the above steps for a new library object, but will also set the number of azimuthal bins at the end.

[18]:
# Let's repeat all of the above for an angular MGXS library so we can gather
# that in the same continuous-energy calculation
angle_mgxs_lib = openmc.mgxs.Library(geometry)
angle_mgxs_lib.energy_groups = groups
angle_mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
                             'nu-scatter matrix', 'multiplicity matrix', 'chi']

angle_mgxs_lib.domain_type = "mesh"
angle_mgxs_lib.domains = [mesh]
angle_mgxs_lib.correction = None
angle_mgxs_lib.scatter_format = 'histogram'
angle_mgxs_lib.histogram_bins = 11

# Set the angular bins to 8
angle_mgxs_lib.num_azimuthal = 8

Now that our libraries have been setup, let’s make sure they contain the types of cross sections which meet the needs of OpenMC’s multi-group solver. Note that this step is done automatically when writing the Multi-Group Library file later in the process (as part of the mgxs_lib.write_mg_library()), but it is a good practice to also run this before spending all the time running OpenMC to generate the cross sections.

[19]:
# Check the libraries - if no errors are raised, then the library is satisfactory.
iso_mgxs_lib.check_library_for_openmc_mgxs()
angle_mgxs_lib.check_library_for_openmc_mgxs()

Lastly, we use our two Library objects to construct the tallies needed to compute all of the requested multi-group cross sections in each domain.

We expect a warning here telling us that the default Legendre order is not meaningful since we are using histogram scattering.

[20]:
# Construct all tallies needed for the multi-group cross section library
iso_mgxs_lib.build_library()
angle_mgxs_lib.build_library()
/home/romano/openmc/openmc/mgxs/mgxs.py:4144: UserWarning: The legendre order will be ignored since the scatter format is set to histogram
  warnings.warn(msg)

The tallies within the libraries can now be exported to a “tallies.xml” input file for OpenMC.

[21]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
iso_mgxs_lib.add_to_tallies_file(tallies_file, merge=True)
angle_mgxs_lib.add_to_tallies_file(tallies_file, merge=True)

In addition, we instantiate a fission rate mesh tally for eventual comparison of results.

[22]:
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)

# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']

# Add tally to collection
tallies_file.append(tally, merge=True)

# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=1.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=2.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=11.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=21.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=22.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=12.
  warn(msg, IDWarning)
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Filter instance already exists with id=18.
  warn(msg, IDWarning)

Time to run the calculation and get our results!

[23]:
# Run OpenMC
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | ed7123f4e7ce7b097c4b2bfb0ef5283eaa8afaea
         Date/Time | 2019-10-04 10:54:35
    OpenMP Threads | 4

 Minimum neutron data temperature: 294.000000 K
 Maximum neutron data temperature: 294.000000 K

 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.83866 +/- 0.00102
 k-effective (Track-length)  = 0.83799 +/- 0.00118
 k-effective (Absorption)    = 0.83968 +/- 0.00103
 Combined k-effective        = 0.83900 +/- 0.00085
 Leakage Fraction            = 0.00000 +/- 0.00000

To make the files available and not be over-written when running the multi-group calculation, we will now rename the statepoint and summary files.

[24]:
# Move the StatePoint File
ce_spfile = './statepoint_ce.h5'
os.rename('statepoint.' + str(batches) + '.h5', ce_spfile)
# Move the Summary file
ce_sumfile = './summary_ce.h5'
os.rename('summary.h5', ce_sumfile)
Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. Let’s begin by loading the StatePoint file, but not automatically linking the summary file.

[25]:
# Load the statepoint file, but not the summary file, as it is a different filename than expected.
sp = openmc.StatePoint(ce_spfile, autolink=False)

In addition to the statepoint file, our simulation also created a summary file which encapsulates information about the materials and geometry. This is necessary for the openmc.Library to properly process the tally data. We first create a Summary object and link it with the statepoint. Normally this would not need to be performed, but since we have renamed our summary file to avoid conflicts with the Multi-Group calculation’s summary file, we will load this in explicitly.

[26]:
su = openmc.Summary(ce_sumfile)
sp.link_with_summary(su)

The statepoint is now ready to be analyzed. To create our libraries we simply have to load the tallies from the statepoint into each Library and our MGXS objects will compute the cross sections for us under-the-hood.

[27]:
# Initialize MGXS Library with OpenMC statepoint data
iso_mgxs_lib.load_from_statepoint(sp)
angle_mgxs_lib.load_from_statepoint(sp)

The next step will be to prepare the input for OpenMC to use our newly created multi-group data.

Isotropic Multi-Group OpenMC Calculation

We will now use the Library to produce the isotropic multi-group cross section data set for use by the OpenMC multi-group solver.

If the model to be run in multi-group mode is the same as the continuous-energy mode, the openmc.mgxs.Library class has the ability to directly create the multi-group geometry, materials, and multi-group library for us. Note that this feature is only useful if the MG model is intended to replicate the CE geometry - it is not useful if the CE library is not the same geometry (like it would be for generating MGXS from a generic spectral region).

This method creates and assigns the materials automatically, including creating a geometry which is equivalent to our mesh cells for which the cross sections were derived.

[28]:
# Allow the API to create our Library, materials, and geometry file
iso_mgxs_file, materials_file, geometry_file = iso_mgxs_lib.create_mg_mode()

# Tell the materials file what we want to call the multi-group library
materials_file.cross_sections = 'mgxs.h5'

# Write our newly-created files to disk
iso_mgxs_file.export_to_hdf5('mgxs.h5')
materials_file.export_to_xml()
geometry_file.export_to_xml()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Universe instance already exists with id=0.
  warn(msg, IDWarning)

Next, we can make the changes we need to the settings file. These changes are limited to telling OpenMC to run a multi-group calculation and provide the location of our multi-group cross section file.

[29]:
# Set the energy mode
settings_file.energy_mode = 'multi-group'

# Export to "settings.xml"
settings_file.export_to_xml()

Let’s clear up the tallies file so it doesn’t include all the extra tallies for re-generating a multi-group library

[30]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()

# Add our fission rate mesh tally
tallies_file.append(tally)

# Export to "tallies.xml"
tallies_file.export_to_xml()

Before running the calculation let’s look at our meshed model. It might not be interesting, but let’s take a look anyways.

[31]:
geometry_file.root_universe.plot(origin=(length / 2., length / 2., 0.),
                       pixels=(300, 300), width=(length, length),
                       color_by='material')
[31]:
<matplotlib.image.AxesImage at 0x14a963c86c88>
_images/examples_mg-mode-part-iii_64_1.png

So, we see a 10x10 grid with a different color for every material, sounds good!

At this point, the problem is set up and we can run the multi-group calculation.

[32]:
# Execute the Isotropic MG OpenMC Run
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | ed7123f4e7ce7b097c4b2bfb0ef5283eaa8afaea
         Date/Time | 2019-10-04 10:56:58
    OpenMP Threads | 4


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.82471 +/- 0.00104
 k-effective (Track-length)  = 0.82466 +/- 0.00103
 k-effective (Absorption)    = 0.82482 +/- 0.00075
 Combined k-effective        = 0.82477 +/- 0.00068
 Leakage Fraction            = 0.00000 +/- 0.00000

Before we go the angle-dependent case, let’s save the StatePoint and Summary files so they don’t get over-written

[33]:
# Move the StatePoint File
iso_mg_spfile = './statepoint_mg_iso.h5'
os.rename('statepoint.' + str(batches) + '.h5', iso_mg_spfile)
# Move the Summary file
iso_mg_sumfile = './summary_mg_iso.h5'
os.rename('summary.h5', iso_mg_sumfile)
Angle-Dependent Multi-Group OpenMC Calculation

Let’s now run the calculation with the angle-dependent multi-group cross sections. This process will be the exact same as above, except this time we will use the angle-dependent Library as our starting point.

We do not need to re-write the materials, geometry, or tallies file to disk since they are the same as for the isotropic case.

[34]:
# Let's repeat for the angle-dependent case
angle_mgxs_lib.load_from_statepoint(sp)
angle_mgxs_file, materials_file, geometry_file = angle_mgxs_lib.create_mg_mode()
angle_mgxs_file.export_to_hdf5()
/home/romano/openmc/openmc/mixin.py:71: IDWarning: Another Universe instance already exists with id=0.
  warn(msg, IDWarning)

At this point, the problem is set up and we can run the multi-group calculation.

[35]:
# Execute the angle-dependent OpenMC Run
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2019 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.11.0-dev
          Git SHA1 | ed7123f4e7ce7b097c4b2bfb0ef5283eaa8afaea
         Date/Time | 2019-10-04 10:57:24
    OpenMP Threads | 4


 ====================>     K EIGENVALUE SIMULATION     <====================


 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.83564 +/- 0.00103
 k-effective (Track-length)  = 0.83572 +/- 0.00104
 k-effective (Absorption)    = 0.83478 +/- 0.00073
 Combined k-effective        = 0.83504 +/- 0.00066
 Leakage Fraction            = 0.00000 +/- 0.00000

Results Comparison

In this section we will compare the eigenvalues and fission rate distributions of the continuous-energy, isotropic multi-group and angle-dependent multi-group cases.

We will begin by loading the multi-group statepoint files, first the isotropic, then angle-dependent. The angle-dependent was not renamed, so we can autolink its summary.

[36]:
# Load the isotropic statepoint file
iso_mgsp = openmc.StatePoint(iso_mg_spfile, autolink=False)
iso_mgsum = openmc.Summary(iso_mg_sumfile)
iso_mgsp.link_with_summary(iso_mgsum)

# Load the angle-dependent statepoint file
angle_mgsp = openmc.StatePoint('statepoint.' + str(batches) + '.h5')
Eigenvalue Comparison

Next, we can load the eigenvalues for comparison and do that comparison

[37]:
ce_keff = sp.k_combined
iso_mg_keff = iso_mgsp.k_combined
angle_mg_keff = angle_mgsp.k_combined

# Find eigenvalue bias
iso_bias = 1.0e5 * (ce_keff - iso_mg_keff)
angle_bias = 1.0e5 * (ce_keff - angle_mg_keff)

Let’s compare the eigenvalues in units of pcm

[38]:
print('Isotropic to CE Bias [pcm]: {0:1.1f}'.format(iso_bias.nominal_value))
print('Angle to CE Bias [pcm]: {0:1.1f}'.format(angle_bias.nominal_value))
Isotropic to CE Bias [pcm]: 1423.5
Angle to CE Bias [pcm]: 396.6

We see a large reduction in error by switching to the usage of angle-dependent multi-group cross sections!

Of course, this rodded and partially voided BWR problem was chosen specifically to exacerbate the angular variation of the reaction rates (and thus cross sections). Such improvements should not be expected in every case, especially if localized absorbers are not present.

It is important to note that both eigenvalues can be improved by the application of finer geometric or energetic discretizations, but this shows that the angle discretization may be a factor for consideration.

Fission Rate Distribution Comparison

Next we will visualize the mesh tally results obtained from our three cases.

This will be performed by first obtaining the one-group fission rate tally information from our state point files. After we have this information we will re-shape the data to match the original mesh laydown. We will then normalize, and finally create side-by-side plots of all.

[39]:
sp_files = [sp, iso_mgsp, angle_mgsp]
titles = ['Continuous-Energy', 'Isotropic Multi-Group',
         'Angle-Dependent Multi-Group']
fiss_rates = []
fig = plt.figure(figsize=(12, 6))
for i, (case, title) in enumerate(zip(sp_files, titles)):
    # Get our mesh tally information
    mesh_tally = case.get_tally(name='mesh tally')
    fiss_rates.append(mesh_tally.get_values(scores=['fission']))

    # Reshape the array
    fiss_rates[-1].shape = mesh.dimension

    # Normalize the fission rates
    fiss_rates[-1] /= np.mean(fiss_rates[-1][fiss_rates[-1] > 0.])

    # Set 0s to NaNs so they show as white
    fiss_rates[-1][fiss_rates[-1] == 0.] = np.nan

    fig = plt.subplot(1, len(titles), i + 1)
    # Plot only the fueled regions
    plt.imshow(fiss_rates[-1][1:-1, 1:-1], cmap='jet', origin='lower',
               vmin=0.4, vmax=4.)
    plt.title(title + '\nFission Rates')
_images/examples_mg-mode-part-iii_80_0.png

With this colormap, dark blue is the lowest power and dark red is the highest power.

We see general agreement between the fission rate distributions, but it looks like there may be less of a gradient near the rods in the continuous-energy and angle-dependent MGXS cases than in the isotropic MGXS case.

To better see the differences, let’s plot ratios of the fission powers for our two multi-group cases compared to the continuous-energy case t

[40]:
# Calculate and plot the ratios of MG to CE for each of the 2 MG cases
ratios = []
fig, axes = plt.subplots(figsize=(12, 6), nrows=1, ncols=2)
for i, (case, title, axis) in enumerate(zip(sp_files[1:], titles[1:], axes.flat)):
    # Get our ratio relative to the CE (in fiss_ratios[0])
    ratios.append(np.divide(fiss_rates[i + 1], fiss_rates[0]))

    # Plot only the fueled regions
    im = axis.imshow(ratios[-1][1:-1, 1:-1], cmap='bwr', origin='lower',
                     vmin = 0.9, vmax = 1.1)
    axis.set_title(title + '\nFission Rates Relative\nto Continuous-Energy')

# Add a color bar
fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
fig.colorbar(im, cax=cbar_ax)
[40]:
<matplotlib.colorbar.Colorbar at 0x14a961258eb8>
_images/examples_mg-mode-part-iii_82_1.png

With this ratio its clear that the errors are significantly worse in the isotropic case. These errors are conveniently located right where the most anisotropy is espected: by the control blades and by the Gd-bearing pins!

Unstructured Mesh

Unstructured Mesh: Introduction

In this example we’ll look at how to setup and use unstructured mesh tallies in OpenMC. Unstructured meshes are able to provide results over spatial regions of a problem while conforming to a specific geometric features – something that is often difficult to do using the regular and rectilinear meshes in OpenMC.

Here, we’ll apply an unstructured mesh tally to the PWR assembly model from the OpenMC examples.

*NOTE: This notebook will not run successfully if OpenMC has not been built with DAGMC support enabled.*

[1]:
from IPython.display import Image
import openmc
import openmc.lib

assert(openmc.lib._dagmc_enabled())

We’ll need to download the unstructured mesh file used in this notebook. We’ll be retrieving those using the function and URLs below.

[2]:
%matplotlib inline
from matplotlib import pyplot as plt
plt.rcParams["figure.figsize"] = (30,10)

import urllib.request

pin_mesh_url = 'https://tinyurl.com/u9ce9d7' # 1.2 MB

def download(url, filename='dagmc.h5m'):
    """
    Helper function for retrieving dagmc models
    """
    u = urllib.request.urlopen(url)

    if u.status != 200:
        raise RuntimeError("Failed to download file.")

    # save file as dagmc.h5m
    with open(filename, 'wb') as f:
        f.write(u.read())

First we’ll import that model from the set of OpenMC examples.

[3]:
model = openmc.examples.pwr_assembly()

We’ll make a couple of adjustments to this 2D model as it won’t play very well with the 3D mesh we’ll be looking at. First, we’ll bound the pincell between +/- 10 cm in the Z dimension.

[4]:
min_z = openmc.ZPlane(z0=-10.0)
max_z = openmc.ZPlane(z0=10.0)

z_region = +min_z & -max_z

cells = model.geometry.get_all_cells()
for cell in cells.values():
    cell.region &= z_region

The other adjustment we’ll make is to remove the reflective boundary conditions on the X and Y boundaries. (This is purely to generate a more interesting flux profile.)

[5]:
surfaces = model.geometry.get_all_surfaces()
# modify the boundary condition of the
# planar surfaces bounding the assembly
for surface in surfaces.values():
    if isinstance(surface, openmc.Plane):
        surface.boundary_type = 'vacuum'

Let’s take a quick look at the model to ensure our changs have been added properly.

[6]:
root_univ = model.geometry.root_universe

# axial image
root_univ.plot(width=(22.0, 22.0),
               pixels=(200, 300),
               basis='xz',
               color_by='material',
               seed=0)
[6]:
<matplotlib.image.AxesImage at 0x7fe9650cd320>
_images/examples_unstructured-mesh-part-i_11_1.png
[7]:
# radial image
root_univ.plot(width=(22.0, 22.0),
               pixels=(400, 400),
               basis='xy',
               color_by='material',
               seed=0)
[7]:
<matplotlib.image.AxesImage at 0x7fe967252e48>
_images/examples_unstructured-mesh-part-i_12_1.png

Looks good! Let’s run some particles through the problem.

[8]:
model.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2020 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.12.0-dev
          Git SHA1 | e7eceff2aa3a9bbfd4dd553f585d1a31b051ddb3
         Date/Time | 2020-03-27 11:18:32
    OpenMP Threads | 2

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading geometry XML file...
 Reading U234 from /home/shriwise/opt/openmc/xs/nndc_hdf5/U234.h5
 Reading U235 from /home/shriwise/opt/openmc/xs/nndc_hdf5/U235.h5
 Reading U238 from /home/shriwise/opt/openmc/xs/nndc_hdf5/U238.h5
 Reading O16 from /home/shriwise/opt/openmc/xs/nndc_hdf5/O16.h5
 Reading Zr90 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Zr90.h5
 Reading Zr91 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Zr91.h5
 Reading Zr92 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Zr92.h5
 Reading Zr94 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Zr94.h5
 Reading Zr96 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Zr96.h5
 Reading H1 from /home/shriwise/opt/openmc/xs/nndc_hdf5/H1.h5
 Reading B10 from /home/shriwise/opt/openmc/xs/nndc_hdf5/B10.h5
 Reading B11 from /home/shriwise/opt/openmc/xs/nndc_hdf5/B11.h5
 Reading c_H_in_H2O from /home/shriwise/opt/openmc/xs/nndc_hdf5/c_H_in_H2O.h5
 Maximum neutron transport energy: 20000000.000000 eV for U235
 Minimum neutron data temperature: 294.000000 K
 Maximum neutron data temperature: 294.000000 K
 Preparing distributed cell instances...
 Writing summary.h5 file...
 Initializing source particles...

 ====================>     K EIGENVALUE SIMULATION     <====================

  Bat./Gen.      k            Average k
  =========   ========   ====================
        1/1    0.20444
        2/1    0.15502
        3/1    0.19804
        4/1    0.22159
        5/1    0.19776
        6/1    0.20086
        7/1    0.21896    0.20991 +/- 0.00905
        8/1    0.23134    0.21706 +/- 0.00885
        9/1    0.29029    0.23536 +/- 0.01935
       10/1    0.20094    0.22848 +/- 0.01649
 Creating state point statepoint.10.h5...

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 6.8458e-01 seconds
   Reading cross sections          = 6.7039e-01 seconds
 Total time in simulation          = 1.9462e-02 seconds
   Time in transport only          = 1.7538e-02 seconds
   Time in inactive batches        = 9.2816e-03 seconds
   Time in active batches          = 1.0181e-02 seconds
   Time synchronizing fission bank = 5.2594e-05 seconds
     Sampling source sites         = 4.6704e-05 seconds
     SEND/RECV source sites        = 3.1580e-06 seconds
   Time accumulating tallies       = 2.0290e-06 seconds
 Total time for finalization       = 1.0320e-06 seconds
 Total time elapsed                = 7.0439e-01 seconds
 Calculation Rate (inactive)       = 53869.9 particles/second
 Calculation Rate (active)         = 49113.3 particles/second

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 0.25094 +/- 0.02010
 k-effective (Track-length)  = 0.22848 +/- 0.01649
 k-effective (Absorption)    = 0.21556 +/- 0.04156
 Combined k-effective        = 0.20707 +/- 0.01965
 Leakage Fraction            = 0.79200 +/- 0.03382

[8]:
0.20706788967181938+/-0.019653037754288005

Now it’s time to apply our mesh tally to the problem. We’ll be using the tetrahedral mesh “pins1-4.h5m” shown below:

[9]:
Image("./images/pin_mesh.png", width=600)
[9]:
_images/examples_unstructured-mesh-part-i_16_0.png

This mesh was generated using Trelis with radii that match the fuel/coolant channels of the PWR model. These four channels correspond to the highlighted channels of the assembly below.

Two of the channels are coolant and the other two are fuel.

[10]:
from matplotlib.patches import Rectangle
from matplotlib import pyplot as plt

pitch = 1.26 # cm

img = root_univ.plot(width=(22.0, 22.0),
                           pixels=(600, 600),
                           basis='xy',
                           color_by='material',
                           seed=0)

# highlight channels
for i in range(0, 4):
    corner = (i * pitch - pitch / 2.0, -i * pitch - pitch / 2.0)
    rect = Rectangle(corner,
                     pitch,
                     pitch,
                     edgecolor='blue',
                     fill=False)
    img.axes.add_artist(rect)

_images/examples_unstructured-mesh-part-i_18_0.png
Applying an unstructured mesh tally

To use this mesh, we’ll create an unstructured mesh instance and apply it to a mesh filter.

[11]:
download(pin_mesh_url, "pins1-4.h5m")
umesh = openmc.UnstructuredMesh("pins1-4.h5m")
mesh_filter = openmc.MeshFilter(umesh)

We can now apply this filter like any other. For this demonstration we’ll score both the flux and heating in these pins.

[12]:
tally = openmc.Tally()
tally.filters = [mesh_filter]
tally.scores = ['heating', 'flux']
tally.estimator = 'tracklength'
model.tallies = [tally]

Now we’ll run this model with the unstructured mesh tally applied. Notice that the simulation takes some time to start due to some additional data structures used by the unstructured mesh tally. Additionally, the particle rate drops dramatically during the active cycles of this simulation.

Unstructured meshes are useful, but they can be computationally expensive!

[13]:
model.settings.particles = 100_000
model.settings.inactive = 20
model.settings.batches = 100
model.run(output=False)
[13]:
0.2311872124292096+/-0.00017018932313754055

At the end of the simulation, we see the statepoint file along with a file named “tally_1.100.vtk”. This file contains the results of the unstructured mesh tally with convenient labels for the scores applied. In our case the following scores will be present in the VTK:

  • flux_total_value
  • flux_total_std_dev
  • heating_total_value
  • heading_total_std_dev

Where “total” represents

Currently, an unstructured VTK file will only be generated for tallies if the unstructured mesh is is the only filter applied to that tally. All results for the unstructured mesh tally are present in the statepoint file regardless of the number of filters applied, however.

These files can be viewed using free tools like Paraview and VisIt to examine the results.

[14]:
!ls *.vtk
tally_1.100.vtk
Flux
[15]:
Image("./images/umesh_flux.png", width=600)
[15]:
_images/examples_unstructured-mesh-part-i_29_0.png
Heating

Here is an image of the heating score as viewed in VisIt. Note that no heating is scored in the water-filled channels as expected.

[16]:
Image("./images/umesh_heating.png", width=600)
[16]:
_images/examples_unstructured-mesh-part-i_31_0.png
Statepoint Data
[17]:
with openmc.StatePoint("statepoint.100.h5") as sp:
    tally = sp.tallies[1]
    umesh = sp.meshes[1]

Enough information for visualization of results on the unstructured mesh is also provided in the statepoint file. Namely, the mesh element volumes and centroids are available.

[18]:
print(umesh.volumes)
print(umesh.centroids)
[1.43381086e-04 1.48043747e-04 1.60408339e-04 ... 7.04197023e-05
 7.04197023e-05 7.04197023e-05]
[[ 2.88485691 -2.55429784  9.97768184]
 [ 2.87565092 -2.60469781  9.8884092 ]
 [ 2.85832254 -2.65291228  9.97768184]
 ...
 [ 1.46082175 -1.15569203 -3.62914358]
 [ 1.4443143  -1.1321793  -3.65475081]
 [ 1.46884412 -1.15657736 -3.68206543]]

The combination of these values can provide for an appoxmiate visualization of the unstructured mesh without its explicit representation or use of an additional mesh library.

We hope you’ve found this example notebook useful!

[19]:
Image("./images/umesh_w_assembly.png", width=600)
[19]:
_images/examples_unstructured-mesh-part-i_37_0.png

Unstructured Mesh: Tallies with CAD and Point Cloud Visualization

In the first notebook on this topic, we looked at how to set up a tally using an unstructured mesh in OpenMC. In this notebook, we will explore using unstructured mesh in conjunction with CAD-based geometry to perform detailed geometry analysis on complex geomerty.

*NOTE: This notebook will not run successfully if OpenMC has not been built with DAGMC support enabled.*

[1]:
import os
from IPython.display import Image
import openmc
import openmc.lib

assert(openmc.lib._dagmc_enabled())

We’ll need to download our DAGMC geometry and unstructured mesh files. We’ll be retrieving those using the function and URLs below.

[2]:
from IPython.display import display, clear_output
import urllib.request

manifold_geom_url = 'https://tinyurl.com/rp7grox' # 99 MB
manifold_mesh_url = 'https://tinyurl.com/wojemuh' # 5.4 MB


def download(url, filename):
    """
    Helper function for retrieving dagmc models
    """
    def progress_hook(count, block_size, total_size):
        prog_percent = 100 * count * block_size / total_size
        prog_percent = min(100., prog_percent)
        clear_output(wait=True)
        display('Downloading {}: {:.1f}%'.format(filename, prog_percent))

    urllib.request.urlretrieve(url, filename, progress_hook)

The model we’ll be looking at in this example is a steel piping manifold:

[3]:
Image("./images/manifold-cad.png", width=800)
[3]:
_images/examples_unstructured-mesh-part-ii_5_0.png

This is a nice example of a model which would be extremely difficult to model using CSG. To get started, we’ll need two files: 1. the DAGMC gometry file on which we’ll track particles and 2. a tetrahedral mesh of the piping structure on which we’ll score tallies

To start, let’s create the materials we’ll need for this problem. The pipes are steel and we’ll model the surrounding area as air.

[4]:
air = openmc.Material(name='air')
air.set_density('g/cc', 0.001205)
air.add_element('N', 0.784431)
air.add_element('O', 0.210748)
air.add_element('Ar',0.0046)

steel = openmc.Material(name='steel')
steel.set_density('g/cc', 8.0)
steel.add_element('Si', 0.010048)
steel.add_element('S', 0.00023)
steel.add_element('Fe', 0.669)
steel.add_element('Ni', 0.12)
steel.add_element('Mo', 0.025)
steel.add_nuclide('P31',0.00023)
steel.add_nuclide('Mn55',0.011014)

materials = openmc.Materials([air, steel])
materials.export_to_xml()

Now let’s download the geometry and mesh files. (This may take some time.)

[5]:
# get the manifold DAGMC geometry file
download(manifold_geom_url, 'dagmc.h5m')
# get the manifold tet mesh
download(manifold_mesh_url, 'manifold.h5m')
'Downloading manifold.h5m: 100.0%'

Next we’ll create a 5 MeV neutron point source at the entrance the single pipe on the low side of the model with

[6]:
src_pnt = openmc.stats.Point(xyz=(0.0, 0.0, 0.0))
src_energy = openmc.stats.Discrete(x=[5.e+06], p=[1.0])

source = openmc.Source(space=src_pnt, energy=src_energy)

settings = openmc.Settings()
settings.source = source

settings.run_mode = "fixed source"
settings.batches = 10
settings.particles = 100

And we’ll indicate that we’re using a CAD-based geometry.

[7]:
settings.dagmc = True

settings.export_to_xml()

We’ll run a few particles through this geometry to make sure everything is working properly.

[8]:
openmc.run()
                                %%%%%%%%%%%%%%%
                           %%%%%%%%%%%%%%%%%%%%%%%%
                        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                                     %%%%%%%%%%%%%%%%%%%%%%%%
                 ###############      %%%%%%%%%%%%%%%%%%%%%%%%
                ##################     %%%%%%%%%%%%%%%%%%%%%%%
                ###################     %%%%%%%%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%%%%%%
                #####################     %%%%%%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%%
                 #######################     %%%%%%%%%%%%%%%%%
                 ######################     %%%%%%%%%%%%%%%%%
                  ####################     %%%%%%%%%%%%%%%%%
                    #################     %%%%%%%%%%%%%%%%%
                     ###############     %%%%%%%%%%%%%%%%
                       ############     %%%%%%%%%%%%%%%
                          ########     %%%%%%%%%%%%%%
                                      %%%%%%%%%%%

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2020 MIT and OpenMC contributors
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.12.0-dev
          Git SHA1 | c9cbdb7c70b202e847c7169f9e5602f110a43853
         Date/Time | 2020-04-24 09:25:02
    OpenMP Threads | 8

 Reading settings XML file...
 Reading cross sections XML file...
 Reading materials XML file...
 Reading DAGMC geometry...
Loading file dagmc.h5m
Initializing the GeomQueryTool...
Using faceting tolerance: 0.001
Building OBB Tree...
 Reading N14 from /home/shriwise/opt/openmc/xs/nndc_hdf5/N14.h5
 Reading N15 from /home/shriwise/opt/openmc/xs/nndc_hdf5/N15.h5
 Reading O16 from /home/shriwise/opt/openmc/xs/nndc_hdf5/O16.h5
 Reading O17 from /home/shriwise/opt/openmc/xs/nndc_hdf5/O17.h5
 Reading Ar36 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ar36.h5
 WARNING: Negative value(s) found on probability table for nuclide Ar36 at 294K
 Reading Ar38 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ar38.h5
 Reading Ar40 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ar40.h5
 Reading Si28 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Si28.h5
 Reading Si29 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Si29.h5
 Reading Si30 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Si30.h5
 Reading S32 from /home/shriwise/opt/openmc/xs/nndc_hdf5/S32.h5
 Reading S33 from /home/shriwise/opt/openmc/xs/nndc_hdf5/S33.h5
 Reading S34 from /home/shriwise/opt/openmc/xs/nndc_hdf5/S34.h5
 Reading S36 from /home/shriwise/opt/openmc/xs/nndc_hdf5/S36.h5
 Reading Fe54 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe54.h5
 Reading Fe56 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe56.h5
 Reading Fe57 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe57.h5
 Reading Fe58 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Fe58.h5
 Reading Ni58 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ni58.h5
 Reading Ni60 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ni60.h5
 Reading Ni61 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ni61.h5
 Reading Ni62 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ni62.h5
 Reading Ni64 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Ni64.h5
 Reading Mo100 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo100.h5
 Reading Mo92 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo92.h5
 Reading Mo94 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo94.h5
 Reading Mo95 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo95.h5
 Reading Mo96 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo96.h5
 Reading Mo97 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo97.h5
 Reading Mo98 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mo98.h5
 Reading P31 from /home/shriwise/opt/openmc/xs/nndc_hdf5/P31.h5
 Reading Mn55 from /home/shriwise/opt/openmc/xs/nndc_hdf5/Mn55.h5
 Maximum neutron transport energy: 20000000.000000 eV for N15
 Minimum neutron data temperature: 294.000000 K
 Maximum neutron data temperature: 294.000000 K
 Reading tallies XML file...
 Preparing distributed cell instances...
 Writing summary.h5 file...

 ===============>     FIXED SOURCE TRANSPORT SIMULATION     <===============

 Simulating batch 1
 Simulating batch 2
 Simulating batch 3
 Simulating batch 4
 Simulating batch 5
 Simulating batch 6
 Simulating batch 7
 Simulating batch 8
 Simulating batch 9
 Simulating batch 10
 Creating state point statepoint.10.h5...
 WARNING: Skipping unstructured mesh writing for tally 1. More than one filter
          is present on the tally.

 =======================>     TIMING STATISTICS     <=======================

 Total time for initialization     = 4.3651e+01 seconds
   Reading cross sections          = 2.1907e+00 seconds
 Total time in simulation          = 3.4743e-01 seconds
   Time in transport only          = 2.9555e-01 seconds
   Time in active batches          = 3.4743e-01 seconds
   Time accumulating tallies       = 7.6575e-03 seconds
 Total time for finalization       = 2.2273e-01 seconds
 Total time elapsed                = 4.4224e+01 seconds
 Calculation Rate (active)         = 2878.27 particles/second

 ============================>     RESULTS     <============================

 Leakage Fraction            = 0.96800 +/- 0.00573

Now let’s setup the unstructured mesh tally. We’ll do this the same way we did in the previous notebook.

[9]:
unstructured_mesh = openmc.UnstructuredMesh("manifold.h5m")

mesh_filter = openmc.MeshFilter(unstructured_mesh)

tally = openmc.Tally()
tally.filters = [mesh_filter]
tally.scores = ['flux']
tally.estimator = 'tracklength'

tallies = openmc.Tallies([tally])
tallies.export_to_xml()
[10]:
settings.batches = 200
settings.particles = 5000
settings.export_to_xml()
[11]:
openmc.run(output=False)

Again we should see that tally_1.200.vtk file which we can use to visualize our results in VisIt, ParaView, or another tool of your choice that supports VTK files.

[12]:
!ls *.vtk
manifold_flux.vtk  tally_1.100.vtk  tally_1.200.vtk
manifold.vtk       tally_1.10.vtk
[13]:
Image("./images/manifold_flux.png", width="800")
[13]:
_images/examples_unstructured-mesh-part-ii_22_0.png

For the purpose of this example, we haven’t run enough particles to score in all of the tet elements, but we indeed see larger flux values near the source location at the bottom of the model.

Visualization with statepoint data

It was mentioned in the previous unstructured mesh example that the centroids and volumes of elements are written to the state point file. Here, we’ll explore how to use that information to produce point cloud information for visualization of this data.

This is particularly important when combining an unstructured mesh tally with other filters as a .vtk file will not automatically be written with the statepoint file in that scenario. To demonstrate this, let’s setup a tally similar to the one above, but add an energy filter and re-run the model.

[14]:
# energy filter with bins from 0 to 1 MeV and 1 MeV to 5 MeV
energy_filter = openmc.EnergyFilter((0.0, 1.e+06, 5.e+06))

tally.filters = [mesh_filter, energy_filter]
print(tally)
print(energy_filter)
tallies.export_to_xml()
Tally
        ID             =        1
        Name           =
        Filters        =        MeshFilter, EnergyFilter
        Nuclides       =
        Scores         =        ['flux']
        Estimator      =        tracklength
EnergyFilter
        Values         =        [      0. 1000000. 5000000.]
        ID             =        2

[15]:
!cat tallies.xml
<?xml version='1.0' encoding='utf-8'?>
<tallies>
  <mesh id="1" type="unstructured">
    <filename>manifold.h5m</filename>
  </mesh>
  <filter id="1" type="mesh">
    <bins>1</bins>
  </filter>
  <filter id="2" type="energy">
    <bins>0.0 1000000.0 5000000.0</bins>
  </filter>
  <tally id="1">
    <filters>1 2</filters>
    <scores>flux</scores>
    <estimator>tracklength</estimator>
  </tally>
</tallies>
[16]:
openmc.run(output=False)

Noice the warning at the end of the output above indicating that the .vtk file we used before isn’t written in this case.

Let’s open up this statepoint file and get the information we need to create the point cloud data instead.

*NOTE: You will need the Python vtk module installed to run this part of the notebook.*

[17]:
with openmc.StatePoint("statepoint.200.h5") as sp:
    tally = sp.tallies[1]

    umesh = sp.meshes[1]
    centroids = umesh.centroids
    mesh_vols = umesh.volumes

    thermal_flux = tally.get_values(scores=['flux'],
                                    filters=[openmc.EnergyFilter],
                                    filter_bins=[((0.0, 1.e+06),)])
    fast_flux = tally.get_values(scores=['flux'],
                                 filters=[openmc.EnergyFilter],
                                 filter_bins=[((1.e+06, 5.e+06),)])
[18]:
data_dict = {'Flux 0 - 1 MeV' : thermal_flux,
             'Flux 1 - 5 MeV' : fast_flux,
             'Total Flux' : thermal_flux + fast_flux}

umesh.write_data_to_vtk("manifold", data_dict)
/home/shriwise/.pyenv/versions/3.7.3/lib/python3.7/site-packages/vtk/util/numpy_support.py:137: FutureWarning: Conversion of the second argument of issubdtype from `complex` to `np.complexfloating` is deprecated. In future, it will be treated as `np.complex128 == np.dtype(complex).type`.
  assert not numpy.issubdtype(z.dtype, complex), \

We should now see our new flux file in the directory. It can be used to visualize the results in the same way as our other .vtk files.

[19]:
!ls *.vtk
manifold_flux.vtk  tally_1.100.vtk  tally_1.200.vtk
manifold.vtk       tally_1.10.vtk
[20]:
Image("./images/manifold_pnt_cld.png", width=800)
[20]:
_images/examples_unstructured-mesh-part-ii_33_0.png

Release Notes

What’s New in 0.12.1

Summary

This release of OpenMC includes an assortment of new features and many bug fixes. The openmc.deplete module incorporates a number of improvements in usability, accuracy, and performance. Other enhancements include generalized rotational periodic boundary conditions, expanded source modeling capabilities, and a capability to generate windowed multipole library files from ENDF files.

New Features

  • Boundary conditions have been refactored and generalized. Rotational periodic boundary conditions can now be applied to any N-fold symmetric geometry.
  • External source distributions have been refactored and extended. Users writing their own C++ custom sources need to write a class that derives from openmc::Source. These changes have enabled new functionality, such as:
    • Mixing more than one custom source library together
    • Mixing a normal source with a custom source
    • Using a file-based source for fixed source simulations
    • Using a file-based source for eigenvalue simulations even when the number of particles doesn’t match
  • New capability to read and write a source file based on particles that cross a surface (known as a “surface source”).
  • Various improvements related to depletion:
    • Reactions used in a depletion chain can now be configured through the reactions argument to openmc.deplete.Chain.from_endf().
    • Specifying a power of zero during a depletion simulation no longer results in an unnecessary transport solve.
    • Reaction rates can be computed either directly or using multigroup flux tallies that are used to collapse reaction rates afterward. This is enabled through the reaction_rate_mode and reaction_rate_opts to openmc.deplete.Operator.
    • Depletion results can be used to create a new openmc.Materials object using the openmc.deplete.ResultsList.export_to_materials() method.
  • Multigroup current and diffusion cross sections can be generated through the openmc.mgxs.Current and openmc.mgxs.DiffusionCoefficient classes.
  • Added openmc.data.isotopes() function that returns a list of naturally occurring isotopes for a given element.
  • Windowed multipole libraries can now be generated directly from the Python API using openmc.data.WindowedMultipole.from_endf().
  • The new openmc.write_source_file() function allows source files to be generated programmatically.

What’s New in 0.12.0

Summary

This release of OpenMC includes an assortment of new features and many bug fixes. In particular, the openmc.deplete module has been heavily tested which has resulted in a number of usability improvements, bug fixes, and other enhancements. Energy deposition calculations, particularly for coupled neutron-photon simulations, have been improved as well.

Improvements in modeling capabilities continue to be added to the code, including the ability to rotate surfaces in the Python API, several new “composite” surfaces, a variety of new methods on openmc.Material, unstructured mesh tallies that leverage the existing DAGMC infrastructure, effective dose coefficients from ICRP-116, and a new cell instance tally filter.

New Features

What’s New in 0.11.0

Summary

This release of OpenMC adds several major new features: depletion, photon transport, and support for CAD geometries through DAGMC. In addition, the core codebase has been rewritten in C++14 (it was previously written in Fortran 2008). This makes compiling the code considerably simpler as no Fortran compiler is needed.

Functional expansion tallies are now supported through several new tally filters that can be arbitrarily combined:

Note that these filters replace the use expansion scores like scatter-P1. Instead, a normal scatter score should be used along with a openmc.LegendreFilter.

The interface for random sphere packing has been significantly improved. A new openmc.model.pack_spheres() function takes a region and generates a random, non-overlapping configuration of spheres within the region.

New Features

  • White boundary conditions can be applied to surfaces
  • Support for rectilinear meshes through openmc.RectilinearMesh.
  • The Geometry, Materials, and Settings classes now have a from_xml method that will build an instance from an existing XML file.
  • Predefined energy group structures can be found in openmc.mgxs.GROUP_STRUCTURES.
  • New tally scores: H1-production, H2-production, H3-production, He3-production, He4-production, heating, heating-local, and damage-energy.
  • Switched to cell-based neighor lists (PR 1140)
  • Two new probability distributions that can be used for source distributions: openmc.stats.Normal and openmc.stats.Muir
  • The openmc.data module now supports reading and sampling from ENDF File 32 resonance covariance data (PR 1024).
  • Several new convenience functions/methods have been added:

Python API Changes

  • All surface classes now have coefficient arguments given as lowercase names.

  • The order of arguments in surface classes has been changed so that coefficients are the first arguments (rather than the optional surface ID). This means you can now write:

    x = openmc.XPlane(5.0, 'reflective')
    zc = openmc.ZCylinder(0., 0., 10.)
    
  • The Mesh class has been renamed openmc.RegularMesh.

  • The get_rectangular_prism function has been renamed openmc.model.rectangular_prism().

  • The get_hexagonal_prism function has been renamed openmc.model.hexagonal_prism().

  • Python bindings to the C/C++ API have been move from openmc.capi to openmc.lib.

What’s New in 0.10.0

This release of OpenMC includes several new features, performance improvements, and bug fixes compared to version 0.9.0. Notably, a C API has been added that enables in-memory coupling of neutronics to other physics fields, e.g., burnup calculations and thermal-hydraulics. The C API is also backed by Python bindings in a new openmc.capi package. Users should be forewarned that the C API is still in an experimental state and the interface is likely to undergo changes in future versions.

The Python API continues to improve over time; several backwards incompatible changes were made in the API which users of previous versions should take note of:

  • To indicate that nuclides in a material should be treated such that elastic scattering is isotropic in the laboratory system, there is a new Material.isotropic property:

    mat = openmc.Material()
    mat.add_nuclide('H1', 1.0)
    mat.isotropic = ['H1']
    

    To treat all nuclides in a material this way, the Material.make_isotropic_in_lab() method can still be used.

  • The initializers for openmc.Intersection and openmc.Union now expect an iterable.

  • Auto-generated unique IDs for classes now start from 1 rather than 10000.

Attention

This is the last release of OpenMC that will support Python 2.7. Future releases of OpenMC will require Python 3.4 or later.

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions and Mac OS X. Numerous users have reported working builds on Microsoft Windows, but your mileage may vary. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides and tallies in the problem).

New Features

  • Rotationally-periodic boundary conditions
  • C API (with Python bindings) for in-memory coupling
  • Improved correlation for Uranium enrichment
  • Support for partial S(a,b) tables
  • Improved handling of autogenerated IDs
  • Many performance/memory improvements

Bug Fixes

  • 937469: Fix energy group sampling for multi-group simulations
  • a149ef: Ensure mutable objects are not hashable
  • 2c9b21: Preserve backwards compatibility for generated HDF5 libraries
  • 8047f6: Handle units of division for tally arithmetic correctly
  • 0beb4c: Compatibility with newer versions of Pandas
  • f124be: Fix generating 0K data with openmc.data.njoy module
  • 0c6915: Bugfix for generating thermal scattering data
  • 61ecb4: Fix bugs in Python multipole objects

What’s New in 0.9.0

This release of OpenMC is the first release to use a new native HDF5 cross section format rather than ACE format cross sections. Other significant new features include a nuclear data interface in the Python API (openmc.data) a stochastic volume calculation capability, a random sphere packing algorithm that can handle packing fractions up to 60%, and a new XML parser with significantly better performance than the parser used previously.

Caution

With the new cross section format, the default energy units are now electronvolts (eV) rather than megaelectronvolts (MeV)! If you are specifying an energy filter for a tally, make sure you use units of eV now.

The Python API continues to improve over time; several backwards incompatible changes were made in the API which users of previous versions should take note of:

  • Each type of tally filter is now specified with a separate class. For example:

    energy_filter = openmc.EnergyFilter([0.0, 0.625, 4.0, 1.0e6, 20.0e6])
    
  • Several attributes of the Plot class have changed (color -> color_by and col_spec > colors). Plot.colors now accepts a dictionary mapping Cell or Material instances to RGB 3-tuples or string colors names, e.g.:

    plot.colors = {
        fuel: 'yellow',
        water: 'blue'
    }
    
  • make_hexagon_region is now get_hexagonal_prism()

  • Several changes in Settings attributes:

    • weight is now set as Settings.cutoff['weight']
    • Shannon entropy is now specified by passing a openmc.Mesh to Settings.entropy_mesh
    • Uniform fission site method is now specified by passing a openmc.Mesh to Settings.ufs_mesh
    • All sourcepoint_* options are now specified in a Settings.sourcepoint dictionary
    • Resonance scattering method is now specified as a dictionary in Settings.resonance_scattering
    • Multipole is now turned on by setting Settings.temperature['multipole'] = True
    • The output_path attribute is now Settings.output['path']
  • All the openmc.mgxs.Nu* classes are gone. Instead, a nu argument was added to the constructor of the corresponding classes.

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions and Mac OS X. Numerous users have reported working builds on Microsoft Windows, but your mileage may vary. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides and tallies in the problem).

New Features

  • Stochastic volume calculations
  • Multi-delayed group cross section generation
  • Ability to calculate multi-group cross sections over meshes
  • Temperature interpolation on cross section data
  • Nuclear data interface in Python API, openmc.data
  • Allow cutoff energy via Settings.cutoff
  • Ability to define fuel by enrichment (see Material.add_element())
  • Random sphere packing for TRISO particle generation, openmc.model.pack_trisos()
  • Critical eigenvalue search, openmc.search_for_keff()
  • Model container, openmc.model.Model
  • In-line plotting in Jupyter, openmc.plot_inline()
  • Energy function tally filters, openmc.EnergyFunctionFilter
  • Replaced FoX XML parser with pugixml
  • Cell/material instance counting, Geometry.determine_paths()
  • Differential tallies (see openmc.TallyDerivative)
  • Consistent multi-group scattering matrices
  • Improved documentation and new Jupyter notebooks
  • OpenMOC compatibility module, openmc.openmoc_compatible

Bug Fixes

  • c5df6c: Fix mesh filter max iterator check
  • 1cfa39: Reject external source only if 95% of sites are rejected
  • 335359: Fix bug in plotting meshlines
  • 17c678: Make sure system_clock uses high-resolution timer
  • 23ec0b: Fix use of S(a,b) with multipole data
  • 7eefb7: Fix several bugs in tally module
  • 7880d4: Allow plotting calculation with no boundary conditions
  • ad2d9f: Fix filter weight missing when scoring all nuclides
  • 59fdca: Fix use of source files for fixed source calculations
  • 9eff5b: Fix thermal scattering bugs
  • 7848a9: Fix combined k-eff estimator producing NaN
  • f139ce: Fix printing bug for tallies with AggregateNuclide
  • b8ddfa: Bugfix for short tracks near tally mesh edges
  • ec3cfb: Fix inconsistency in filter weights
  • 5e9b06: Fix XML representation for verbosity
  • c39990: Fix bug tallying reaction rates with multipole on
  • c6b67e: Fix fissionable source sampling bug
  • 489540: Check for void materials in tracklength tallies
  • f0214f: Fixes/improvements to the ARES algorithm

Contributors

This release contains new contributions from the following people:

What’s New in 0.8.0

This release of OpenMC includes a few new major features including the capability to perform neutron transport with multi-group cross section data as well as experimental support for the windowed multipole method being developed at MIT. Source sampling options have also been expanded significantly, with the option to supply arbitrary tabular and discrete distributions for energy, angle, and spatial coordinates.

The Python API has been significantly restructured in this release compared to version 0.7.1. Any scripts written based on the version 0.7.1 API will likely need to be rewritten. Some of the most visible changes include the following:

  • SettingsFile is now Settings, MaterialsFile is now Materials, and TalliesFile is now Tallies.
  • The GeometryFile class no longer exists and is replaced by the Geometry class which now has an export_to_xml() method.
  • Source distributions are defined using the Source class and assigned to the Settings.source property.
  • The Executor class no longer exists and is replaced by openmc.run() and openmc.plot_geometry() functions.

The Python API documentation has also been significantly expanded.

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions and Mac OS X. Numerous users have reported working builds on Microsoft Windows, but your mileage may vary. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides and tallies in the problem).

New Features

  • Multi-group mode
  • Vast improvements to the Python API
  • Experimental windowed multipole capability
  • Periodic boundary conditions
  • Expanded source sampling options
  • Distributed materials
  • Subcritical multiplication support
  • Improved method for reproducible URR table sampling
  • Refactor of continuous-energy reaction data
  • Improved documentation and new Jupyter notebooks

Bug Fixes

  • 70daa7: Make sure MT=3 cross section is not used
  • 40b05f: Ensure source bank is resampled for fixed source runs
  • 9586ed: Fix two hexagonal lattice bugs
  • a855e8: Make sure graphite models don’t error out on max events
  • 7294a1: Fix incorrect check on cmfd.xml
  • 12f246: Ensure number of realizations is written to statepoint
  • 0227f4: Fix bug when sampling multiple energy distributions
  • 51deaa: Prevent segfault when user specifies ‘18’ on tally scores
  • fed74b: Prevent duplicate tally scores
  • 8467ae: Better threshold for allowable lost particles
  • 493c6f: Fix type of return argument for h5pget_driver_f

Contributors

This release contains new contributions from the following people:

What’s New in 0.7.1

This release of OpenMC provides some substantial improvements over version 0.7.0. Non-simple cell regions can now be defined through the | (union) and ~ (complement) operators. Similar changes in the Python API also allow complex cell regions to be defined. A true secondary particle bank now exists; this is crucial for photon transport (to be added in the next minor release). A rich API for multi-group cross section generation has been added via the openmc.mgxs Python module.

Various improvements to tallies have also been made. It is now possible to explicitly specify that a collision estimator be used in a tally. A new delayedgroup filter and delayed-nu-fission score allow a user to obtain delayed fission neutron production rates filtered by delayed group. Finally, the new inverse-velocity score may be useful for calculating kinetics parameters.

Caution

In previous versions, depending on how OpenMC was compiled binary output was either given in HDF5 or a flat binary format. With this version, all binary output is now HDF5 which means you must have HDF5 in order to install OpenMC. Please consult the user’s guide for instructions on how to compile with HDF5.

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Support for complex cell regions (union and complement operators)
  • Generic quadric surface type
  • Improved handling of secondary particles
  • Binary output is now solely HDF5
  • openmc.mgxs Python module enabling multi-group cross section generation
  • Collision estimator for tallies
  • Delayed fission neutron production tallies with ability to filter by delayed group
  • Inverse velocity tally score
  • Performance improvements for binary search
  • Performance improvements for reaction rate tallies

Bug Fixes

  • 299322: Bug with material filter when void material present
  • d74840: Fix triggers on tallies with multiple filters
  • c29a81: Correctly handle maximum transport energy
  • 3edc23: Fixes in the nu-scatter score
  • 629e3b: Assume unspecified surface coefficients are zero in Python API
  • 5dbe8b: Fix energy filters for openmc-plot-mesh-tally
  • ff66f4: Fixes in the openmc-plot-mesh-tally script
  • 441fd4: Fix bug in kappa-fission score
  • 7e5974: Allow fixed source simulations from Python API

Contributors

This release contains new contributions from the following people:

What’s New in 0.7.0

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Complete Python API
  • Python 3 compatability for all scripts
  • All scripts consistently named openmc-* and installed together
  • New ‘distribcell’ tally filter for repeated cells
  • Ability to specify outer lattice universe
  • XML input validation utility (openmc-validate-xml)
  • Support for hexagonal lattices
  • Material union energy grid method
  • Tally triggers
  • Remove dependence on PETSc
  • Significant OpenMP performance improvements
  • Support for Fortran 2008 MPI interface
  • Use of Travis CI for continuous integration
  • Simplifications and improvements to test suite

Bug Fixes

  • b5f712: Fix bug in spherical harmonics tallies
  • e6675b: Ensure all constants are double precision
  • 04e2c1: Fix potential bug in sample_nuclide routine
  • 6121d9: Fix bugs related to particle track files
  • 2f0e89: Fixes for nuclide specification in tallies

Contributors

This release contains new contributions from the following people:

What’s New in 0.6.2

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Meshline plotting capability
  • Support for plotting cells/materials on middle universe levels
  • Ability to model cells with no surfaces
  • Compatibility with PETSc 3.5
  • Compatability with OpenMPI 1.7/1.8
  • Improved overall performance via logarithmic-mapped energy grid search
  • Improved multi-threaded performance with atomic operations
  • Support for fixed source problems with fissionable materials

Bug Fixes

  • 26fb93: Fix problem with -t, –track command-line flag
  • 2f07c0: Improved evaporation spectrum algorithm
  • e6abb9: Fix segfault when tallying in a void material
  • 291b45: Handle metastable nuclides in NNDC data and multiplicities in MT=5 data

Contributors

This release contains new contributions from the following people:

What’s New in 0.6.1

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Coarse mesh finite difference (CMFD) acceleration no longer requires PETSc
  • Statepoint file numbering is now zero-padded
  • Python scripts now compatible with Python 2 or 3
  • Ability to run particle restarts in fixed source calculations
  • Capability to filter box source by fissionable materials
  • Nuclide/element names are now case insensitive in input files
  • Improved treatment of resonance scattering for heavy nuclides

Bug Fixes

  • 03e890: Check for energy-dependent multiplicities in ACE files
  • 4439de: Fix distance-to-surface calculation for general plane surface
  • 5808ed: Account for differences in URR band probabilities at different energies
  • 2e60c0: Allow zero atom/weight percents in materials
  • 3e0870: Don’t use PWD environment variable when setting path to input files
  • dc4776: Handle probability table resampling correctly
  • 01178b: Fix metastables nuclides in NNDC cross_sections.xml file
  • 62ec43: Don’t read tallies.xml when OpenMC is run in plotting mode
  • 2a95ef: Prevent segmentation fault on “current” score without mesh filter
  • 93e482: Check for negative values in probability tables

Contributors

This release contains new contributions from the following people:

What’s New in 0.6.0

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Legendre and spherical harmonic expansion tally scores
  • CMake is now default build system
  • Regression test suite based on CTests and NNDC cross sections
  • FoX is now a git submodule
  • Support for older cross sections (e.g. MCNP 66c)
  • Progress bar for plots
  • Expanded support for natural elements via <natural_elements> in settings.xml

Bug Fixes

  • 41f7ca: Fixed erroneous results from survival biasing
  • 038736: Fix tallies over void materials
  • 46f9e8: Check for negative values in probability tables
  • d1ca35: Fixed sampling of angular distribution
  • 0291c0: Fixed indexing error in plotting
  • d7a7d0: Fix bug with <element> specifying xs attribute
  • 85b3cb: Fix out-of-bounds error with OpenMP threading

Contributors

This release contains new contributions from the following people:

What’s New in 0.5.4

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Source sites outside geometry are resampled
  • XML-Fortran backend replaced by FoX XML
  • Ability to write particle track files
  • Handle lost particles more gracefully (via particle track files)
  • Multiple random number generator streams
  • Mesh tally plotting utility converted to use Tkinter rather than PyQt
  • Script added to download ACE data from NNDC
  • Mixed ASCII/binary cross_sections.xml now allowed
  • Expanded options for writing source bank
  • Re-enabled ability to use source file as starting source
  • S(a,b) recalculation avoided when same nuclide and S(a,b) table are accessed

Bug Fixes

  • 32c03c: Check for valid data in cross_sections.xml
  • c71ef5: Fix bug in statepoint.py
  • 8884fb: Check for all ZAIDs for S(a,b) tables
  • b38af0: Fix XML reading on multiple levels of input
  • d28750: Fix bug in convert_xsdir.py
  • cf567c: ENDF/B-VI data checked for compatibility
  • 6b9461: Fix p_valid sampling inside of sample_energy

Contributors

This release contains new contributions from the following people:

What’s New in 0.5.3

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Output interface enhanced to allow multiple files handles to be opened
  • Particle restart file linked to output interface
  • Particle restarts and state point restarts are both identified with the -r command line flag.
  • Particle instance no longer global, passed to all physics routines
  • Physics routines refactored to rely less on global memory, more arguments passed in
  • CMFD routines refactored and now can compute dominance ratio on the fly
  • PETSc 3.4.2 or higher must be used and compiled with fortran datatype support
  • Memory leaks fixed except for ones from xml-fortran package
  • Test suite enhanced to test output with different compiler options
  • Description of OpenMC development workflow added
  • OpenMP shared-memory parallelism added
  • Special run mode –tallies removed.

Bug Fixes

  • 2b1e8a: Normalize direction vector after reflecting particle.
  • 5853d2: Set blank default for cross section listing alias.
  • e178c7: Fix infinite loop with words greater than 80 characters in write_message.
  • c18a6e: Check for valid secondary mode on S(a,b) tables.
  • 82c456: Fix bug where last process could have zero particles.

What’s New in 0.5.2

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Python script for mesh tally plotting
  • Isotopic abundances based on IUPAC 2009 when using <element>
  • Particle restart files for debugging
  • Code will abort after certain number of lost particles (defaults to 10)
  • Region outside lattice can be filled with material (void by default)
  • 3D voxel plots
  • Full HDF5/PHDF5 support (including support in statepoint.py)
  • Cell overlap checking with -g command line flag (or when plotting)

Bug Fixes

  • 7632f3: Fixed bug in statepoint.py for multiple generations per batch.
  • f85ac4: Fix infinite loop bug in error module.
  • 49c36b: Don’t convert surface ids if surface filter is for current tallies.
  • 5ccc78: Fix bug in reassignment of bins for mesh filter.
  • b1f52f: Fixed bug in plot color specification.
  • eae7e5: Fixed many memory leaks.
  • 10c1cc: Minor CMFD fixes.
  • afdb50: Add compatibility for XML comments without whitespace.
  • a3c593: Fixed bug in use of free gas scattering for H-1.
  • 3a66e3: Fixed bug in 2D mesh tally implementation.
  • ab0793: Corrected PETSC_NULL references to their correct types.
  • 182ebd: Use analog estimator with energyout filter.

What’s New in 0.5.1

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Absorption and combined estimators for k-effective.
  • Natural elements can now be specified in materials using <element> rather than <nuclide>.
  • Support for multiple S(a,b) tables in a single material (e.g. BeO).
  • Test suite using Python nosetests.
  • Proper install capability with ‘make install’.
  • Lattices can now be 2 or 3 dimensions.
  • New scatter-PN score type.
  • New kappa-fission score type.
  • Ability to tally any reaction by specifying MT.

Bug Fixes

  • 94103e: Two checks for outgoing energy filters.
  • e77059: Fix reaction name for MT=849.
  • b0fe88: Fix distance to surface for cones.
  • 63bfd2: Fix tracklength tallies with cell filter and universes.
  • 88daf7: Fix analog tallies with survival biasing.

What’s New in 0.5.0

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • All user input options that formerly accepted “off” or “on” should now be “false” or “true” (the proper XML schema datatype).
  • The <criticality> element is deprecated and was replaced with <eigenvalue>.
  • Added ‘events’ score that returns number of events that scored to a tally.
  • Restructured tally filter implementation and user input.
  • Source convergence acceleration via CMFD (implemented with PETSc).
  • Ability to read source files in parallel when number of particles is greater than that number of source sites.
  • Cone surface types.

Bug Fixes

  • 737b90: Coincident surfaces from separate universes / particle traveling tangent to a surface.
  • a819b4: Output of surface neighbors in summary.out file.
  • b11696: Reading long attribute lists in XML input.
  • 2bd46a: Search for tallying nuclides when no default_xs specified.
  • 7a1f08: Fix word wrapping when writing messages.
  • c0e3ec: Prevent underflow when compiling with MPI=yes and DEBUG=yes.
  • 6f8d9d: Set default tally labels.
  • 6a3a5e: Fix problem with corner-crossing in lattices.

What’s New in 0.4.4

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Ability to write state points when using <no_reduce>.
  • Real-time XML validation in GNU Emacs with RELAX NG schemata.
  • Writing state points every n batches with <state_point interval=”…” />
  • Suppress creation of summary.out and cross_sections.out by default with option to turn them on with <output> tag in settings.xml file.
  • Ability to create HDF5 state points.
  • Binary source file is now part of state point file by default.
  • Enhanced state point usage and added state point Python scripts.
  • Turning confidence intervals on affects k-effective.
  • Option to specify <upper_right> for tally meshes.

Bug Fixes

  • 4654ee: Fixed plotting with void cells.
  • 7ee461: Fixed bug with multi-line input using type=’word’.
  • 792eb3: Fixed degrees of freedom for confidence intervals.
  • 7fd617: Fixed bug with restart runs in parallel.
  • dc4a8f: Fixed bug with fixed source restart runs.

What’s New in 0.4.3

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Option to report confidence intervals for tally results.
  • Rotation and translation for filled cells.
  • Ability to explicitly specify <estimator> for tallies.
  • Ability to store state points and use them to restart runs.
  • Fixed source calculations (no subcritical multiplication however).
  • Expanded options for external source distribution.
  • Ability to tally reaction rates for individual nuclides within a material.
  • Reduced memory usage by removing redundant storage or some cross-sections.
  • 3bd35b: Log-log interpolation for URR probability tables.
  • Support to specify labels on tallies (nelsonag).

Bug Fixes

  • 33f29a: Handle negative values in probability table.
  • 1c472d: Fixed survival biasing with probability tables.
  • 3c6e80: Fixed writing tallies with no filters.
  • 460ef1: Invalid results for duplicate tallies.
  • 0069d5: Fixed bug with 0 inactive batches.
  • 7af2cf: Fixed bug in score_analog_tallies.
  • 85a60e: Pick closest angular distribution for law 61.
  • 3212f5: Fixed issue with blank line at beginning of XML files.

What’s New in 0.4.2

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions, Mac OS X, and Microsoft Windows 7. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • Ability to specify void materials.
  • Option to not reduce tallies across processors at end of each batch.
  • Uniform fission site method for reducing variance on local tallies.
  • Reading/writing binary source files.
  • Added more messages for <trace> or high verbosity.
  • Estimator for diffusion coefficient.
  • Ability to specify ‘point’ source type.
  • Ability to change random number seed.
  • Users can now specify units=’sum’ on a <density> tag. This tells the code that the total material density is the sum of the atom fractions listed for each nuclide on the material.

Bug Fixes

  • a27f8f: Fixed runtime error bug when using Intel compiler with DEBUG on.
  • afe121: Fixed minor bug in fission bank algorithms.
  • e0968e: Force re-evaluation of cross-sections when each particle is born.
  • 298db8: Fixed bug in surface currents when using energy-in filter.
  • 2f3bbe: Fixed subtle bug in S(a,b) cross section calculation.
  • 671f30: Fixed surface currents on mesh not encompassing geometry.
  • b2c40e: Fixed bug in incoming energy filter for track-length tallies.
  • 5524fd: Mesh filter now works with track-length tallies.
  • d050c7: Added Bessel’s correction to make estimate of variance unbiased.
  • 2a5b9c: Fixed regression in plotting.

What’s New in 0.4.1

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions as well as Mac OS X. However, it has not been tested yet on any releases of Microsoft Windows. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • A batching method has been implemented so that statistics can be calculated based on multiple generations instead of a single generation. This can help to overcome problems with underpredicted variance in problems where there is correlation between successive fission source iterations.
  • Users now have the option to select a non-unionized energy grid for problems with many nuclides where the use of a unionized grid is not feasible.
  • Improved plotting capability (Nick Horelik). The plotting input is now in plots.xml instead of plot.xml.
  • Added multiple estimators for k-effective and added a global tally for leakage.
  • Moved cross section-related output into cross_sections.out.
  • Improved timing capabilities.
  • Can now use more than 2**31 - 1 particles per generation.
  • Improved fission bank synchronization method. This also necessitated changing the source bank to be of type Bank rather than of type Particle.
  • Added HDF5 output (not complete yet).
  • Major changes to tally implementation.

Bug Fixes

  • b206a8: Fixed subtle error in the sampling of energy distributions.
  • 800742: Fixed error in sampling of angle and rotating angles.
  • a07c08: Fixed bug in linear-linear interpolation during sampling energy.
  • a75283: Fixed energy and energyout tally filters to support many bins.
  • 95cfac: Fixed error in cell neighbor searches.
  • 83a803: Fixed bug related to probability tables.

What’s New in 0.4.0

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions as well as Mac OS X. However, it has not been tested yet on any releases of Microsoft Windows. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides in the problem).

New Features

  • The probability table method for treatment of energy self-shielding in the unresolved resonance range has been implemented and is now turned on by default.
  • Calculation of Shannon entropy for assessing convergence of the fission source distribution.
  • Ability to compile with the PGI Fortran compiler.
  • Ability to run on IBM BlueGene/P machines.
  • Completely rewrote how nested universes are handled. Geometry is now much more robust.

Bug Fixes

  • Many geometry errors have been fixed. The Monte Carlo performance benchmark can now be successfully run in OpenMC.

Theory and Methodology

Introduction

The physical process by which a population of particles evolves over time is governed by a number of probability distributions. For instance, given a particle traveling through some material, there is a probability distribution for the distance it will travel until its next collision (an exponential distribution). Then, when it collides with a nucleus, there is an associated probability of undergoing each possible reaction with that nucleus. While the behavior of any single particle is unpredictable, the average behavior of a large population of particles originating from the same source is well defined.

If the probability distributions that govern the transport of a particle are known, the process of single particles randomly streaming and colliding with nuclei can be simulated directly with computers using a technique known as Monte Carlo simulation. If enough particles are simulated this way, the average behavior can be determined to within arbitrarily small statistical error, a fact guaranteed by the central limit theorem. To be more precise, the central limit theorem tells us that the variance of the sample mean of some physical parameter being estimated with Monte Carlo will be inversely proportional to the number of realizations, i.e. the number of particles we simulate:

\[\sigma^2 \propto \frac{1}{N}.\]

where \(\sigma^2\) is the variance of the sample mean and \(N\) is the number of realizations.

Overview of Program Flow

OpenMC performs a Monte Carlo simulation one particle at a time – at no point is more than one particle being tracked on a single program instance. Before any particles are tracked, the problem must be initialized. This involves the following steps:

  • Read input files and building data structures for the geometry, materials, tallies, and other associated variables.
  • Initialize the pseudorandom number generator.
  • Read the contiuous-energy or multi-group cross section data specified in the problem.
  • If using a special energy grid treatment such as a union energy grid or lethargy bins, that must be initialized as well in a continuous-energy problem.
  • In a multi-group problem, individual nuclide cross section information is combined to produce material-specific cross section data.
  • In a fixed source problem, source sites are sampled from the specified source. In an eigenvalue problem, source sites are sampled from some initial source distribution or from a source file. The source sites consist of coordinates, a direction, and an energy.

Once initialization is complete, the actual transport simulation can proceed. The life of a single particle will proceed as follows:

  1. The particle’s properties are initialized from a source site previously sampled.

  2. Based on the particle’s coordinates, the current cell in which the particle resides is determined.

  3. The energy-dependent cross sections for the material that the particle is currently in are determined. Note that this includes the total cross section, which is not pre-calculated.

  4. The distance to the nearest boundary of the particle’s cell is determined based on the bounding surfaces to the cell.

  5. The distance to the next collision is sampled. If the total material cross section is \(\Sigma_t\), this can be shown to be

    \[d = -\frac{\ln \xi}{\Sigma_t}\]

    where \(\xi\) is a pseudorandom number sampled from a uniform distribution on \([0,1)\).

  6. If the distance to the nearest boundary is less than the distance to the next collision, the particle is moved forward to this boundary. Then, the process is repeated from step 2. If the distance to collision is closer than the distance to the nearest boundary, then the particle will undergo a collision.

  7. The material at the collision site may consist of multiple nuclides. First, the nuclide with which the collision will happen is sampled based on the total cross sections. If the total cross section of material \(i\) is \(\Sigma_{t,i}\), then the probability that any nuclide is sampled is

    \[P(i) = \frac{\Sigma_{t,i}}{\Sigma_t}.\]

    Note that the above selection of collided nuclide only applies to continuous-energy simulations as multi-group simulations use nuclide data which has already been combined in to material-specific data.

  8. Once the specific nuclide is sampled, a reaction for that nuclide is randomly sampled based on the microscopic cross sections. If the microscopic cross section for some reaction \(x\) is \(\sigma_x\) and the total microscopic cross section for the nuclide is \(\sigma_t\), then the probability that reaction \(x\) will occur is

    \[P(x) = \frac{\sigma_x}{\sigma_t}.\]

    Since multi-group simulations use material-specific data, the above is performed with those material multi-group cross sections (i.e., macroscopic cross sections for the material) instead of microscopic cross sections for the nuclide).

  9. If the sampled reaction is elastic or inelastic scattering, the outgoing energy and angle is sampled from the appropriate distribution. In continuous-energy simulation, reactions of type \((n,xn)\) are treated as scattering and any additional particles which may be created are added to a secondary particle bank to be tracked later. In a multi-group simulation, this secondary bank is not used but the particle weight is increased accordingly. The original particle then continues from step 3. If the reaction is absorption or fission, the particle dies and if necessary, fission sites are created and stored in the fission bank.

After all particles have been simulated, there are a few final tasks that must be performed before the run is finished. This include the following:

  • With the accumulated sum and sum of squares for each tally, the sample mean and its variance is calculated.
  • All tallies and other results are written to disk.
  • If requested, a source file is written to disk.
  • Dynamically-allocated memory should be freed.

Geometry

Constructive Solid Geometry

OpenMC uses a technique known as constructive solid geometry (CSG) to build arbitrarily complex three-dimensional models in Euclidean space. In a CSG model, every unique object is described as the union and/or intersection of half-spaces created by bounding surfaces. Every surface divides all of space into exactly two half-spaces. We can mathematically define a surface as a collection of points that satisfy an equation of the form \(f(x,y,z) = 0\) where \(f(x,y,z)\) is a given function. All coordinates for which \(f(x,y,z) < 0\) are referred to as the negative half-space (or simply the negative side) and coordinates for which \(f(x,y,z) > 0\) are referred to as the positive half-space.

Let us take the example of a sphere centered at the point \((x_0,y_0,z_0)\) with radius \(R\). One would normally write the equation of the sphere as

(1)\[(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = R^2\]

By subtracting the right-hand term from both sides of equation (1), we can then write the surface equation for the sphere:

(2)\[f(x,y,z) = (x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\]

One can confirm that any point inside this sphere will correspond to \(f(x,y,z) < 0\) and any point outside the sphere will correspond to \(f(x,y,z) > 0\).

In OpenMC, every surface defined by the user is assigned an integer to uniquely identify it. We can then refer to either of the two half-spaces created by a surface by a combination of the unique ID of the surface and a positive/negative sign. Figure 5 shows an example of an ellipse with unique ID 1 dividing space into two half-spaces.

_images/halfspace.svg

Figure 5: Example of an ellipse and its associated half-spaces.

References to half-spaces created by surfaces are used to define regions of space of uniform composition, which are then assigned to cells. OpenMC allows regions to be defined using union, intersection, and complement operators. As in MCNP, the intersection operator is implicit as doesn’t need to be written in a region specification. A defined region is then associated with a material composition in a cell. Figure 6 shows an example of a cell region defined as the intersection of an ellipse and two planes.

_images/union.svg

Figure 6: The shaded region represents a cell bounded by three surfaces.

The ability to form regions based on bounding quadratic surfaces enables OpenMC to model arbitrarily complex three-dimensional objects. In practice, one is limited only by the different surface types available in OpenMC. The following table lists the available surface types, the identifier used to specify them in input files, the corresponding surface equation, and the input parameters needed to fully define the surface.

Surface types available in OpenMC.
Surface Identifier Equation Parameters
Plane perpendicular to \(x\)-axis x-plane \(x - x_0 = 0\) \(x_0\)
Plane perpendicular to \(y\)-axis y-plane \(y - y_0 = 0\) \(y_0\)
Plane perpendicular to \(z\)-axis z-plane \(z - z_0 = 0\) \(z_0\)
Arbitrary plane plane \(Ax + By + Cz = D\) \(A\;B\;C\;D\)
Infinite cylinder parallel to \(x\)-axis x-cylinder \((y-y_0)^2 + (z-z_0)^2 = R^2\) \(y_0\;z_0\;R\)
Infinite cylinder parallel to \(y\)-axis y-cylinder \((x-x_0)^2 + (z-z_0)^2 = R^2\) \(x_0\;z_0\;R\)
Infinite cylinder parallel to \(z\)-axis z-cylinder \((x-x_0)^2 + (y-y_0)^2 = R^2\) \(x_0\;y_0\;R\)
Sphere sphere \((x-x_0)^2 + (y-y_0)^2 + (z-z_0)^2 = R^2\) \(x_0 \; y_0 \; z_0 \; R\)
Cone parallel to the \(x\)-axis x-cone \((y-y_0)^2 + (z-z_0)^2 = R^2(x-x_0)^2\) \(x_0 \; y_0 \; z_0 \; R^2\)
Cone parallel to the \(y\)-axis y-cone \((x-x_0)^2 + (z-z_0)^2 = R^2(y-y_0)^2\) \(x_0 \; y_0 \; z_0 \; R^2\)
Cone parallel to the \(z\)-axis z-cone \((x-x_0)^2 + (y-y_0)^2 = R^2(z-z_0)^2\) \(x_0 \; y_0 \; z_0 \; R^2\)
General quadric surface quadric \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K\) \(A \; B \; C \; D \; E \; F \; G \; H \; J \; K\)
Universes

OpenMC supports universe-based geometry similar to the likes of MCNP and Serpent. This capability enables user to model any identical repeated structures once and then fill them in various spots in the geometry. A prototypical example of a repeated structure would be a fuel pin within a fuel assembly or a fuel assembly within a core.

Each cell in OpenMC can either be filled with a normal material or with a universe. If the cell is filled with a universe, only the region of the universe that is within the defined boundaries of the parent cell will be present in the geometry. That is to say, even though a collection of cells in a universe may extend to infinity, not all of the universe will be “visible” in the geometry since it will be truncated by the boundaries of the cell that contains it.

When a cell is filled with a universe, it is possible to specify that the universe filling the cell should be rotated and translated. This is done through the rotation and translation attributes on a cell (note though that these can only be specified on a cell that is filled with another universe, not a material).

It is not necessary to use or assign universes in a geometry if there are no repeated structures. Any cell in the geometry that is not assigned to a specified universe is automatically part of the base universe whose coordinates are just the normal coordinates in Euclidean space.

Lattices

Often times, repeated structures in a geometry occur in a regular pattern such as a rectangular or hexagonal lattice. In such a case, it would be cumbersome for a user to have to define the boundaries of each of the cells to be filled with a universe. Thus, OpenMC provides a lattice capability similar to that used in MCNP and Serpent.

The implementation of lattices is similar in principle to universes — instead of a cell being filled with a universe, the user can specify that it is filled with a finite lattice. The lattice is then defined by a two-dimensional array of universes that are to fill each position in the lattice. A good example of the use of lattices and universes can be seen in the OpenMC model for the Monte Carlo Performance benchmark.

Computing the Distance to Nearest Boundary

One of the most basic algorithms in any Monte Carlo code is determining the distance to the nearest surface within a cell. Since each cell is defined by the surfaces that bound it, if we compute the distance to all surfaces bounding a cell, we can determine the nearest one.

With the possibility of a particle having coordinates on multiple levels (universes) in a geometry, we must exercise care when calculating the distance to the nearest surface. Each different level of geometry has a set of boundaries with which the particle’s direction of travel may intersect. Thus, it is necessary to check the distance to the surfaces bounding the cell in each level. This should be done starting the highest (most global) level going down to the lowest (most local) level. That ensures that if two surfaces on different levels are coincident, by default the one on the higher level will be selected as the nearest surface. Although they are not explicitly defined, it is also necessary to check the distance to surfaces representing lattice boundaries if a lattice exists on a given level.

The following procedure is used to calculate the distance to each bounding surface. Suppose we have a particle at \((x_0,y_0,z_0)\) traveling in the direction \(u_0,v_0,w_0\). To find the distance \(d\) to a surface \(f(x,y,z) = 0\), we need to solve the equation:

(3)\[f(x_0 + du_0, y_0 + dv_0, z_0 + dw_0) = 0\]

If no solutions to equation (3) exist or the only solutions are complex, then the particle’s direction of travel will not intersect the surface. If the solution to equation (3) is negative, this means that the surface is “behind” the particle, i.e. if the particle continues traveling in its current direction, it will not hit the surface. The complete derivation for different types of surfaces used in OpenMC will be presented in the following sections.

Since \(f(x,y,z)\) in general is quadratic in \(x\), \(y\), and \(z\), this implies that \(f(x_0 + du_0, y + dv_0, z + dw_0)\) is quadratic in \(d\). Thus we expect at most two real solutions to (3). If no solutions to (3) exist or the only solutions are complex, then the particle’s direction of travel will not intersect the surface. If the solution to (3) is negative, this means that the surface is “behind” the particle, i.e. if the particle continues traveling in its current direction, it will not hit the surface.

Once a distance has been computed to a surface, we need to check if it is closer than previously-computed distances to surfaces. Unfortunately, we cannot just use the minimum function because some of the calculated distances, which should be the same in theory (e.g. coincident surfaces), may be slightly different due to the use of floating-point arithmetic. Consequently, we should first check for floating-point equality of the current distance calculated and the minimum found thus far. This is done by checking if

(4)\[\frac{| d - d_{min} |}{d_{min}} < \epsilon\]

where \(d\) is the distance to a surface just calculated, \(d_{min}\) is the minimum distance found thus far, and \(\epsilon\) is a small number. In OpenMC, this parameter is set to \(\epsilon = 10^{-14}\) since all floating calculations are done on 8-byte floating point numbers.

Plane Perpendicular to an Axis

The equation for a plane perpendicular to, for example, the x-axis is simply \(x - x_0 = 0\). As such, we need to solve \(x + du - x_0 = 0\). The solution for the distance is

(5)\[d = \frac{x_0 - x}{u}\]

Note that if the particle’s direction of flight is parallel to the x-axis, i.e. \(u = 0\), the distance to the surface will be infinity. While the example here was for a plane perpendicular to the x-axis, the same formula can be applied for the surfaces \(y = y_0\) and \(z = z_0\).

Generic Plane

The equation for a generic plane is \(Ax + By + Cz = D\). Thus, we need to solve the equation \(A(x + du) + B(y + dv) + C(z + dw) = D\). The solution to this equation for the distance is

(6)\[d = \frac{D - Ax - By - Cz}{Au + Bv + Cw}\]

Again, we need to check whether the denominator is zero. If so, this means that the particle’s direction of flight is parallel to the plane and it will therefore never hit the plane.

Cylinder Parallel to an Axis

The equation for a cylinder parallel to, for example, the x-axis is \((y - y_0)^2 + (z - z_0)^2 = R^2\). Thus, we need to solve \((y + dv - y_0)^2 + (z + dw - z_0)^2 = R^2\). Let us define \(\bar{y} = y - y_0\) and \(\bar{z} = z - z_0\). We then have

(7)\[(\bar{y} + dv)^2 + (\bar{z} + dw)^2 = R^2\]

Expanding equation (7) and rearranging terms, we obtain

(8)\[(v^2 + w^2) d^2 + 2 (\bar{y}v + \bar{z}w) d + (\bar{y}^2 + \bar{z}^2 - R^2) = 0\]

This is a quadratic equation for \(d\). To simplify notation, let us define \(a = v^2 + w^2\), \(k = \bar{y}v + \bar{z}w\), and \(c = \bar{y}^2 + \bar{z}^2 - R^2\). Thus, the distance is just the solution to \(ad^2 + 2kd + c = 0\):

(9)\[d = \frac{-k \pm \sqrt{k^2 - ac}}{a}\]

A few conditions must be checked for. If \(a = 0\), this means the particle is parallel to the cylinder and will thus never intersect it. Also, if \(k^2 - ac < 0\), this means that both solutions to the quadratic are complex. In physical terms, this means that the ray along which the particle is traveling does not make any intersections with the cylinder.

If we do have intersections and \(c < 0\), this means that the particle is inside the cylinder. Thus, one solution should be positive and one should be negative. Clearly, the positive distance will occur when the sign on the square root of the discriminant is positive since \(a > 0\).

If we have intersections and \(c > 0\) this means that the particle is outside the cylinder. Thus, the solutions to the quadratic are either both positive or both negative. If they are both positive, the smaller (closer) one will be the solution with a negative sign on the square root of the discriminant.

The same equations and logic here can be used for cylinders that are parallel to the y- or z-axis with appropriate substitution of constants.

Sphere

The equation for a sphere is \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = R^2\). Thus, we need to solve the equation

(10)\[(x + du - x_0)^2 + (y + dv - y_0)^2 + (z + dw - z_0)^2 = R^2\]

Let us define \(\bar{x} = x - x_0\), \(\bar{y} = y - y_0\), and \(\bar{z} = z - z_0\). We then have

(11)\[(\bar{x} + du)^2 + (\bar{y} + dv)^2 + (\bar{z} - dw)^2 = R^2\]

Expanding equation (11) and rearranging terms, we obtain

(12)\[d^2 + 2 (\bar{x}u + \bar{y}v + \bar{z}w) d + (\bar{x}^2 + \bar{y}^2 + \bar{z}^2 - R^2) = 0\]

This is a quadratic equation for \(d\). To simplify notation, let us define \(k = \bar{x}u + \bar{y}v + \bar{z}w\) and \(c = \bar{x}^2 + \bar{y}^2 + \bar{z}^2 - R^2\). Thus, the distance is just the solution to \(d^2 + 2kd + c = 0\):

(13)\[d = -k \pm \sqrt{k^2 - c}\]

If the discriminant \(k^2 - c < 0\), this means that both solutions to the quadratic are complex. In physical terms, this means that the ray along which the particle is traveling does not make any intersections with the sphere.

If we do have intersections and \(c < 0\), this means that the particle is inside the sphere. Thus, one solution should be positive and one should be negative. The positive distance will occur when the sign on the square root of the discriminant is positive. If we have intersections but \(c > 0\) this means that the particle is outside the sphere. The solutions to the quadratic will then be either both positive or both negative. If they are both positive, the smaller (closer) one will be the solution with a negative sign on the square root of the discriminant.

Cone Parallel to an Axis

The equation for a cone parallel to, for example, the x-axis is \((y - y_0)^2 + (z - z_0)^2 = R^2(x - x_0)^2\). Thus, we need to solve \((y + dv - y_0)^2 + (z + dw - z_0)^2 = R^2(x + du - x_0)^2\). Let us define \(\bar{x} = x - x_0\), \(\bar{y} = y - y_0\), and \(\bar{z} = z - z_0\). We then have

(14)\[(\bar{y} + dv)^2 + (\bar{z} + dw)^2 = R^2(\bar{x} + du)^2\]

Expanding equation (14) and rearranging terms, we obtain

(15)\[(v^2 + w^2 - R^2u^2) d^2 + 2 (\bar{y}v + \bar{z}w - R^2\bar{x}u) d + (\bar{y}^2 + \bar{z}^2 - R^2\bar{x}^2) = 0\]

Defining the terms

(16)\[a = v^2 + w^2 - R^2u^2 k = \bar{y}v + \bar{z}w - R^2\bar{x}u c = \bar{y}^2 + \bar{z}^2 - R^2\bar{x}^2\]

we then have the simple quadratic equation \(ad^2 + 2kd + c = 0\) which can be solved as described in Cylinder Parallel to an Axis.

General Quadric

The equation for a general quadric surface is \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\). Thus, we need to solve the equation

(17)\[A(x+du)^2 + B(y+dv)^2 + C(z+dw)^2 + D(x+du)(y+dv) + E(y+dv)(z+dw) + \\ F(x+du)(z+dw) + G(x+du) + H(y+dv) + J(z+dw) + K = 0\]

Expanding equation (17) and rearranging terms, we obtain

(18)\[d^2(uv + vw + uw) + 2d(Aux + Bvy + Cwx + (D(uv + vx) + E(vz + wy) + \\ F(wx + uz))/2) + (x(Ax + Dy) + y(By + Ez) + z(Cz + Fx)) = 0\]

Defining the terms

(19)\[a = uv + vw + uw k = Aux + Bvy + Cwx + (D(uv + vx) + E(vz + wy) + F(wx + uz))/2 c = x(Ax + Dy) + y(By + Ez) + z(Cz + Fx)\]

we then have the simple quadratic equation \(ad^2 + 2kd + c = 0\) which can be solved as described in Cylinder Parallel to an Axis.

Finding a Cell Given a Point

Another basic algorithm is to determine which cell contains a given point in the global coordinate system, i.e. if the particle’s position is \((x,y,z)\), what cell is it currently in. This is done in the following manner in OpenMC. With the possibility of multiple levels of coordinates, we must perform a recursive search for the cell. First, we start in the highest (most global) universe, which we call the base universe, and loop over each cell within that universe. For each cell, we check whether the specified point is inside the cell using the algorithm described in Finding a Lattice Tile. If the cell is filled with a normal material, the search is done and we have identified the cell containing the point. If the cell is filled with another universe, we then search all cells within that universe to see if any of them contain the specified point. If the cell is filled with a lattice, the position within the lattice is determined, and then whatever universe fills that lattice position is recursively searched. The search ends once a cell containing a normal material is found that contains the specified point.

Finding a Lattice Tile

If a particle is inside a lattice, its position inside the lattice must be determined before assigning it to a cell. Throughout this section, the volumetric units of the lattice will be referred to as “tiles”. Tiles are identified by thier indices, and the process of discovering which tile contains the particle is referred to as “indexing”.

Rectilinear Lattice Indexing

Indices are assigned to tiles in a rectilinear lattice based on the tile’s position along the \(x\), \(y\), and \(z\) axes. Figure 7 maps the indices for a 2D lattice. The indices, (1, 1), map to the lower-left tile. (5, 1) and (5, 5) map to the lower-right and upper-right tiles, respectively.

_images/rect_lat.svg

Figure 7: Rectilinear lattice tile indices.

In general, a lattice tile is specified by the three indices, \((i_x, i_y, i_z)\). If a particle’s current coordinates are \((x, y, z)\) then the indices can be determined from these formulas:

(20)\[i_x = \left \lceil \frac{x - x_0}{p_0} \right \rceil i_y = \left \lceil \frac{y - y_0}{p_1} \right \rceil i_z = \left \lceil \frac{z - z_0}{p_2} \right \rceil\]

where \((x_0, y_0, z_0)\) are the coordinates to the lower-left-bottom corner of the lattice, and \(p_0, p_1, p_2\) are the pitches along the \(x\), \(y\), and \(z\) axes, respectively.

Hexagonal Lattice Indexing

A skewed coordinate system is used for indexing hexagonal lattice tiles. Rather than a \(y\)-axis, another axis is used that is rotated 30 degrees counter-clockwise from the \(y\)-axis. This axis is referred to as the \(\alpha\)-axis. Figure 8 shows how 2D hexagonal tiles are mapped with the \((x, \alpha)\) basis. In this system, (0, 0) maps to the center tile, (0, 2) to the top tile, and (2, -1) to the middle tile on the right side.

_images/hex_lat.svg

Figure 8: Hexagonal lattice tile indices.

Unfortunately, the indices cannot be determined with one simple formula as before. Indexing requires a two-step process, a coarse step which determines a set of four tiles that contains the particle and a fine step that determines which of those four tiles actually contains the particle.

In the first step, indices are found using these formulas:

(21)\[\alpha = -\frac{x}{\sqrt{3}} + y i_x^* = \left \lfloor \frac{x}{p_0 \sqrt{3} / 2} \right \rfloor i_\alpha^* = \left \lfloor \frac{\alpha}{p_0} \right \rfloor\]

where \(p_0\) is the lattice pitch (in the \(x\)-\(y\) plane). The true index of the particle could be \((i_x^*, i_\alpha^*)\), \((i_x^* + 1, i_\alpha^*)\), \((i_x^*, i_\alpha^* + 1)\), or \((i_x^* + 1, i_\alpha^* + 1)\).

The second step selects the correct tile from that neighborhood of 4. OpenMC does this by calculating the distance between the particle and the centers of each of the 4 tiles, and then picking the closest tile. This works because regular hexagonal tiles form a Voronoi tessellation which means that all of the points within a tile are closest to the center of that same tile.

Indexing along the \(z\)-axis uses the same method from rectilinear lattices, i.e.

(22)\[i_z = \left \lceil \frac{z - z_0}{p_2} \right \rceil\]

Determining if a Coordinate is in a Cell

To determine which cell a particle is in given its coordinates, we need to be able to check whether a given cell contains a point. The algorithm for determining if a cell contains a point is as follows. For each surface that bounds a cell, we determine the particle’s sense with respect to the surface. As explained earlier, if we have a point \((x_0,y_0,z_0)\) and a surface \(f(x,y,z) = 0\), the point is said to have negative sense if \(f(x_0,y_0,z_0) < 0\) and positive sense if \(f(x_0,y_0,z_0) > 0\). If for all surfaces, the sense of the particle with respect to the surface matches the specified sense that defines the half-space within the cell, then the point is inside the cell. Note that this algorithm works only for simple cells defined as intersections of half-spaces.

It may help to illustrate this algorithm using a simple example. Let’s say we have a cell defined as

<surface id="1" type="sphere"  coeffs="0 0 0 10" />
<surface id="2" type="x-plane" coeffs="-3" />
<surface id="3" type="y-plane" coeffs="2" />
<cell id="1" surfaces="-1 2 -3" />

This means that the cell is defined as the intersection of the negative half space of a sphere, the positive half-space of an x-plane, and the negative half-space of a y-plane. Said another way, any point inside this cell must satisfy the following equations

(23)\[x^2 + y^2 + z^2 - 10^2 < 0 \\ x - (-3) > 0 \\ y - 2 < 0\]

In order to determine if a point is inside the cell, we would substitute its coordinates into equation (23). If the inequalities are satisfied, than the point is indeed inside the cell.

Handling Surface Crossings

A particle will cross a surface if the distance to the nearest surface is closer than the distance sampled to the next collision. A number of things happen when a particle hits a surface. First, we need to check if a non-transmissive boundary condition has been applied to the surface. If a vacuum boundary condition has been applied, the particle is killed and any surface current tallies are scored to as needed. If a reflective boundary condition has been applied to the surface, surface current tallies are scored to and then the particle’s direction is changed according to the procedure in Reflective Boundary Conditions. Note that the white boundary condition can be considered as the special case of reflective boundary condition, where the same processing method will be applied to deal with the surface current tallies scoring, except for determining the changes of particle’s direction according to the procedures in White Boundary Conditions.

Next, we need to determine what cell is beyond the surface in the direction of travel of the particle so that we can evaluate cross sections based on its material properties. At initialization, a list of neighboring cells is created for each surface in the problem as described in Building Neighbor Lists. The algorithm outlined in Finding a Cell Given a Point is used to find a cell containing the particle with one minor modification; rather than searching all cells in the base universe, only the list of neighboring cells is searched. If this search is unsuccessful, then a search is done over every cell in the base universe.

Building Neighbor Lists

After the geometry has been loaded and stored in memory from an input file, OpenMC builds a list for each surface containing any cells that are bounded by that surface in order to speed up processing of surface crossings. The algorithm to build these lists is as follows. First, we loop over all cells in the geometry and count up how many times each surface appears in a specification as bounding a negative half-space and bounding a positive half-space. Two arrays are then allocated for each surface, one that lists each cell that contains the negative half-space of the surface and one that lists each cell that contains the positive half-space of the surface. Another loop is performed over all cells and the neighbor lists are populated for each surface.

Reflective Boundary Conditions

If the velocity of a particle is \(\mathbf{v}\) and it crosses a surface of the form \(f(x,y,z) = 0\) with a reflective boundary condition, it can be shown based on geometric arguments that the velocity vector will then become

(24)\[\mathbf{v'} = \mathbf{v} - 2 (\mathbf{v} \cdot \hat{\mathbf{n}}) \hat{\mathbf{n}}\]

where \(\hat{\mathbf{n}}\) is a unit vector normal to the surface at the point of the surface crossing. The rationale for this can be understood by noting that \((\mathbf{v} \cdot \hat{\mathbf{n}}) \hat{\mathbf{n}}\) is the projection of the velocity vector onto the normal vector. By subtracting two times this projection, the velocity is reflected with respect to the surface normal. Since the magnitude of the velocity of the particle will not change as it undergoes reflection, we can work with the direction of the particle instead, simplifying equation (24) to

(25)\[\mathbf{\Omega'} = \mathbf{\Omega} - 2 (\mathbf{\Omega} \cdot \hat{\mathbf{n}}) \hat{\mathbf{n}}\]

where \(\mathbf{v} = || \mathbf{v} || \mathbf{\Omega}\). The direction of the surface normal will be the gradient of the surface at the point of crossing, i.e. \(\mathbf{n} = \nabla f(x,y,z)\). Substituting this into equation (25), we get

(26)\[\mathbf{\Omega'} = \mathbf{\Omega} - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \nabla f\]

If we write the initial and final directions in terms of their vector components, \(\mathbf{\Omega} = (u,v,w)\) and \(\mathbf{\Omega'} = (u', v', w')\), this allows us to represent equation (25) as a series of equations:

(27)\[u' = u - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial x} \\ v' = v - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial y} \\ w' = w - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial z}\]

One can then use equation (27) to develop equations for transforming a particle’s direction given the equation of the surface.

Plane Perpendicular to an Axis

For a plane that is perpendicular to an axis, the rule for reflection is almost so simple that no derivation is needed at all. Nevertheless, we will proceed with the derivation to confirm that the rules of geometry agree with our intuition. The gradient of the surface \(f(x,y,z) = x - x_0 = 0\) is simply \(\nabla f = (1, 0, 0)\). Note that this vector is already normalized, i.e. \(|| \nabla f || = 1\). The second two equations in (27) tell us that \(v\) and \(w\) do not change and the first tell us that

(28)\[u' = u - 2u = -u\]

We see that reflection for a plane perpendicular to an axis only entails negating the directional cosine for that axis.

Generic Plane

A generic plane has the form \(f(x,y,z) = Ax + By + Cz - D = 0\). Thus, the gradient to the surface is simply \(\nabla f = (A,B,C)\) whose norm squared is \(A^2 + B^2 + C^2\). This implies that

(29)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{2(Au + Bv + Cw)}{A^2 + B^2 + C^2}\]

Substituting equation (29) into equation (27) gives us the form of the solution. For example, the x-component of the reflected direction will be

(30)\[u' = u - \frac{2A(Au + Bv + Cw)}{A^2 + B^2 + C^2}\]
Cylinder Parallel to an Axis

A cylinder parallel to, for example, the x-axis has the form \(f(x,y,z) = (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\). Thus, the gradient to the surface is

(31)\[\nabla f = 2 \left ( \begin{array}{c} 0 \\ y - y_0 \\ z - z_0 \end{array} \right ) = 2 \left ( \begin{array}{c} 0 \\ \bar{y} \\ \bar{z} \end{array} \right )\]

where we have introduced the constants \(\bar{y}\) and \(\bar{z}\). Taking the square of the norm of the gradient, we find that

(32)\[|| \nabla f ||^2 = 4 \bar{y}^2 + 4 \bar{z}^2 = 4 R^2\]

This implies that

(33)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{\bar{y}v + \bar{z}w}{R^2}\]

Substituting equations (33) and (31) into equation (27) gives us the form of the solution. In this case, the x-component will not change. The y- and z-components of the reflected direction will be

(34)\[v' = v - \frac{2 ( \bar{y}v + \bar{z}w ) \bar{y}}{R^2} \\ w' = w - \frac{2 ( \bar{y}v + \bar{z}w ) \bar{z}}{R^2}\]
Sphere

The surface equation for a sphere has the form \(f(x,y,z) = (x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\). Thus, the gradient to the surface is

(35)\[\nabla f = 2 \left ( \begin{array}{c} x - x_0 \\ y - y_0 \\ z - z_0 \end{array} \right ) = 2 \left ( \begin{array}{c} \bar{x} \\ \bar{y} \\ \bar{z} \end{array} \right )\]

where we have introduced the constants \(\bar{x}, \bar{y}, \bar{z}\). Taking the square of the norm of the gradient, we find that

(36)\[|| \nabla f ||^2 = 4 \bar{x}^2 + 4 \bar{y}^2 + 4 \bar{z}^2 = 4 R^2\]

This implies that

(37)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{\bar{x}u + \bar{y}v + \bar{z}w}{R^2}\]

Substituting equations (37) and (35) into equation (27) gives us the form of the solution:

(38)\[u' = u - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{x} }{R^2} \\ v' = v - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{y} }{R^2} \\ w' = w - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{z} }{R^2}\]
Cone Parallel to an Axis

A cone parallel to, for example, the z-axis has the form \(f(x,y,z) = (x - x_0)^2 + (y - y_0)^2 - R^2(z - z_0)^2 = 0\). Thus, the gradient to the surface is

(39)\[\nabla f = 2 \left ( \begin{array}{c} x - x_0 \\ y - y_0 \\ -R^2(z - z_0) \end{array} \right ) = 2 \left ( \begin{array}{c} \bar{x} \\ \bar{y} \\ -R^2\bar{z} \end{array} \right )\]

where we have introduced the constants \(\bar{x}\), \(\bar{y}\), and \(\bar{z}\). Taking the square of the norm of the gradient, we find that

(40)\[|| \nabla f ||^2 = 4 \bar{x}^2 + \bar{y}^2 + 4 R^4 \bar{z}^2 \\ = 4 R^2 \bar{z}^2 + 4 R^4 \bar{z}^2 \\ = 4 R^2 (1 + R^2) \bar{z}^2\]

This implies that

(41)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{\bar{x}u + \bar{y}v - R^2\bar{z}w}{R^2 (1 + R^2) \bar{z}^2}\]

Substituting equations (41) and (39) into equation (27) gives us the form of the solution:

(42)\[u' = u - \frac{2 (\bar{x}u + \bar{y}v - R^2\bar{z}w) \bar{x}}{R^2 (1 + R^2) \bar{z}^2} v' = v - \frac{2 (\bar{x}u + \bar{y}v - R^2\bar{z}w) \bar{y}}{R^2 (1 + R^2) \bar{z}^2} w' = w + \frac{2 (\bar{x}u + \bar{y}v - R^2\bar{z}w)}{R^2 (1 + R^2) \bar{z}}\]
General Quadric

A general quadric surface has the form \(f(x,y,z) = Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\). Thus, the gradient to the surface is

(43)\[\nabla f = \left ( \begin{array}{c} 2Ax + Dy + Fz + G \\ 2By + Dx + Ez + H \\ 2Cz + Ey + Fx + J \end{array} \right ).\]

White Boundary Conditions

The white boundary condition is usually applied in deterministic codes, where the particle will hit the surface and travel back with isotropic angular distribution. The change in particle’s direction is sampled from a cosine distribution instead of uniform. Figure 9 shows an example of cosine-distribution reflection on the arbitrary surface relative to the surface normal.

_images/cosine-dist.png

Figure 9: Cosine-distribution reflection on an arbitrary surface.

The probability density function (pdf) for the reflected direction can be expressed as follows,

(44)\[f(\mu, \phi) d\mu d\phi = \frac{\mu}{\pi} d\mu d\phi = 2\mu d\mu \frac{d\phi}{2\pi}\]

where \(\mu = \cos \theta\) is the cosine of the polar angle between reflected direction and the normal to the surface; and \(\theta\) is the azimuthal angle in \([0,2\pi]\). We can separate the multivariate probability density into two separate univariate density functions, one for the cosine of the polar angle,

(45)\[f(\mu) = 2\mu\]

and one for the azimuthal angle,

(46)\[f(\phi) = \frac{1}{2\pi}.\]

Each of these density functions can be sampled by analytical inversion of the cumulative distribution distribution, resulting in the following sampling scheme:

(47)\[\mu = \sqrt{\xi_1} \\ \phi = 2\pi\xi_2\]

where \(\xi_1\) and \(\xi_2\) are uniform random numbers on \([0,1)\). With the sampled values of \(\mu\) and \(\phi\), the final reflected direction vector can be computed via rotation of the surface normal using the equations from Transforming a Particle’s Coordinates. The white boundary condition can be applied to any kind of surface, as long as the normal to the surface is known as in Reflective Boundary Conditions.

Cross Section Representations

Continuous-Energy Data

In OpenMC, the data governing the interaction of neutrons with various nuclei for continous-energy problems are represented using an HDF5 format that can be produced by converting files in the ACE format, which is used by MCNP and Serpent. ACE-format data can be generated with the NJOY nuclear data processing system, which converts raw ENDF/B data into linearly-interpolable data as required by most Monte Carlo codes. Since ACE-format data can be converted into OpenMC’s HDF5 format, it is possible to perform direct comparison of OpenMC with other codes using the same underlying nuclear data library.

The ACE format contains continuous-energy cross sections for the following types of reactions: elastic scattering, fission (or first-chance fission, second-chance fission, etc.), inelastic scattering, \((n,xn)\), \((n,\gamma)\), and various other absorption reactions. For those reactions with one or more neutrons in the exit channel, secondary angle and energy distributions may be provided. In addition, fissionable nuclides have total, prompt, and/or delayed \(\nu\) as a function of energy and neutron precursor distributions. Many nuclides also have probability tables to be used for accurate treatment of self-shielding in the unresolved resonance range. For bound scatterers, separate tables with \(S(\alpha,\beta,T)\) scattering law data can be used.

Energy Grid Methods

The method by which continuous-energy cross sections for each nuclide in a problem are stored as a function of energy can have a substantial effect on the performance of a Monte Carlo simulation. Since the ACE format is based on linearly-interpolable cross sections, each nuclide has cross sections tabulated over a wide range of energies. Some nuclides may only have a few points tabulated (e.g. H-1) whereas other nuclides may have hundreds or thousands of points tabulated (e.g. U-238).

At each collision, it is necessary to sample the probability of having a particular type of interaction whether it be elastic scattering, \((n,2n)\), level inelastic scattering, etc. This requires looking up the microscopic cross sections for these reactions for each nuclide within the target material. Since each nuclide has a unique energy grid, it would be necessary to search for the appropriate index for each nuclide at every collision. This can become a very time-consuming process, especially if there are many nuclides in a problem as there would be for burnup calculations. Thus, there is a strong motive to implement a method of reducing the number of energy grid searches in order to speed up the calculation.

Logarithmic Mapping

To speed up energy grid searches, OpenMC uses a logarithmic mapping technique to limit the range of energies that must be searched for each nuclide. The entire energy range is divided up into equal-lethargy segments, and the bounding energies of each segment are mapped to bounding indices on each of the nuclide energy grids. By default, OpenMC uses 8000 equal-lethargy segments as recommended by Brown.

Other Methods

A good survey of other energy grid techniques, including unionized energy grids, can be found in a paper by Leppanen.

Windowed Multipole Representation

In addition to the usual pointwise representation of cross sections, OpenMC offers support for a data format called windowed multipole (WMP). This data format requires less memory than pointwise cross sections, and it allows on-the-fly Doppler broadening to arbitrary temperature.

The multipole method was introduced by Hwang and the faster windowed multipole method by Josey. In the multipole format, cross section resonances are represented by poles, \(p_j\), and residues, \(r_j\), in the complex plane. The 0K cross sections in the resolved resonance region can be computed by summing up a contribution from each pole:

\[\sigma(E, T=0\text{K}) = \frac{1}{E} \sum_j \text{Re} \left[ \frac{i r_j}{\sqrt{E} - p_j} \right] \]

Assuming free-gas thermal motion, cross sections in the multipole form can be analytically Doppler broadened to give the form:

\[\sigma(E, T) = \frac{1}{2 E \sqrt{\xi}} \sum_j \text{Re} \left[r_j \sqrt{\pi} W_i(z) - \frac{r_j}{\sqrt{\pi}} C \left(\frac{p_j}{\sqrt{\xi}}, \frac{u}{2 \sqrt{\xi}}\right)\right] \]
\[W_i(z) = \frac{i}{\pi} \int_{-\infty}^\infty dt \frac{e^{-t^2}}{z - t} \]
\[C \left(\frac{p_j}{\sqrt{\xi}},\frac{u}{2 \sqrt{\xi}}\right) = 2p_j \int_0^\infty du' \frac{e^{-(u + u')^2/4\xi}}{p_j^2 - u'^2} \]
\[z = \frac{\sqrt{E} - p_j}{2 \sqrt{\xi}} \]
\[\xi = \frac{k_B T}{4 A} \]
\[u = \sqrt{E} \]

where \(T\) is the temperature of the resonant scatterer, \(k_B\) is the Boltzmann constant, \(A\) is the mass of the target nucleus. For \(E \gg k_b T/A\), the \(C\) integral is approximately zero, simplifying the cross section to:

\[\sigma(E, T) = \frac{1}{2 E \sqrt{\xi}} \sum_j \text{Re} \left[i r_j \sqrt{\pi} W_i(z)\right] \]

The \(W_i\) integral simplifies down to an analytic form. We define the Faddeeva function, \(W\) as:

\[W(z) = e^{-z^2} \text{Erfc}(-iz) \]

Through this, the integral transforms as follows:

\[\text{Im} (z) > 0 : W_i(z) = W(z) \]
\[\text{Im} (z) < 0 : W_i(z) = -W(z^*)^* \]

There are freely available algorithms to evaluate the Faddeeva function. For many nuclides, the Faddeeva function needs to be evaluated thousands of times to calculate a cross section. To mitigate that computational cost, the WMP method only evaluates poles within a certain energy “window” around the incident neutron energy and accounts for the effect of resonances outside that window with a polynomial fit. This polynomial fit is then broadened exactly. This exact broadening can make up for the removal of the \(C\) integral, as typically at low energies, only curve fits are used.

Note that the implementation of WMP in OpenMC currently assumes that inelastic scattering does not occur in the resolved resonance region. This is usually, but not always the case. Future library versions may eliminate this issue.

The data format used by OpenMC to represent windowed multipole data is specified in Windowed Multipole Library Format with a publicly available WMP library.

Temperature Treatment

At the beginning of a simulation, OpenMC collects a list of all temperatures that are present in a model. It then uses this list to determine what cross sections to load. The data that is loaded depends on what temperature method has been selected. There are three methods available:

Nearest:Cross sections are loaded only if they are within a specified tolerance of the actual temperatures in the model.
Interpolation:Cross sections are loaded at temperatures that bound the actual temperatures in the model. During transport, cross sections for each material are calculated using statistical linear-linear interpolation between bounding temperature. Suppose cross sections are available at temperatures \(T_1, T_2, ..., T_n\) and a material is assigned a temperature \(T\) where \(T_i < T < T_{i+1}\). Statistical interpolation is applied as follows: a uniformly-distributed random number of the unit interval, \(\xi\), is sampled. If \(\xi < (T - T_i)/(T_{i+1} - T_i)\), then cross sections at temperature \(T_{i+1}\) are used. Otherwise, cross sections at \(T_i\) are used. This procedure is applied for pointwise cross sections in the resolved resonance range, unresolved resonance probability tables, and \(S(\alpha,\beta)\) thermal scattering tables.
Multipole:Resolved resonance cross sections are calculated on-the-fly using techniques/data described in Windowed Multipole Representation. Cross section data is loaded for a single temperature and is used in the unresolved resonance and fast energy ranges.

Multi-Group Data

The data governing the interaction of particles with various nuclei or materials are represented using a multi-group library format specific to the OpenMC code. The format is described in the MGXS Library Specification. The data itself can be prepared via traditional paths or directly from a continuous-energy OpenMC calculation by use of the Python API as is shown in an example notebook. This multi-group library consists of meta-data (such as the energy group structure) and multiple xsdata objects which contains the required microscopic or macroscopic multi-group data.

At a minimum, the library must contain the absorption cross section (\(\sigma_{a,g}\)) and a scattering matrix. If the problem is an eigenvalue problem then all fissionable materials must also contain either a fission production matrix cross section (\(\nu\sigma_{f,g\rightarrow g'}\)), or both the fission spectrum data (\(\chi_{g'}\)) and a fission production cross section (\(\nu\sigma_{f,g}\)), or, . The library must also contain the fission cross section (\(\sigma_{f,g}\)) or the fission energy release cross section (\(\kappa\sigma_{f,g}\)) if the associated tallies are required by the model using the library.

After a scattering collision, the outgoing particle experiences a change in both energy and angle. The probability of a particle resulting in a given outgoing energy group (g’) given a certain incoming energy group (g) is provided by the scattering matrix data. The angular information can be expressed either via Legendre expansion of the particle’s change-in-angle (\(\mu\)), a tabular representation of the probability distribution function of \(\mu\), or a histogram representation of the same PDF. The formats used to represent these are described in the MGXS Library Specification.

Unlike the continuous-energy mode, the multi-group mode does not explicitly track particles produced from scattering multiplication (i.e., \((n,xn)\)) reactions. These are instead accounted for by adjusting the weight of the particle after the collision such that the correct total weight is maintained. The weight adjustment factor is optionally provided by the multiplicity data which is required to be provided in the form of a group-wise matrix. This data is provided as a group-wise matrix since the probability of producing multiple particles in a scattering reaction depends on both the incoming energy, g, and the sampled outgoing energy, g’. This data represents the average number of particles emitted from a scattering reaction, given a scattering reaction has occurred:

\[multiplicity_{g \rightarrow g'} = \frac{\nu_{scatter}\sigma_{s,g \rightarrow g'}}{ \sigma_{s,g \rightarrow g'}}\]

If this scattering multiplication information is not provided in the library then no weight adjustment will be performed. This is equivalent to neglecting any additional particles produced in scattering multiplication reactions. However, this assumption will result in a loss of accuracy since the total particle population would not be conserved. This reduction in accuracy due to the loss in particle conservation can be mitigated by reducing the absorption cross section as needed to maintain particle conservation. This adjustment can be done when generating the library, or by OpenMC. To have OpenMC perform the adjustment, the total cross section (\(\sigma_{t,g}\)) must be provided. With this information, OpenMC will then adjust the absorption cross section as follows:

\[\sigma_{a,g} = \sigma_{t,g} - \sum_{g'}\nu_{scatter}\sigma_{s,g \rightarrow g'}\]

The above method is the same as is usually done with most deterministic solvers. Note that this method is less accurate than using the scattering multiplication weight adjustment since simply reducing the absorption cross section does not include any information about the outgoing energy of the particles produced in these reactions.

All of the data discussed in this section can be provided to the code independent of the particle’s direction of motion (i.e., isotropic), or the data can be provided as a tabular distribution of the polar and azimuthal particle direction angles. The isotropic representation is the most commonly used, however inaccuracies are to be expected especially near material interfaces where a material has a very large cross sections relative to the other material (as can be expected in the resonance range). The angular representation can be used to minimize this error.

Finally, the above options for representing the physics do not have to be consistent across the problem. The number of groups and the structure, however, does have to be consistent across the data sets. That is to say that each microscopic or macroscopic data set does not have to apply the same scattering expansion, treatment of multiplicity or angular representation of the cross sections. This allows flexibility for the model to use highly anisotropic scattering information in the water while the fuel can be simulated with linear or even isotropic scattering.

Random Number Generation

In order to sample probability distributions, one must be able to produce random numbers. The standard technique to do this is to generate numbers on the interval \([0,1)\) from a deterministic sequence that has properties that make it appear to be random, e.g. being uniformly distributed and not exhibiting correlation between successive terms. Since the numbers produced this way are not truly “random” in a strict sense, they are typically referred to as pseudorandom numbers, and the techniques used to generate them are pseudorandom number generators (PRNGs). Numbers sampled on the unit interval can then be transformed for the purpose of sampling other continuous or discrete probability distributions.

Linear Congruential Generators

There are a great number of algorithms for generating random numbers. One of the simplest and commonly used algorithms is called a linear congruential generator. We start with a random number seed \(\xi_0\) and a sequence of random numbers can then be generated using the following recurrence relation:

(1)\[\xi_{i+1} = g \xi_i + c \mod M\]

where \(g\), \(c\), and \(M\) are constants. The choice of these constants will have a profound effect on the quality and performance of the generator, so they should not be chosen arbitrarily. As Donald Knuth stated in his seminal work The Art of Computer Programming, “random numbers should not be generated with a method chosen at random. Some theory should be used.” Typically, \(M\) is chosen to be a power of two as this enables \(x \mod M\) to be performed using the bitwise AND operator with a bit mask. The constants for the linear congruential generator used by default in OpenMC are \(g = 2806196910506780709\), \(c = 1\), and \(M = 2^{63}\) (see L’Ecuyer).

Skip-ahead Capability

One of the important capabilities for a random number generator is to be able to skip ahead in the sequence of random numbers. Without this capability, it would be very difficult to maintain reproducibility in a parallel calculation. If we want to skip ahead \(N\) random numbers and \(N\) is large, the cost of sampling \(N\) random numbers to get to that position may be prohibitively expensive. Fortunately, algorithms have been developed that allow us to skip ahead in \(O(\log_2 N)\) operations instead of \(O(N)\). One algorithm to do so is described in a paper by Brown. This algorithm relies on the following relationship:

(2)\[\xi_{i+k} = g^k \xi_i + c \frac{g^k - 1}{g - 1} \mod M\]

Note that equation (2) has the same general form as equation (1), so the idea is to determine the new multiplicative and additive constants in \(O(\log_2 N)\) operations.

References

Neutron Physics

There are limited differences between physics treatments used in the continuous-energy and multi-group modes. If distinctions are necessary, each of the following sections will provide an explanation of the differences. Otherwise, replacing any references of the particle’s energy (E) with references to the particle’s energy group (g) will suffice.

Sampling Distance to Next Collision

As a particle travels through a homogeneous material, the probability distribution function for the distance to its next collision \(\ell\) is

(1)\[p(\ell) d\ell = \Sigma_t e^{-\Sigma_t \ell} d\ell\]

where \(\Sigma_t\) is the total macroscopic cross section of the material. Equation (1) tells us that the further the distance is to the next collision, the less likely the particle will travel that distance. In order to sample the probability distribution function, we first need to convert it to a cumulative distribution function

(2)\[\int_0^{\ell} d\ell' p(\ell') = \int_0^{\ell} d\ell' \Sigma_t e^{-\Sigma_t \ell'} = 1 - e^{-\Sigma_t \ell}.\]

By setting the cumulative distribution function equal to \(\xi\), a random number on the unit interval, and solving for the distance \(\ell\), we obtain a formula for sampling the distance to next collision:

(3)\[\ell = -\frac{\ln (1 - \xi)}{\Sigma_t}.\]

Since \(\xi\) is uniformly distributed on \([0,1)\), this implies that \(1 - \xi\) is also uniformly distributed on \([0,1)\) as well. Thus, the formula usually used to calculate the distance to next collision is

(4)\[\ell = -\frac{\ln \xi}{\Sigma_t}\]

\((n,\gamma)\) and Other Disappearance Reactions

All absorption reactions other than fission do not produce any secondary neutrons. As a result, these are the easiest type of reactions to handle. When a collision occurs, the first step is to sample a nuclide within a material. Once the nuclide has been sampled, then a specific reaction for that nuclide is sampled. Since the total absorption cross section is pre-calculated at the beginning of a simulation, the first step in sampling a reaction is to determine whether a “disappearance” reaction occurs where no secondary neutrons are produced. This is done by sampling a random number \(\xi\) on the interval \([0,1)\) and checking whether

(5)\[\xi \sigma_t (E) < \sigma_a (E) - \sigma_f (E)\]

where \(\sigma_t\) is the total cross section, \(\sigma_a\) is the absorption cross section (this includes fission), and \(\sigma_f\) is the total fission cross section. If this condition is met, then the neutron is killed and we proceed to simulate the next neutron from the source bank.

Note that photons arising from \((n,\gamma)\) and other neutron reactions are not produced in a microscopically correct manner. Instead, photons are sampled probabilistically at each neutron collision, regardless of what reaction actually takes place. This is described in more detail in Photon Production.

Elastic Scattering

Note that the multi-group mode makes no distinction between elastic or inelastic scattering reactions. The specific multi-group scattering implementation is discussed in the Multi-Group Scattering section.

Elastic scattering refers to the process by which a neutron scatters off a nucleus and does not leave it in an excited. It is referred to as “elastic” because in the center-of-mass system, the neutron does not actually lose energy. However, in lab coordinates, the neutron does indeed lose energy. Elastic scattering can be treated exactly in a Monte Carlo code thanks to its simplicity.

Let us discuss how OpenMC handles two-body elastic scattering kinematics. The first step is to determine whether the target nucleus has any associated motion. Above a certain energy threshold (400 kT by default), all scattering is assumed to take place with the target at rest. Below this threshold though, we must account for the thermal motion of the target nucleus. Methods to sample the velocity of the target nucleus are described later in section Effect of Thermal Motion on Cross Sections. For the time being, let us assume that we have sampled the target velocity \(\mathbf{v}_t\). The velocity of the center-of-mass system is calculated as

(6)\[\mathbf{v}_{cm} = \frac{\mathbf{v}_n + A \mathbf{v}_t}{A + 1}\]

where \(\mathbf{v}_n\) is the velocity of the neutron and \(A\) is the atomic mass of the target nucleus measured in neutron masses (commonly referred to as the atomic weight ratio). With the velocity of the center-of-mass calculated, we can then determine the neutron’s velocity in the center-of-mass system:

(7)\[\mathbf{V}_n = \mathbf{v}_n - \mathbf{v}_{cm}\]

where we have used uppercase \(\mathbf{V}\) to denote the center-of-mass system. The direction of the neutron in the center-of-mass system is

(8)\[\mathbf{\Omega}_n = \frac{\mathbf{V}_n}{|| \mathbf{V}_n ||}.\]

At low energies, elastic scattering will be isotropic in the center-of-mass system, but for higher energies, there may be p-wave and higher order scattering that leads to anisotropic scattering. Thus, in general, we need to sample a cosine of the scattering angle which we will refer to as \(\mu\). For elastic scattering, the secondary angle distribution is always given in the center-of-mass system and is sampled according to the procedure outlined in Sampling Angular Distributions. After the cosine of the angle of scattering has been sampled, we need to determine the neutron’s new direction \(\mathbf{\Omega}'_n\) in the center-of-mass system. This is done with the procedure in Transforming a Particle’s Coordinates. The new direction is multiplied by the speed of the neutron in the center-of-mass system to obtain the new velocity vector in the center-of-mass:

(9)\[\mathbf{V}'_n = || \mathbf{V}_n || \mathbf{\Omega}'_n.\]

Finally, we transform the velocity in the center-of-mass system back to lab coordinates:

(10)\[\mathbf{v}'_n = \mathbf{V}'_n + \mathbf{v}_{cm}\]

In OpenMC, the angle and energy of the neutron are stored rather than the velocity vector itself, so the post-collision angle and energy can be inferred from the post-collision velocity of the neutron in the lab system.

For tallies that require the scattering cosine, it is important to store the scattering cosine in the lab system. If we know the scattering cosine in the center-of-mass, the scattering cosine in the lab system can be calculated as

(11)\[\mu_{lab} = \frac{1 + A\mu}{\sqrt{A^2 + 2A\mu + 1}}.\]

However, equation (11) is only valid if the target was at rest. When the target nucleus does have thermal motion, the cosine of the scattering angle can be determined by simply taking the dot product of the neutron’s initial and final direction in the lab system.

Inelastic Scattering

Note that the multi-group mode makes no distinction between elastic or inelastic scattering reactions. The spceific multi-group scattering implementation is discussed in the Multi-Group Scattering section.

The major algorithms for inelastic scattering were described in previous sections. First, a scattering cosine is sampled using the algorithms in Sampling Angular Distributions. Then an outgoing energy is sampled using the algorithms in Sampling Energy Distributions. If the outgoing energy and scattering cosine were given in the center-of-mass system, they are transformed to laboratory coordinates using the algorithm described in Transforming a Particle’s Coordinates. Finally, the direction of the particle is changed also using the procedure in Transforming a Particle’s Coordinates.

Although inelastic scattering leaves the target nucleus in an excited state, no secondary photons from nuclear de-excitation are tracked in OpenMC.

\((n,xn)\) Reactions

Note that the multi-group mode makes no distinction between elastic or inelastic scattering reactions. The specific multi-group scattering implementation is discussed in the Multi-Group Scattering section.

These types of reactions are just treated as inelastic scattering and as such are subject to the same procedure as described in Inelastic Scattering. For reactions with integral multiplicity, e.g., \((n,2n)\), an appropriate number of secondary neutrons are created. For reactions that have a multiplicity given as a function of the incoming neutron energy (which occasionally occurs for MT=5), the weight of the outgoing neutron is multiplied by the multiplicity.

Multi-Group Scattering

In multi-group mode, a scattering collision requires that the outgoing energy group of the simulated particle be selected from a probability distribution, the change-in-angle selected from a probability distribution according to the outgoing energy group, and finally the particle’s weight adjusted again according to the outgoing energy group.

The first step in selecting an outgoing energy group for a particle in a given incoming energy group is to select a random number (\(\xi\)) between 0 and 1. This number is then compared to the cumulative distribution function produced from the outgoing group (g’) data for the given incoming group (g):

\[CDF = \sum_{g'=1}^{h}\Sigma_{s,g \rightarrow g'} \]

If the scattering data is represented as a Legendre expansion, then the value of \(\Sigma_{s,g \rightarrow g'}\) above is the 0th order for the given group transfer. If the data is provided as tabular or histogram data, then \(\Sigma_{s,g \rightarrow g'}\) is the sum of all bins of data for a given g and g’ pair.

Now that the outgoing energy is known the change-in-angle, \(\mu\) can be determined. If the data is provided as a Legendre expansion, this is done by rejection sampling of the probability distribution represented by the Legendre series. For efficiency, the selected values of the PDF (\(f(\mu)\)) are chosen to be between 0 and the maximum value of \(f(\mu)\) in the domain of -1 to 1. Note that this sampling scheme automatically forces negative values of the \(f(\mu)\) probability distribution function to be treated as zero probabilities.

If the angular data is instead provided as a tabular representation, then the value of \(\mu\) is selected as described in the Tabular Angular Distribution section with a linear-linear interpolation scheme.

If the angular data is provided as a histogram representation, then the value of \(\mu\) is selected in a similar fashion to that described for the selection of the outgoing energy (since the energy group representation is simply a histogram representation) except the CDF is composed of the angular bins and not the energy groups. However, since we are interested in a specific value of \(\mu\) instead of a group, then an angle is selected from a uniform distribution within from the chosen angular bin.

The final step in the scattering treatment is to adjust the weight of the neutron to account for any production of neutrons due to \((n,xn)\) reactions. This data is obtained from the multiplicity data provided in the multi-group cross section library for the material of interest. The scaled value will default to 1.0 if no value is provided in the library.

Fission

While fission is normally considered an absorption reaction, as far as it concerns a Monte Carlo simulation it actually bears more similarities to inelastic scattering since fission results in secondary neutrons in the exit channel. Other absorption reactions like \((n,\gamma)\) or \((n,\alpha)\), on the contrary, produce no neutrons. There are a few other idiosyncrasies in treating fission. In an eigenvalue calculation, secondary neutrons from fission are only “banked” for use in the next generation rather than being tracked as secondary neutrons from elastic and inelastic scattering would be. On top of this, fission is sometimes broken into first-chance fission, second-chance fission, etc. The nuclear data file either lists the partial fission reactions with secondary energy distributions for each one, or a total fission reaction with a single secondary energy distribution.

When a fission reaction is sampled in OpenMC (either total fission or, if data exists, first- or second-chance fission), the following algorithm is used to create and store fission sites for the following generation. First, the average number of prompt and delayed neutrons must be determined to decide whether the secondary neutrons will be prompt or delayed. This is important because delayed neutrons have a markedly different spectrum from prompt neutrons, one that has a lower average energy of emission. The total number of neutrons emitted \(\nu_t\) is given as a function of incident energy in the ENDF format. Two representations exist for \(\nu_t\). The first is a polynomial of order \(N\) with coefficients \(c_0,c_1,\dots,c_N\). If \(\nu_t\) has this format, we can evaluate it at incoming energy \(E\) by using the equation

(12)\[\nu_t (E) = \sum_{i = 0}^N c_i E^i.\]

The other representation is just a tabulated function with a specified interpolation law. The number of prompt neutrons released per fission event \(\nu_p\) is also given as a function of incident energy and can be specified in a polynomial or tabular format. The number of delayed neutrons released per fission event \(\nu_d\) can only be specified in a tabular format. In practice, we only need to determine \(nu_t\) and \(nu_d\). Once these have been determined, we can calculated the delayed neutron fraction

(13)\[\beta = \frac{\nu_d}{\nu_t}.\]

We then need to determine how many total neutrons should be emitted from fission. If no survival biasing is being used, then the number of neutrons emitted is

(14)\[\nu = \frac{w \nu_t}{k_{eff}}\]

where \(w\) is the statistical weight and \(k_{eff}\) is the effective multiplication factor from the previous generation. The number of neutrons produced is biased in this manner so that the expected number of fission neutrons produced is the number of source particles that we started with in the generation. Since \(\nu\) is not an integer, we use the following procedure to obtain an integral number of fission neutrons to produce. If \(\xi > \nu - \lfloor \nu \rfloor\), then we produce \(\lfloor \nu \rfloor\) neutrons. Otherwise, we produce \(\lfloor \nu \rfloor + 1\) neutrons. Then, for each fission site produced, we sample the outgoing angle and energy according to the algorithms given in Sampling Angular Distributions and Sampling Energy Distributions respectively. If the neutron is to be born delayed, then there is an extra step of sampling a delayed neutron precursor group since they each have an associated secondary energy distribution.

The sampled outgoing angle and energy of fission neutrons along with the position of the collision site are stored in an array called the fission bank. In a subsequent generation, these fission bank sites are used as starting source sites.

The above description is similar for the multi-group mode except the data are provided as group-wise data instead of in a continuous-energy format. In this case, the outgoing energy of the fission neutrons are represented as histograms by way of either the nu-fission matrix or chi vector.

Secondary Angle-Energy Distributions

Note that this section is specific to continuous-energy mode since the multi-group scattering process has already been described including the secondary energy and angle sampling.

For a reaction with secondary products, it is necessary to determine the outgoing angle and energy of the products. For any reaction other than elastic and level inelastic scattering, the outgoing energy must be determined based on tabulated or parameterized data. The ENDF-6 Format specifies a variety of ways that the secondary energy distribution can be represented. ENDF File 5 contains uncorrelated energy distribution whereas ENDF File 6 contains correlated energy-angle distributions. The ACE format specifies its own representations based loosely on the formats given in ENDF-6. OpenMC’s HDF5 nuclear data files use a combination of ENDF and ACE distributions; in this section, we will describe how the outgoing angle and energy of secondary particles are sampled.

One of the subtleties in the nuclear data format is the fact that a single reaction product can have multiple angle-energy distributions. This is mainly useful for reactions with multiple products of the same type in the exit channel such as \((n,2n)\) or \((n,3n)\). In these types of reactions, each neutron is emitted corresponding to a different excitation level of the compound nucleus, and thus in general the neutrons will originate from different energy distributions. If multiple angle-energy distributions are present, they are assigned incoming-energy-dependent probabilities that can then be used to randomly select one.

Once a distribution has been selected, the procedure for determining the outgoing angle and energy will depend on the type of the distribution.

Uncorrelated Angle-Energy Distributions

The first set of distributions we will look at are uncorrelated angle-energy distributions, where angle and energy are specified separately. For these distributions, OpenMC first samples the angular distribution as described Sampling Angular Distributions and then samples an energy as described in Sampling Energy Distributions.

Sampling Angular Distributions

For elastic scattering, it is only necessary to specific a secondary angle distribution since the outgoing energy can be determined analytically. Other reactions may also have separate secondary angle and secondary energy distributions that are uncorrelated. In these cases, the secondary angle distribution is represented as either

  • An isotropic angular distribution,
  • A tabular distribution.
Isotropic Angular Distribution

In the first case, no data is stored in the nuclear data file, and the cosine of the scattering angle is simply calculated as

(15)\[\mu = 2\xi - 1\]

where \(\mu\) is the cosine of the scattering angle and \(\xi\) is a random number sampled uniformly on \([0,1)\).

Tabular Angular Distribution

In this case, we have a table of cosines and their corresponding values for a probability distribution function and cumulative distribution function. For each incoming neutron energy \(E_i\), let us call \(p_{i,j}\) the j-th value in the probability distribution function and \(c_{i,j}\) the j-th value in the cumulative distribution function. We first find the interpolation factor on the incoming energy grid:

(16)\[f = \frac{E - E_i}{E_{i+1} - E_i}\]

where \(E\) is the incoming energy of the particle. Then, statistical interpolation is performed to choose between using the cosines and distribution functions corresponding to energy \(E_i\) and \(E_{i+1}\). Let \(\ell\) be the chosen table where \(\ell = i\) if \(\xi_1 > f\) and \(\ell = i + 1\) otherwise, where \(\xi_1\) is a random number. Another random number \(\xi_2\) is used to sample a scattering cosine bin \(j\) using the cumulative distribution function:

(17)\[c_{\ell,j} < \xi_2 < c_{\ell,j+1}\]

The final scattering cosine will depend on whether histogram or linear-linear interpolation is used. In general, we can write the cumulative distribution function as

(18)\[c(\mu) = \int_{-1}^\mu p(\mu') d\mu'\]

where \(c(\mu)\) is the cumulative distribution function and \(p(\mu)\) is the probability distribution function. Since we know that \(c(\mu_{\ell,j}) = c_{\ell,j}\), this implies that for \(\mu > \mu_{\ell,j}\),

(19)\[c(\mu) = c_{\ell,j} + \int_{\mu_{\ell,j}}^{\mu} p(\mu') d\mu'\]

For histogram interpolation, we have that \(p(\mu') = p_{\ell,j}\) for \(\mu_{\ell,j} \le \mu' < \mu_{\ell,j+1}\). Thus, after integrating (19) we have that

(20)\[c(\mu) = c_{\ell,j} + (\mu - \mu_{\ell,j}) p_{\ell,j} = \xi_2\]

Solving for the scattering cosine, we obtain the final form for histogram interpolation:

(21)\[\mu = \mu_{\ell,j} + \frac{\xi_2 - c_{\ell,j}}{p_{\ell,j}}.\]

For linear-linear interpolation, we represent the function \(p(\mu')\) as a first-order polynomial in \(\mu'\). If we interpolate between successive values on the probability distribution function, we know that

(22)\[p(\mu') - p_{\ell,j} = \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}} (\mu' - \mu_{\ell,j})\]

Solving for \(p(\mu')\) in equation (22) and inserting it into equation (19), we obtain

(23)\[c(\mu) = c_{\ell,j} + \int_{\mu_{\ell,j}}^{\mu} \left [ \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}} (\mu' - \mu_{\ell,j}) + p_{\ell,j} \right ] d\mu'.\]

Let us now make a change of variables using

(24)\[\eta = \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}} (\mu' - \mu_{\ell,j}) + p_{\ell,j}.\]

Equation (23) then becomes

(25)\[c(\mu) = c_{\ell,j} + \frac{1}{m} \int_{p_{\ell,j}}^{m(\mu - \mu_{\ell,j}) + p_{\ell,j}} \eta \, d\eta\]

where we have used

(26)\[m = \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}}.\]

Integrating equation (25), we have

(27)\[c(\mu) = c_{\ell,j} + \frac{1}{2m} \left ( \left [ m (\mu - \mu_{\ell,j} ) + p_{\ell,j} \right ]^2 - p_{\ell,j}^2 \right ) = \xi_2\]

Solving for \(\mu\), we have the final form for the scattering cosine using linear-linear interpolation:

(28)\[\mu = \mu_{\ell,j} + \frac{1}{m} \left ( \sqrt{p_{\ell,j}^2 + 2 m (\xi_2 - c_{\ell,j} )} - p_{\ell,j} \right )\]
Sampling Energy Distributions
Inelastic Level Scattering

It can be shown (see Foderaro) that in inelastic level scattering, the outgoing energy of the neutron \(E'\) can be related to the Q-value of the reaction and the incoming energy:

(29)\[E' = \left ( \frac{A}{A+1} \right )^2 \left ( E - \frac{A + 1}{A} Q \right )\]

where \(A\) is the mass of the target nucleus measured in neutron masses.

Continuous Tabular Distribution

In a continuous tabular distribution, a tabulated energy distribution is provided for each of a set of incoming energies. While the representation itself is simple, the complexity lies in how one interpolates between incident as well as outgoing energies on such a table. If one performs simple interpolation between tables for neighboring incident energies, it is possible that the resulting energies would violate laws governing the kinematics, i.e., the outgoing energy may be outside the range of available energy in the reaction.

To avoid this situation, the accepted practice is to use a process known as scaled interpolation. First, we find the tabulated incident energies which bound the actual incoming energy of the particle, i.e., find \(i\) such that \(E_i < E < E_{i+1}\) and calculate the interpolation factor \(f\) via (16). Then, we interpolate between the minimum and maximum energies of the outgoing energy distributions corresponding to \(E_i\) and \(E_{i+1}\):

(30)\[E_{min} = E_{i,1} + f ( E_{i+1,1} - E_{i,1} ) \\ E_{max} = E_{i,M} + f ( E_{i+1,M} - E_{i,M} )\]

where \(E_{min}\) and \(E_{max}\) are the minimum and maximum outgoing energies of a scaled distribution, \(E_{i,j}\) is the j-th outgoing energy corresponding to the incoming energy \(E_i\), and \(M\) is the number of outgoing energy bins.

Next, statistical interpolation is performed to choose between using the outgoing energy distributions corresponding to energy \(E_i\) and \(E_{i+1}\). Let \(\ell\) be the chosen table where \(\ell = i\) if \(\xi_1 > f\) and \(\ell = i + 1\) otherwise, and \(\xi_1\) is a random number. For each incoming neutron energy \(E_i\), let us call \(p_{i,j}\) the j-th value in the probability distribution function, \(c_{i,j}\) the j-th value in the cumulative distribution function, and \(E_{i,j}\) the j-th outgoing energy. We then sample an outgoing energy bin \(j\) using the cumulative distribution function:

(31)\[c_{\ell,j} < \xi_2 < c_{\ell,j+1}\]

where \(\xi_2\) is a random number sampled uniformly on \([0,1)\). At this point, we need to interpolate between the successive values on the outgoing energy distribution using either histogram or linear-linear interpolation. The formulas for these can be derived along the same lines as those found in Tabular Angular Distribution. For histogram interpolation, the interpolated outgoing energy on the \(\ell\)-th distribution is

(32)\[\hat{E} = E_{\ell,j} + \frac{\xi_2 - c_{\ell,j}}{p_{\ell,j}}.\]

If linear-linear interpolation is to be used, the outgoing energy on the \(\ell\)-th distribution is

(33)\[\hat{E} = E_{\ell,j} + \frac{E_{\ell,j+1} - E_{\ell,j}}{p_{\ell,j+1} - p_{\ell,j}} \left ( \sqrt{p_{\ell,j}^2 + 2 \frac{p_{\ell,j+1} - p_{\ell,j}}{E_{\ell,j+1} - E_{\ell,j}} ( \xi_2 - c_{\ell,j} )} - p_{\ell,j} \right ).\]

Since this outgoing energy may violate reaction kinematics, we then scale it to minimum and maximum energies calculated in equation (30) to get the final outgoing energy:

(34)\[E' = E_{min} + \frac{\hat{E} - E_{\ell,1}}{E_{\ell,M} - E_{\ell,1}} (E_{max} - E_{min})\]

where \(E_{min}\) and \(E_{max}\) are defined the same as in equation (30).

Maxwell Fission Spectrum

One representation of the secondary energies for neutrons from fission is the so-called Maxwell spectrum. A probability distribution for the Maxwell spectrum can be written in the form

(35)\[p(E') dE' = c E'^{1/2} e^{-E'/T(E)} dE'\]

where \(E\) is the incoming energy of the neutron and \(T\) is the so-called nuclear temperature, which is a function of the incoming energy of the neutron. The ENDF format contains a list of nuclear temperatures versus incoming energies. The nuclear temperature is interpolated between neighboring incoming energies using a specified interpolation law. Once the temperature \(T\) is determined, we then calculate a candidate outgoing energy based on rule C64 in the Monte Carlo Sampler:

(36)\[E' = -T \left [ \log (\xi_1) + \log (\xi_2) \cos^2 \left ( \frac{\pi \xi_3}{2} \right ) \right ]\]

where \(\xi_1, \xi_2, \xi_3\) are random numbers sampled on the unit interval. The outgoing energy is only accepted if

(37)\[0 \le E' \le E - U\]

where \(U\) is called the restriction energy and is specified in the ENDF data. If the outgoing energy is rejected, it is resampled using equation (36).

Evaporation Spectrum

Evaporation spectra are primarily used in compound nucleus processes where a secondary particle can “evaporate” from the compound nucleus if it has sufficient energy. The probability distribution for an evaporation spectrum can be written in the form

(38)\[p(E') dE' = c E' e^{-E'/T(E)} dE'\]

where \(E\) is the incoming energy of the neutron and \(T\) is the nuclear temperature, which is a function of the incoming energy of the neutron. The ENDF format contains a list of nuclear temperatures versus incoming energies. The nuclear temperature is interpolated between neighboring incoming energies using a specified interpolation law. Once the temperature \(T\) is determined, we then calculate a candidate outgoing energy based on the algorithm given in LA-UR-14-27694:

(39)\[E' = -T \log ((1 - g\xi_1)(1 - g\xi_2))\]

where \(g = 1 - e^{-w}\), \(w = (E - U)/T\), \(U\) is the restriction energy, and \(\xi_1, \xi_2\) are random numbers sampled on the unit interval. The outgoing energy is only accepted according to the restriction energy as in equation (37). This algorithm has a much higher rejection efficiency than the standard technique, i.e. rule C45 in the Monte Carlo Sampler.

Energy-Dependent Watt Spectrum

The probability distribution for a Watt fission spectrum can be written in the form

(40)\[p(E') dE' = c e^{-E'/a(E)} \sinh \sqrt{b(E) \, E'} dE'\]

where \(a\) and \(b\) are parameters for the distribution and are given as tabulated functions of the incoming energy of the neutron. These two parameters are interpolated on the incoming energy grid using a specified interpolation law. Once the parameters have been determined, we sample a Maxwellian spectrum with nuclear temperature \(a\) using the algorithm described in Maxwell Fission Spectrum to get an energy \(W\). Then, the outgoing energy is calculated as

(41)\[E' = W + \frac{a^2 b}{4} + (2\xi - 1) \sqrt{a^2 b W}\]

where \(\xi\) is a random number sampled on the interval \([0,1)\). The outgoing energy is only accepted according to a specified restriction energy \(U\) as defined in equation (37).

A derivation of the algorithm described here can be found in a paper by Romano.

Product Angle-Energy Distributions

If the secondary distribution for a product was given in file 6 in ENDF, the angle and energy are correlated with one another and cannot be sampled separately. Several representations exist in ENDF/ACE for correlated angle-energy distributions.

Kalbach-Mann Correlated Scattering

This law is very similar to the uncorrelated continuous tabular energy distribution except now the outgoing angle of the neutron is correlated to the outgoing energy and is not sampled from a separate distribution. For each incident neutron energy \(E_i\) tabulated, there is an array of precompound factors \(R_{i,j}\) and angular distribution slopes \(A_{i,j}\) corresponding to each outgoing energy bin \(j\) in addition to the outgoing energies and distribution functions as in Continuous Tabular Distribution.

The calculation of the outgoing energy of the neutron proceeds exactly the same as in the algorithm described in Continuous Tabular Distribution. In that algorithm, we found an interpolation factor \(f\), statistically sampled an incoming energy bin \(\ell\), and sampled an outgoing energy bin \(j\) based on the tabulated cumulative distribution function. Once the outgoing energy has been determined with equation (34), we then need to calculate the outgoing angle based on the tabulated Kalbach-Mann parameters. These parameters themselves are subject to either histogram or linear-linear interpolation on the outgoing energy grid. For histogram interpolation, the parameters are

(42)\[R = R_{\ell,j} \\ A = A_{\ell,j}.\]

If linear-linear interpolation is specified, the parameters are

(43)\[R = R_{\ell,j} + \frac{\hat{E} - E_{\ell,j}}{E_{\ell,j+1} - E_{\ell,j}} ( R_{\ell,j+1} - R_{\ell,j} ) \\ A = A_{\ell,j} + \frac{\hat{E} - E_{\ell,j}}{E_{\ell,j+1} - E_{\ell,j}} ( A_{\ell,j+1} - A_{\ell,j} )\]

where \(\hat{E}\) is defined in equation (33). With the parameters determined, the probability distribution function for the cosine of the scattering angle is

(44)\[p(\mu) d\mu = \frac{A}{2 \sinh (A)} \left [ \cosh (A\mu) + R \sinh (A\mu) \right ] d\mu.\]

The rules for sampling this probability distribution function can be derived based on rules C39 and C40 in the Monte Carlo Sampler. First, we sample two random numbers \(\xi_3, \xi_4\) on the unit interval. If \(\xi_3 > R\) then the outgoing angle is

(45)\[\mu = \frac{1}{A} \ln \left ( T + \sqrt{T^2 + 1} \right )\]

where \(T = (2 \xi_4 - 1) \sinh (A)\). If \(\xi_3 \le R\), then the outgoing angle is

(46)\[\mu = \frac{1}{A} \ln \left ( \xi_4 e^A + (1 - \xi_4) e^{-A} \right ).\]
Correlated Energy and Angle Distribution

This distribution is very similar to a Kalbach-Mann distribution in the sense that the outgoing angle of the neutron is correlated to the outgoing energy and is not sampled from a separate distribution. In this case though, rather than being determined from an analytical distribution function, the cosine of the scattering angle is determined from a tabulated distribution. For each incident energy \(i\) and outgoing energy \(j\), there is a tabulated angular distribution.

The calculation of the outgoing energy of the neutron proceeds exactly the same as in the algorithm described in Continuous Tabular Distribution. In that algorithm, we found an interpolation factor \(f\), statistically sampled an incoming energy bin \(\ell\), and sampled an outgoing energy bin \(j\) based on the tabulated cumulative distribution function. Once the outgoing energy has been determined with equation (34), we then need to decide which angular distribution to use. If histogram interpolation was used on the outgoing energy bins, then we use the angular distribution corresponding to incoming energy bin \(\ell\) and outgoing energy bin \(j\). If linear-linear interpolation was used on the outgoing energy bins, then we use the whichever angular distribution was closer to the sampled value of the cumulative distribution function for the outgoing energy. The actual algorithm used to sample the chosen tabular angular distribution has been previously described in Tabular Angular Distribution.

N-Body Phase Space Distribution

Reactions in which there are more than two products of similar masses are sometimes best treated by using what’s known as an N-body phase distribution. This distribution has the following probability density function for outgoing energy and angle of the \(i\)-th particle in the center-of-mass system:

(47)\[p_i(\mu, E') dE' d\mu = C_n \sqrt{E'} (E_i^{max} - E')^{(3n/2) - 4} dE' d\mu\]

where \(n\) is the number of outgoing particles, \(C_n\) is a normalization constant, \(E_i^{max}\) is the maximum center-of-mass energy for particle \(i\), and \(E'\) is the outgoing energy. We see in equation (47) that the angle is simply isotropic in the center-of-mass system. The algorithm for sampling the outgoing energy is based on algorithms R28, C45, and C64 in the Monte Carlo Sampler. First we calculate the maximum energy in the center-of-mass using the following equation:

(48)\[E_i^{max} = \frac{A_p - 1}{A_p} \left ( \frac{A}{A+1} E + Q \right )\]

where \(A_p\) is the total mass of the outgoing particles in neutron masses, \(A\) is the mass of the original target nucleus in neutron masses, and \(Q\) is the Q-value of the reaction. Next we sample a value \(x\) from a Maxwell distribution with a nuclear temperature of one using the algorithm outlined in Maxwell Fission Spectrum. We then need to determine a value \(y\) that will depend on how many outgoing particles there are. For \(n = 3\), we simply sample another Maxwell distribution with unity nuclear temperature. For \(n = 4\), we use the equation

(49)\[y = -\ln ( \xi_1 \xi_2 \xi_3 )\]

where \(\xi_i\) are random numbers sampled on the interval \([0,1)\). For \(n = 5\), we use the equation

(50)\[y = -\ln ( \xi_1 \xi_2 \xi_3 \xi_4 ) - \ln ( \xi_5 ) \cos^2 \left ( \frac{\pi}{2} \xi_6 \right )\]

After \(x\) and \(y\) have been determined, the outgoing energy is then calculated as

(51)\[E' = \frac{x}{x + y} E_i^{max}\]

There are two important notes to make regarding the N-body phase space distribution. First, the documentation (and code) for MCNP5-1.60 has a mistake in the algorithm for \(n = 4\). That being said, there are no existing nuclear data evaluations which use an N-body phase space distribution with \(n = 4\), so the error would not affect any calculations. In the ENDF/B-VII.1 nuclear data evaluation, only one reaction uses an N-body phase space distribution at all, the \((n,2n)\) reaction with H-2.

Transforming a Particle’s Coordinates

Since all the multi-group data exists in the laboratory frame of reference, this section does not apply to the multi-group mode.

Once the cosine of the scattering angle \(\mu\) has been sampled either from a angle distribution or a correlated angle-energy distribution, we are still left with the task of transforming the particle’s coordinates. If the outgoing energy and scattering cosine were given in the center-of-mass system, then we first need to transform these into the laboratory system. The relationship between the outgoing energy in center-of-mass and laboratory is

(52)\[E' = E'_{cm} + \frac{E + 2\mu_{cm} (A + 1) \sqrt{EE'_{cm}}}{(A+1)^2}.\]

where \(E'_{cm}\) is the outgoing energy in the center-of-mass system, \(\mu_{cm}\) is the scattering cosine in the center-of-mass system, \(E'\) is the outgoing energy in the laboratory system, and \(E\) is the incident neutron energy. The relationship between the scattering cosine in center-of-mass and laboratory is

(53)\[\mu = \mu_{cm} \sqrt{\frac{E'_{cm}}{E'}} + \frac{1}{A + 1} \sqrt{\frac{E}{E'}}\]

where \(\mu\) is the scattering cosine in the laboratory system. The scattering cosine still only tells us the cosine of the angle between the original direction of the particle and the new direction of the particle. If we express the pre-collision direction of the particle as \(\mathbf{\Omega} = (u,v,w)\) and the post-collision direction of the particle as \(\mathbf{\Omega}' = (u',v',w')\), it is possible to relate the pre- and post-collision components. We first need to uniformly sample an azimuthal angle \(\phi\) in \([0, 2\pi)\). After the azimuthal angle has been sampled, the post-collision direction is calculated as

(54)\[u' = \mu u + \frac{\sqrt{1 - \mu^2} ( uw \cos\phi - v \sin\phi )}{\sqrt{1 - w^2}} \\ v' = \mu v + \frac{\sqrt{1 - \mu^2} ( vw \cos\phi + u \sin\phi )}{\sqrt{1 - w^2}} \\ w' = \mu w - \sqrt{1 - \mu^2} \sqrt{1 - w^2} \cos\phi.\]

Effect of Thermal Motion on Cross Sections

Since all the multi-group data should be generated with thermal scattering treatments already, this section does not apply to the multi-group mode.

When a neutron scatters off of a nucleus, it may often be assumed that the target nucleus is at rest. However, the target nucleus will have motion associated with its thermal vibration, even at absolute zero (This is due to the zero-point energy arising from quantum mechanical considerations). Thus, the velocity of the neutron relative to the target nucleus is in general not the same as the velocity of the neutron entering the collision.

The effect of the thermal motion on the interaction probability can be written as

(55)\[v_n \bar{\sigma} (v_n, T) = \int d\mathbf{v}_T v_r \sigma(v_r) M (\mathbf{v}_T)\]

where \(v_n\) is the magnitude of the velocity of the neutron, \(\bar{\sigma}\) is an effective cross section, \(T\) is the temperature of the target material, \(\mathbf{v}_T\) is the velocity of the target nucleus, \(v_r = || \mathbf{v}_n - \mathbf{v}_T ||\) is the magnitude of the relative velocity, \(\sigma\) is the cross section at 0 K, and \(M (\mathbf{v}_T)\) is the probability distribution for the target nucleus velocity at temperature \(T\) (a Maxwellian). In a Monte Carlo code, one must account for the effect of the thermal motion on both the integrated cross section as well as secondary angle and energy distributions. For integrated cross sections, it is possible to calculate thermally-averaged cross sections by applying a kernel Doppler broadening algorithm to data at 0 K (or some temperature lower than the desired temperature). The most ubiquitous algorithm for this purpose is the SIGMA1 method developed by Red Cullen and subsequently refined by others. This method is used in the NJOY and PREPRO data processing codes.

The effect of thermal motion on secondary angle and energy distributions can be accounted for on-the-fly in a Monte Carlo simulation. We must first qualify where it is actually used however. All threshold reactions are treated as being independent of temperature, and therefore they are not Doppler broadened in NJOY and no special procedure is used to adjust the secondary angle and energy distributions. The only non-threshold reactions with secondary neutrons are elastic scattering and fission. For fission, it is assumed that the neutrons are emitted isotropically (this is not strictly true, but is nevertheless a good approximation). This leaves only elastic scattering that needs a special thermal treatment for secondary distributions.

Fortunately, it is possible to directly sample the velocity of the target nuclide and then use it directly in the kinematic calculations. However, this calculation is a bit more nuanced than it might seem at first glance. One might be tempted to simply sample a Maxwellian distribution for the velocity of the target nuclide. Careful inspection of equation (55) however tells us that target velocities that produce relative velocities which correspond to high cross sections will have a greater contribution to the effective reaction rate. This is most important when the velocity of the incoming neutron is close to a resonance. For example, if the neutron’s velocity corresponds to a trough in a resonance elastic scattering cross section, a very small target velocity can cause the relative velocity to correspond to the peak of the resonance, thus making a disproportionate contribution to the reaction rate. The conclusion is that if we are to sample a target velocity in the Monte Carlo code, it must be done in such a way that preserves the thermally-averaged reaction rate as per equation (55).

The method by which most Monte Carlo codes sample the target velocity for use in elastic scattering kinematics is outlined in detail by [Gelbard]. The derivation here largely follows that of Gelbard. Let us first write the reaction rate as a function of the velocity of the target nucleus:

(56)\[R(\mathbf{v}_T) = || \mathbf{v}_n - \mathbf{v}_T || \sigma ( || \mathbf{v}_n - \mathbf{v}_T || ) M ( \mathbf{v}_T )\]

where \(R\) is the reaction rate. Note that this is just the right-hand side of equation (55). Based on the discussion above, we want to construct a probability distribution function for sampling the target velocity to preserve the reaction rate – this is different from the overall probability distribution function for the target velocity, \(M ( \mathbf{v}_T )\). This probability distribution function can be found by integrating equation (56) to obtain a normalization factor:

(57)\[p( \mathbf{v}_T ) d\mathbf{v}_T = \frac{R(\mathbf{v}_T) d\mathbf{v}_T}{\int d\mathbf{v}_T \, R(\mathbf{v}_T)}\]

Let us call the normalization factor in the denominator of equation (57) \(C\).

Constant Cross Section Model

It is often assumed that \(\sigma (v_r)\) is constant over the range of relative velocities of interest. This is a good assumption for almost all cases since the elastic scattering cross section varies slowly with velocity for light nuclei, and for heavy nuclei where large variations can occur due to resonance scattering, the moderating effect is rather small. Nonetheless, this assumption may cause incorrect answers in systems with low-lying resonances that can cause a significant amount of up-scatter that would be ignored by this assumption (e.g. U-238 in commercial light-water reactors). We will revisit this assumption later in Energy-Dependent Cross Section Model. For now, continuing with the assumption, we write \(\sigma (v_r) = \sigma_s\) which simplifies (57) to

(58)\[p( \mathbf{v}_T ) d\mathbf{v}_T = \frac{\sigma_s}{C} || \mathbf{v}_n - \mathbf{v}_T || M ( \mathbf{v}_T ) d\mathbf{v}_T\]

The Maxwellian distribution in velocity is

(59)\[M (\mathbf{v}_T) = \left ( \frac{m}{2\pi kT} \right )^{3/2} \exp \left ( \frac{-m || \mathbf{v}_T^2 ||}{2kT} \right )\]

where \(m\) is the mass of the target nucleus and \(k\) is Boltzmann’s constant. Notice here that the term in the exponential is dependent only on the speed of the target, not on the actual direction. Thus, we can change the Maxwellian into a distribution for speed rather than velocity. The differential element of velocity is

(60)\[d\mathbf{v}_T = v_T^2 dv_T d\mu d\phi\]

Let us define the Maxwellian distribution in speed as

(61)\[M (v_T) dv_T = \int_{-1}^1 d\mu \int_{0}^{2\pi} d\phi \, dv_T \, v_T^2 M(\mathbf{v}_T) = \sqrt{ \frac{2}{\pi} \left ( \frac{m}{kT} \right )^3} v_T^2 \exp \left ( \frac{-m v_T}{2kT} \right ) dv_T.\]

To simplify things a bit, we’ll define a parameter

(62)\[\beta = \sqrt{\frac{m}{2kT}}.\]

Substituting equation (62) into equation (61), we obtain

(63)\[M (v_T) dv_T = \frac{4}{\sqrt{\pi}} \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ) dv_T.\]

Now, changing variables in equation (58) by using the result from equation (61), our new probability distribution function is

(64)\[p( v_T, \mu ) dv_T d\mu = \frac{4\sigma_s}{\sqrt{\pi}C'} || \mathbf{v}_n - \mathbf{v}_T || \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ) dv_T d\mu\]

Again, the Maxwellian distribution for the speed of the target nucleus has no dependence on the angle between the neutron and target velocity vectors. Thus, only the term \(|| \mathbf{v}_n - \mathbf{v}_T ||\) imposes any constraint on the allowed angle. Our last task is to take that term and write it in terms of magnitudes of the velocity vectors and the angle rather than the vectors themselves. We can establish this relation based on the law of cosines which tells us that

(65)\[2 v_n v_T \mu = v_n^2 + v_T^2 - v_r^2.\]

Thus, we can infer that

(66)\[|| \mathbf{v}_n - \mathbf{v}_T || = || \mathbf{v}_r || = v_r = \sqrt{v_n^2 + v_T^2 - 2v_n v_T \mu}.\]

Inserting equation (66) into (64), we obtain

(67)\[p( v_T, \mu ) dv_T d\mu = \frac{4\sigma_s}{\sqrt{\pi}C'} \sqrt{v_n^2 + v_T^2 - 2v_n v_T \mu} \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ) dv_T d\mu\]

This expression is still quite formidable and does not lend itself to any natural sampling scheme. We can divide this probability distribution into two parts as such:

(68)\[\begin{aligned} p(v_T, \mu) &= f_1(v_T, \mu) f_2(v_T) \\ f_1(v_T, \mu) &= \frac{4\sigma_s}{\sqrt{\pi} C'} \frac{ \sqrt{v_n^2 + v_T^2 - 2v_n v_T \mu}}{v_n + v_T} \\ f_2(v_T) &= (v_n + v_T) \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ). \end{aligned}\]

In general, any probability distribution function of the form \(p(x) = f_1(x) f_2(x)\) with \(f_1(x)\) bounded can be sampled by sampling \(x'\) from the distribution

(69)\[q(x) dx = \frac{f_2(x) dx}{\int f_2(x) dx}\]

and accepting it with probability

(70)\[p_{accept} = \frac{f_1(x')}{\max f_1(x)}\]

The reason for dividing and multiplying the terms by \(v_n + v_T\) is to ensure that the first term is bounded. In general, \(|| \mathbf{v}_n - \mathbf{v}_T ||\) can take on arbitrarily large values, but if we divide it by its maximum value \(v_n + v_T\), then it ensures that the function will be bounded. We now must come up with a sampling scheme for equation (69). To determine \(q(v_T)\), we need to integrate \(f_2\) in equation (68). Doing so we find that

(71)\[\int_0^{\infty} dv_T (v_n + v_T) \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ) = \frac{1}{4\beta} \left ( \sqrt{\pi} \beta v_n + 2 \right ).\]

Thus, we need to sample the probability distribution function

(72)\[q(v_T) dv_T = \left ( \frac{4\beta^2 v_n v_T^2}{\sqrt{\pi} \beta v_n + 2} + \frac{4\beta^4 v_T^3}{\sqrt{\pi} \beta v_n + 2} \right ) exp \left ( -\beta^2 v_T^2 \right ).\]

Now, let us do a change of variables with the following definitions

(73)\[x = \beta v_T \\ y = \beta v_n.\]

Substituting equation (73) into equation (72) along with \(dx = \beta dv_T\) and doing some crafty rearranging of terms yields

(74)\[q(x) dx = \left [ \left ( \frac{\sqrt{\pi} y}{\sqrt{\pi} y + 2} \right ) \frac{4}{\sqrt{\pi}} x^2 e^{-x^2} + \left ( \frac{2}{\sqrt{\pi} y + 2} \right ) 2x^3 e^{-x^2} \right ] dx.\]

It’s important to make note of the following two facts. First, the terms outside the parentheses are properly normalized probability distribution functions that can be sampled directly. Secondly, the terms inside the parentheses are always less than unity. Thus, the sampling scheme for \(q(x)\) is as follows. We sample a random number \(\xi_1\) on the interval \([0,1)\) and if

(75)\[\xi_1 < \frac{2}{\sqrt{\pi} y + 2}\]

then we sample the probability distribution \(2x^3 e^{-x^2}\) for \(x\) using rule C49 in the Monte Carlo Sampler which we can then use to determine the speed of the target nucleus \(v_T\) from equation (73). Otherwise, we sample the probability distribution \(\frac{4}{\sqrt{\pi}} x^2 e^{-x^2}\) for \(x\) using rule C61 in the Monte Carlo Sampler.

With a target speed sampled, we must then decide whether to accept it based on the probability in equation (70). The cosine can be sampled isotropically as \(\mu = 2\xi_2 - 1\) where \(\xi_2\) is a random number on the unit interval. Since the maximum value of \(f_1(v_T, \mu)\) is \(4\sigma_s / \sqrt{\pi} C'\), we then sample another random number \(\xi_3\) and accept the sampled target speed and cosine if

(76)\[\xi_3 < \frac{\sqrt{v_n^2 + v_T^2 - 2 v_n v_T \mu}}{v_n + v_T}.\]

If is not accepted, then we repeat the process and resample a target speed and cosine until a combination is found that satisfies equation (76).

Energy-Dependent Cross Section Model

As was noted earlier, assuming that the elastic scattering cross section is constant in (56) is not strictly correct, especially when low-lying resonances are present in the cross sections for heavy nuclides. To correctly account for energy dependence of the scattering cross section entails performing another rejection step. The most common method is to sample \(\mu\) and \(v_T\) as in the constant cross section approximation and then perform a rejection on the ratio of the 0 K elastic scattering cross section at the relative velocity to the maximum 0 K elastic scattering cross section over the range of velocities considered:

(77)\[p_{dbrc} = \frac{\sigma_s(v_r)}{\sigma_{s,max}}\]

where it should be noted that the maximum is taken over the range \([v_n - 4/\beta, 4_n + 4\beta]\). This method is known as Doppler broadening rejection correction (DBRC) and was first introduced by Becker et al.. OpenMC has an implementation of DBRC as well as an accelerated sampling method that samples the relative velocity directly.

S(\(\alpha,\beta,T\)) Tables

Note that S(\(\alpha,\beta,T\)) tables are only applicable to continuous-energy transport.

For neutrons with thermal energies, generally less than 4 eV, the kinematics of scattering can be affected by chemical binding and crystalline effects of the target molecule. If these effects are not accounted for in a simulation, the reported results may be highly inaccurate. There is no general analytic treatment for the scattering kinematics at low energies, and thus when nuclear data is processed for use in a Monte Carlo code, special tables are created that give cross sections and secondary angle/energy distributions for thermal scattering that account for thermal binding effects. These tables are mainly used for moderating materials such as light or heavy water, graphite, hydrogen in ZrH, beryllium, etc.

The theory behind S(\(\alpha,\beta,T\)) is rooted in quantum mechanics and is quite complex. Those interested in first principles derivations for formulae relating to S(\(\alpha,\beta,T\)) tables should be referred to the excellent books by [Williams] and [Squires]. For our purposes here, we will focus only on the use of already processed data as it appears in the ACE format.

Each S(\(\alpha,\beta,T\)) table can contain the following:

  • Thermal inelastic scattering cross section;
  • Thermal elastic scattering cross section;
  • Correlated energy-angle distributions for thermal inelastic and elastic scattering.

Note that when we refer to “inelastic” and “elastic” scattering now, we are actually using these terms with respect to the scattering system. Thermal inelastic scattering means that the scattering system is left in an excited state; no particular nucleus is left in an excited state as would be the case for inelastic level scattering. In a crystalline material, the excitation of the scattering could correspond to the production of phonons. In a molecule, it could correspond to the excitation of rotational or vibrational modes.

Both thermal elastic and thermal inelastic scattering are generally divided into incoherent and coherent parts. Coherent elastic scattering refers to scattering in crystalline solids like graphite or beryllium. These cross sections are characterized by the presence of Bragg edges that relate to the crystal structure of the scattering material. Incoherent elastic scattering refers to scattering in hydrogenous solids such as polyethylene. As it occurs in ACE data, thermal inelastic scattering includes both coherent and incoherent effects and is dominant for most other materials including hydrogen in water.

Calculating Integrated Cross Sections

The first aspect of using S(\(\alpha,\beta,T\)) tables is calculating cross sections to replace the data that would normally appear on the incident neutron data, which do not account for thermal binding effects. For incoherent inelastic scattering, the cross section is stored as a linearly interpolable function on a specified energy grid. For coherent elastic data, the cross section can be expressed as

(78)\[\sigma(E) = \frac{1}{E} \sum_{E_i < E} s_i\]

where \(E_i\) are the energies of the Bragg edges and \(s_i\) are related to crystallographic structure factors. Since the functional form of the cross section is just 1/E and the proportionality constant changes only at Bragg edges, the proportionality constants are stored and then the cross section can be calculated analytically based on equation (78). For incoherent elastic data, the cross section can be expressed as

(79)\[\sigma(E) = \frac{\sigma_b}{2} \left( \frac{1 - e^{-4EW'}}{2EW'} \right)\]

where \(\sigma_b\) is the characteristic bound cross section and \(W'\) is the Debye-Waller integral divided by the atomic mass.

Outgoing Angle for Coherent Elastic Scattering

Another aspect of using S(\(\alpha,\beta,T\)) tables is determining the outgoing energy and angle of the neutron after scattering. For incoherent and coherent elastic scattering, the energy of the neutron does not actually change, but the angle does change. For coherent elastic scattering, the angle will depend on which Bragg edge scattered the neutron. The probability that edge \(i\) will scatter then neutron is given by

(80)\[\frac{s_i}{\sum_j s_j}.\]

After a Bragg edge has been sampled, the cosine of the angle of scattering is given analytically by

(81)\[\mu = 1 - \frac{E_i}{E}\]

where \(E_i\) is the energy of the Bragg edge that scattered the neutron.

Outgoing Angle for Incoherent Elastic Scattering

For incoherent elastic scattering, OpenMC has two methods for calculating the cosine of the angle of scattering. The first method uses the Debye-Waller integral, \(W'\), and the characteristic bound cross section as given directly in an ENDF-6 formatted file. In this case, the cosine of the angle of scattering can be sampled by inverting equation 7.4 from the ENDF-6 Format Manual:

(82)\[\mu = \frac{1}{c} \log \left( 1 + \xi \left( e^{2c} - 1 \right) \right) - 1\]

where \(\xi\) is a random number sampled on unit interval and \(c = 2EW'\). In the second method, the probability distribution for the cosine of the angle of scattering is represented as a series of equally-likely discrete cosines \(\mu_{i,j}\) for each incoming energy \(E_i\) on the thermal elastic energy grid. First the outgoing angle bin \(j\) is sampled. Then, if the incoming energy of the neutron satisfies \(E_i < E < E_{i+1}\) the cosine of the angle of scattering is

(83)\[\mu' = \mu_{i,j} + f (\mu_{i+1,j} - \mu_{i,j})\]

where the interpolation factor is defined as

(84)\[f = \frac{E - E_i}{E_{i+1} - E_i}.\]

To better represent the true, continuous nature of the cosine distribution, the sampled value of \(mu'\) is then “smeared” based on the neighboring values. First, values of \(\mu\) are calculated for outgoing angle bins \(j-1\) and \(j+1\):

(85)\[\mu_\text{left} = \mu_{i,j-1} + f (\mu_{i+1,j-1} - \mu_{i,j-1}) \\ \mu_\text{right} = \mu_{i,j+1} + f (\mu_{i+1,j+1} - \mu_{i,j+1}).\]

Then, a final cosine is calculated as:

(86)\[\mu = \mu' + \min (\mu - \mu_\text{left}, \mu + \mu_\text{right} ) \cdot \left( \xi - \frac{1}{2} \right)\]

where \(\xi\) is again a random number sampled on the unit interval. Care must be taken to ensure that \(\mu\) does not fall outside the interval \([-1,1]\).

Outgoing Energy and Angle for Inelastic Scattering

Each S(\(\alpha,\beta,T\)) table provides a correlated angle-energy secondary distribution for neutron thermal inelastic scattering. There are three representations used in the ACE thermal scattering data: equiprobable discrete outgoing energies, non-uniform yet still discrete outgoing energies, and continuous outgoing energies with corresponding probability and cumulative distribution functions provided in tabular format. These three representations all represent the angular distribution in a common format, using a series of discrete equiprobable outgoing cosines.

Equi-Probable Outgoing Energies

If the thermal data was processed with \(iwt = 1\) in NJOY, then the outgoing energy spectra is represented in the ACE data as a set of discrete and equiprobable outgoing energies. The procedure to determine the outgoing energy and angle is as such. First, the interpolation factor is determined from equation (84). Then, an outgoing energy bin is sampled from a uniform distribution and then interpolated between values corresponding to neighboring incoming energies:

(87)\[E = E_{i,j} + f (E_{i+1,j} - E_{i,j})\]

where \(E_{i,j}\) is the j-th outgoing energy corresponding to the i-th incoming energy. For each combination of incoming and outgoing energies, there is a series equiprobable outgoing cosines. An outgoing cosine bin is sampled uniformly and then the final cosine is interpolated on the incoming energy grid:

(88)\[\mu = \mu_{i,j,k} + f (\mu_{i+1,j,k} - \mu_{i,j,k})\]

where \(\mu_{i,j,k}\) is the k-th outgoing cosine corresponding to the j-th outgoing energy and the i-th incoming energy.

Skewed Equi-Probable Outgoing Energies

If the thermal data was processed with \(iwt=0\) in NJOY, then the outgoing energy spectra is represented in the ACE data according to the following: the first and last outgoing energies have a relative probability of 1, the second and second-to-last energies have a relative probability of 4, and all other energies have a relative probability of 10. The procedure to determine the outgoing energy and angle is similar to the method discussed above, except that the sampled probability distribution is now skewed accordingly.

Continuous Outgoing Energies

If the thermal data was processed with \(iwt=2\) in NJOY, then the outgoing energy spectra is represented by a continuous outgoing energy spectra in tabular form with linear-linear interpolation. The sampling of the outgoing energy portion of this format is very similar to Correlated Energy and Angle Distribution, but the sampling of the correlated angle is performed as it was in the other two representations discussed in this sub-section. In the Law 61 algorithm, we found an interpolation factor \(f\), statistically sampled an incoming energy bin \(\ell\), and sampled an outgoing energy bin \(j\) based on the tabulated cumulative distribution function. Once the outgoing energy has been determined with equation (34), we then need to decide which angular distribution data to use. Like the linear-linear interpolation case in Law 61, the angular distribution closest to the sampled value of the cumulative distribution function for the outgoing energy is utilized. The actual algorithm utilized to sample the outgoing angle is shown in equation (88). As in the case of incoherent elastic scattering with discrete cosine bins, the sampled cosine is smeared over neighboring angle bins to better approximate a continuous distribution.

Unresolved Resonance Region Probability Tables

Note that unresolved resonance treatments are only applicable to continuous-energy transport.

In the unresolved resonance energy range, resonances may be so closely spaced that it is not possible for experimental measurements to resolve all resonances. To properly account for self-shielding in this energy range, OpenMC uses the probability table method. For most thermal reactors, the use of probability tables will not significantly affect problem results. However, for some fast reactors and other problems with an appreciable flux spectrum in the unresolved resonance range, not using probability tables may lead to incorrect results.

Probability tables in the ACE format are generated from the UNRESR module in NJOY following the method of Levitt. A similar method employed for the RACER and MC21 Monte Carlo codes is described in a paper by Sutton and Brown. For the discussion here, we will focus only on use of the probability table table as it appears in the ACE format.

Each probability table for a nuclide contains the following information at a number of incoming energies within the unresolved resonance range:

  • Cumulative probabilities for cross section bands;
  • Total cross section (or factor) in each band;
  • Elastic scattering cross section (or factor) in each band;
  • Fission cross section (or factor) in each band;
  • \((n,\gamma)\) cross section (or factor) in each band; and
  • Neutron heating number (or factor) in each band.

It should be noted that unresolved resonance probability tables affect only integrated cross sections and no extra data need be given for secondary angle/energy distributions. Secondary distributions for elastic and inelastic scattering would be specified whether or not probability tables were present.

The procedure for determining cross sections in the unresolved range using probability tables is as follows. First, the bounding incoming energies are determined, i.e. find \(i\) such that \(E_i < E < E_{i+1}\). We then sample a cross section band \(j\) using the cumulative probabilities for table \(i\). This allows us to then calculate the elastic, fission, and capture cross sections from the probability tables interpolating between neighboring incoming energies. If interpolation is specified, then the cross sections are calculated as

(89)\[\sigma = \sigma_{i,j} + f (\sigma_{i+1,j} - \sigma{i,j})\]

where \(\sigma_{i,j}\) is the j-th band cross section corresponding to the i-th incoming neutron energy and \(f\) is the interpolation factor defined in the same manner as (84). If logarithmic interpolation is specified, the cross sections are calculated as

(90)\[\sigma = \exp \left ( \log \sigma_{i,j} + f \log \frac{\sigma_{i+1,j}}{\sigma_{i,j}} \right )\]

where the interpolation factor is now defined as

(91)\[f = \frac{\log \frac{E}{E_i}}{\log \frac{E_{i+1}}{E_i}}.\]

A flag is also present in the probability table that specifies whether an inelastic cross section should be calculated. If so, this is done from a normal reaction cross section (either MT=51 or a special MT). Finally, if the cross sections defined are above are specified to be factors and not true cross sections, they are multiplied by the underlying smooth cross section in the unresolved range to get the actual cross sections. Lastly, the total cross section is calculated as the sum of the elastic, fission, capture, and inelastic cross sections.

Variance Reduction Techniques

Survival Biasing

In problems with highly absorbing materials, a large fraction of neutrons may be killed through absorption reactions, thus leading to tallies with very few scoring events. To remedy this situation, an algorithm known as survival biasing or implicit absorption (or sometimes implicit capture, even though this is a misnomer) is commonly used.

In survival biasing, absorption reactions are prohibited from occurring and instead, at every collision, the weight of neutron is reduced by probability of absorption occurring, i.e.

(92)\[w' = w \left ( 1 - \frac{\sigma_a (E)}{\sigma_t (E)} \right )\]

where \(w'\) is the weight of the neutron after adjustment and \(w\) is the weight of the neutron before adjustment. A few other things need to be handled differently if survival biasing is turned on. Although fission reactions never actually occur with survival biasing, we still need to create fission sites to serve as source sites for the next generation in the method of successive generations. The algorithm for sampling fission sites is the same as that described in Fission. The only difference is in equation (14). We now need to produce

(93)\[\nu = \frac{w}{k} \frac{\nu_t \sigma_f(E)}{\sigma_t (E)}\]

fission sites, where \(w\) is the weight of the neutron before being adjusted. One should note this is just the expected number of neutrons produced per collision rather than the expected number of neutrons produced given that fission has already occurred.

Additionally, since survival biasing can reduce the weight of the neutron to very low values, it is always used in conjunction with a weight cutoff and Russian rouletting. Two user adjustable parameters \(w_c\) and \(w_s\) are given which are the weight below which neutrons should undergo Russian roulette and the weight should they survive Russian roulette. The algorithm for Russian rouletting is as follows. After a collision if \(w < w_c\), then the neutron is killed with probability \(1 - w/w_s\). If it survives, the weight is set equal to \(w_s\). One can confirm that the average weight following Russian roulette is simply \(w\), so the game can be considered “fair”. By default, the cutoff weight in OpenMC is \(w_c = 0.25\) and the survival weight is \(w_s = 1.0\). These parameters vary from one Monte Carlo code to another.

References

[Gelbard]Ely M. Gelbard, “Epithermal Scattering in VIM,” FRA-TM-123, Argonne National Laboratory (1979).
[Squires]G. L. Squires, Introduction to the Theory of Thermal Neutron Scattering, Cambridge University Press (1978).
[Williams]M. M. R. Williams, The Slowing Down and Thermalization of Neutrons, North-Holland Publishing Co., Amsterdam (1966). Note: This book can be obtained for free from the OECD.

Photon Physics

Photons, being neutral particles, behave much in the same manner as neutrons, traveling in straight lines and experiencing occasional collisions that change their energy and direction. Photons undergo four basic interactions as they pass through matter: coherent (Rayleigh) scattering, incoherent (Compton) scattering, photoelectric effect, and pair/triplet production. Photons with energy in the MeV range may also undergo photonuclear reactions with an atomic nucleus. In addition to these primary interaction mechanisms, all processes other than coherent scattering can result in the excitation/ionization of atoms. The de-excitation of these atoms can result in the emission of electrons and photons. Electrons themselves also can produce photons by means of bremsstrahlung radiation.

Photon Interactions

Coherent (Rayleigh) Scattering

The elastic scattering of a photon off a free charged particle is known as Thomson scattering. The differential cross section is independent of the energy of the incident photon. For scattering off a free electron, the differential cross section is

(1)\[\frac{d\sigma}{d\mu} = \pi r_e^2 ( 1 + \mu^2 )\]

where \(\mu\) is the cosine of the scattering angle and \(r_e\) is the classical electron radius. Thomson scattering can generally occur when the photon energy is much less than the rest mass energy of the particle.

In practice, most elastic scattering of photons off electrons happens not with free electrons but those bound in atoms. This process is known as Rayleigh scattering. The radiation scattered off of individual bound electrons combines coherently, and thus Rayleigh scattering is also known as coherent scattering. Even though conceptually we think of the photon interacting with a single electron, because the wave functions combine constructively it is really as though the photon is interacting with the entire atom.

The differential cross section for Rayleigh scattering is given by

(2)\[\begin{aligned} \frac{d\sigma(E,E',\mu)}{d\mu} &= \pi r_e^2 ( 1 + \mu^2 )~\left| F(x,Z) + F' + iF'' \right|^2 \\ &= \pi r_e^2 ( 1 + \mu^2 ) \left [ ( F(x,Z) + F'(E) )^2 + F''(E)^2 \right ] \end{aligned}\]

where \(F(x,Z)\) is a form factor as a function of the momentum transfer \(x\) and the atomic number \(Z\) and the term \(F' + iF''\) accounts for anomalous scattering which can occur near absorption edges. In a Monte Carlo simulation, when coherent scattering occurs, we only need to sample the scattering angle using the differential cross section in (2) since the energy of the photon does not change. In OpenMC, anomalous scattering is ignored such that the differential cross section becomes

(3)\[\frac{d\sigma(E,E',\mu)}{d\mu} = \pi r_e^2 ( 1 + \mu^2 ) F(x, Z)^2\]

To construct a proper probability density, we need to normalize the differential cross section in (3) by the integrated coherent scattering cross section:

(4)\[p(\mu) d\mu = \frac{\pi r_e^2}{\sigma(E)} ( 1 + \mu^2 ) F(x, Z)^2 d\mu.\]

Since the form factor is given in terms of the momentum transfer, it is more convenient to change variables of the probability density to \(x^2\). The momentum transfer is traditionally expressed as

(5)\[x = a k \sqrt{1 - \mu}\]

where \(k\) is the ratio of the photon energy to the electron rest mass, and the coefficient \(a\) can be shown to be

(6)\[a = \frac{m_e c^2}{\sqrt{2}hc} \approx 2.914329\times10^{-9}~\text{m}\]

where \(m_e\) is the mass of the electron, \(c\) is the speed of light in a vacuum, and \(h\) is Planck’s constant. Using (5), we have \(\mu = 1 - [x/(ak)]^2\) and \(d\mu/dx^2 = -1/(ak)^2\). The probability density in \(x^2\) is

(7)\[p(x^2) dx^2 = p(\mu) \left | \frac{d\mu}{dx^2} \right | dx^2 = \frac{2\pi r_e^2 A(\bar{x}^2,Z)}{(ak)^2 \sigma(E)} \left ( \frac{1 + \mu^2}{2} \right ) \left ( \frac{F(x, Z)^2}{A(\bar{x}^2, Z)} \right ) dx^2\]

where \(\bar{x}\) is the maximum value of \(x\) that occurs for \(\mu=-1\),

(8)\[\bar{x} = a k \sqrt{2} = \frac{m_e c^2}{hc} k,\]

and \(A(x^2, Z)\) is the integral of the square of the form factor:

(9)\[A(x^2, Z) = \int_0^{x^2} F(x,Z)^2 dx^2.\]

As you see, we have multiplied and divided the probability density by the integral of the squared form factor so that the density in (7) is expressed as the product of two separate densities in parentheses. In OpenMC, a table of \(A(x^2, Z)\) versus \(x^2\) is pre-generated and used at run-time to do a table search on the cumulative distribution function:

(10)\[\frac{\int_0^{x^2} F(x,Z)^2 dx^2}{\int_0^{\bar{x}^2} F(x,Z)^2 dx^2}\]

Once a trial \(x^2\) value has been selected, we can calculate \(\mu\) and perform rejection sampling using the Thomson scattering differential cross section. The complete algorithm is as follows:

  1. Determine \(\bar{x}^2\) using (8).
  2. Determine \(A_{max} = A(\bar{x}^2, Z)\) using the pre-generated tabulated data.
  3. Sample the cumulative density by calculating \(A' = \xi_1 A_{max}\) where \(\xi_1\) is a uniformly distributed random number.
  4. Perform a binary search to determine the value of \(x^2\) which satisfies \(A(x^2, Z) = A'\).
  5. By combining (5) and (8), calculate \(\mu = 1 - 2x^2/\bar{x}^2\).
  6. If \(\xi_2 < (1 + \mu^2)/2\), accept \(\mu\). Otherwise, repeat the sampling at step 3.
Incoherent (Compton) Scattering

Before we noted that the Thomson cross section gives the behavior for photons scattering off of free electrons valid at low energies. The formula for photon scattering off of free electrons that is valid for all energies can be found using quantum electrodynamics and is known as the Klein-Nishina formula after the two authors who discovered it:

(11)\[\frac{d\sigma_{KN}}{d\mu} = \pi r_e^2 \left ( \frac{k'}{k} \right)^2 \left [ \frac{k'}{k} + \frac{k}{k'} + \mu^2 - 1 \right ]\]

where \(k\) and \(k'\) are the ratios of the incoming and exiting photon energies to the electron rest mass energy equivalent (0.511 MeV), respectively. Although it appears that the outgoing energy and angle are separate, there is actually a one-to-one relationship between them such that only one needs to be sampled:

(12)\[k' = \frac{k}{1 + k(1 - \mu)}.\]

Note that when \(k'/k\) goes to one, i.e., scattering is elastic, the Klein-Nishina cross section becomes identical to the Thomson cross section. In general though, the scattering is inelastic and is known as Compton scattering. When a photon interacts with a bound electron in an atom, the Klein-Nishina formula must be modified to account for the binding effects. As in the case of coherent scattering, this is done by means of a form factor. The differential cross section for incoherent scattering is given by

(13)\[\frac{d\sigma}{d\mu} = \frac{d\sigma_{KN}}{d\mu} S(x,Z) = \pi r_e^2 \left ( \frac{k'}{k} \right )^2 \left [ \frac{k'}{k} + \frac{k}{k'} + \mu^2 - 1 \right ] S(x,Z)\]

where \(S(x,Z)\) is the form factor. The approach in OpenMC is to first sample the Klein-Nishina cross section and then perform rejection sampling on the form factor. As in other codes, Kahn’s rejection method is used for \(k < 3\) and a direct method by Koblinger is used for \(k \ge 3\). The complete algorithm is as follows:

  1. If \(k < 3\), sample \(\mu\) from the Klein-Nishina cross section using Kahn’s rejection method. Otherwise, use Koblinger’s direct method.
  2. Calculate \(x\) and \(\bar{x}\) using (5) and (8), respectively.
  3. If \(\xi < S(x, Z)/S(\bar{x}, Z)\), accept \(\mu\). Otherwise repeat from step 1.
Doppler Energy Broadening

Bound electrons are not at rest but have a momentum distribution that will cause the energy of the scattered photon to be Doppler broadened. More tightly bound electrons have a wider momentum distribution, so the energy spectrum of photons scattering off inner shell electrons will be broadened the most. In addition, scattering from bound electrons places a limit on the maximum scattered photon energy:

(14)\[E'_{\text{max}} = E - E_{b,i},\]

where \(E_{b,i}\) is the binding energy of the \(i\)-th subshell.

Compton profiles \(J_i(p_z)\) are used to account for the binding effects. The quantity \(p_z = {\bf p} \cdot {\bf q}/q\) is the projection of the initial electron momentum on \({\bf q}\), where the scattering vector \({\bf q} = {\bf p} - {\bf p'}\) is the momentum gained by the photon, \({\bf p}\) is the initial momentum of the electron, and \({\bf p'}\) is the momentum of the scattered electron. Applying the conservation of energy and momentum, \(p_z\) can be written in terms of the photon energy and scattering angle:

(15)\[p_z = \frac{E - E' - EE'(1 - \mu)/(m_e c^2)}{-\alpha \sqrt{E^2 + E'^2 - 2EE'\mu}},\]

where \(\alpha\) is the fine structure constant. The maximum momentum transferred, \(p_{z,\text{max}}\), can be calculated from (15) using \(E' = E'_{\text{max}}\). The Compton profile of the \(i\)-th electron subshell is defined as

(16)\[J_i(p_z) = \int \int \rho_i({\bf p}) dp_x dp_y,\]

where \(\rho_i({\bf p})\) is the initial electron momentum distribution. \(J_i(p_z)\) can be interpreted as the probability density function of \(p_z\).

The Doppler broadened energy of the Compton-scattered photon can be sampled by selecting an electron shell, sampling a value of \(p_z\) using the Compton profile, and calculating the scattered photon energy. The theory and methods used to do this are described in detail in LA-UR-04-0487 and LA-UR-04-0488. The sampling algorithm is summarized below:

  1. Sample \(\mu\) from (13) using the algorithm described in Incoherent (Compton) Scattering.
  2. Sample the electron subshell \(i\) using the number of electrons per shell as the probability mass function.
  3. Sample \(p_z\) using \(J_i(p_z)\) as the PDF.
  4. Calculate \(E'\) by solving (15) for \(E'\) using the sampled value of \(p_z\).
  5. If \(p_z < p_{z,\text{max}}\) for shell \(i\), accept \(E'\). Otherwise repeat from step 2.
Compton Electrons

Because the Compton-scattered photons can transfer a large fraction of their energy to the kinetic energy of the recoil electron, which may in turn go on to lose its energy as bremsstrahlung radiation, it is necessary to accurately model the angular and energy distributions of Compton electrons. The energy of the recoil electron ejected from the \(i\)-th subshell is given by

(17)\[E_{-} = E - E' - E_{b,i}.\]

The direction of the electron is assumed to be in the direction of the momentum transfer, with the cosine of the polar angle given by

(18)\[\mu_{-} = \frac{E - E'\mu}{\sqrt{E^2 +E'^2 - 2EE'\mu}}\]

and the azimuthal angle \(\phi_{-} = \phi + \pi\), where \(\phi\) is the azimuthal angle of the photon. The vacancy left by the ejected electron is filled through atomic relaxation.

Photoelectric Effect

In the photoelectric effect, the incident photon is absorbed by an atomic electron, which is then emitted from the \(i\)-th shell with kinetic energy

(19)\[E_{-} = E - E_{b,i}.\]

Photoelectric emission is only possible when the photon energy exceeds the binding energy of the shell. These binding energies are often referred to as edge energies because the otherwise continuously decreasing cross section has discontinuities at these points, creating the characteristic sawtooth shape. The photoelectric effect dominates at low energies and is more important for heavier elements.

When simulating the photoelectric effect, the first step is to sample the electron shell. The shell \(i\) where the ionization occurs can be considered a discrete random variable with probability mass function

(20)\[p_i = \frac{\sigma_{\text{pe},i}}{\sigma_{\text{pe}}},\]

where \(\sigma_{\text{pe},i}\) is the cross section of the \(i\)-th shell, and the total photoelectric cross section of the atom, \(\sigma_{\text{pe}}\), is the sum over the shell cross sections. Once the shell has been sampled, the energy of the photoelectron is calculated using (19).

To determine the direction of the photoelectron, we implement the method described in Kaltiaisenaho, which models the angular distribution of the photoelectrons using the K-shell cross section derived by Sauter (K-shell electrons are the most tightly bound, and they contribute the most to \(\sigma_{\text{pe}}\)). The non-relativistic Sauter distribution for unpolarized photons can be approximated as

(21)\[\frac{d\sigma_{\text{pe}}}{d\mu_{-}} \propto \frac{1 - \mu_{-}^2}{(1 - \beta_{-} \mu_{-})^4},\]

where \(\beta_{-}\) is the ratio of the velocity of the electron to the speed of light,

(22)\[\beta_{-} = \frac{\sqrt{(E_{-}(E_{-} + 2m_e c^2)}}{E_{-} + m_e c^2}.\]

To sample \(\mu_{-}\) from the Sauter distribution, we first express (21) in the form:

(23)\[f(\mu_{-}) = \frac{3}{2} \psi(\mu_{-}) g(\mu_{-}),\]

where

(24)\[\begin{aligned} \psi(\mu_{-}) &= \frac{(1 - \beta_{-}^2)(1 - \mu_{-}^2)}{(1 - \beta_{-}\mu_{-})^2}, \\ g(\mu_{-}) &= \frac{1 - \beta_{-}^2}{2 (1 - \beta_{-}\mu_{-})^2}. \end{aligned}\]

In the interval \([-1, 1]\), \(g(\mu_{-})\) is a normalized PDF and \(\psi(\mu_{-})\) satisfies the condition \(0 < \psi(\mu_{-}) < 1\). The following algorithm can now be used to sample \(\mu_{-}\):

  1. Using the inverse transform method, sample \(\mu_{-}\) from \(g(\mu_{-})\) using the sampling formula

    \[\mu_{-} = \frac{2\xi_1 + \beta_{-} - 1}{2\beta_{-}\xi_1 - \beta_{-} + 1}.\]
  2. If \(\xi_2 \le \psi(\mu_{-})\), accept \(\mu_{-}\). Otherwise, repeat the sampling from step 1.

The azimuthal angle is sampled uniformly on \([0, 2\pi)\).

The atom is left in an excited state with a vacancy in the \(i\)-th shell and decays to its ground state through a cascade of transitions that produce fluorescent photons and Auger electrons.

Pair Production

In electron-positron pair production, a photon is absorbed in the vicinity of an atomic nucleus or an electron and an electron and positron are created. Pair production is the dominant interaction with matter at high photon energies and is more important for high-Z elements. When it takes place in the field of a nucleus, energy is essentially conserved among the incident photon and the resulting charged particles. Therefore, in order for pair production to occur, the photon energy must be greater than the sum of the rest mass energies of the electron and positron, i.e., \(E_{\text{threshold,pp}} = 2 m_e c^2 = 1.022\) MeV.

The photon can also interact in the field of an atomic electron. This process is referred to as “triplet production” because the target electron is ejected from the atom and three charged particles emerge from the interaction. In this case, the recoiling electron also absorbs some energy, so the energy threshold for triplet production is greater than that of pair production from atomic nuclei, with \(E_{\text{threshold,tp}} = 4 m_e c^2 = 2.044\) MeV. The ratio of the triplet production cross section to the pair production cross section is approximately 1/Z, so triplet production becomes increasingly unimportant for high-Z elements. Though it can be significant in lighter elements, the momentum of the recoil electron becomes negligible in the energy regime where pair production dominates. For our purposes, it is a good approximation to treat triplet production as pair production and only simulate the electron-positron pair.

Accurately modeling the creation of electron-positron pair is important because the charged particles can go on to lose much of their energy as bremsstrahlung radiation, and the subsequent annihilation of the positron with an electron produces two additional photons. We sample the energy and direction of the charged particles using a semiempirical model described in Salvat. The Bethe-Heitler differential cross section, given by

(25)\[\frac{d\sigma_{\text{pp}}}{d\epsilon} = \alpha r_e^2 Z^2 \left[ (\epsilon^2 + (1-\epsilon)^2) (\Phi_1 - 4f_C) + \frac{2}{3}\epsilon(1-\epsilon)(\Phi_2 - 4f_C) \right],\]

is used as a starting point, where \(\alpha\) is the fine structure constant, \(f_C\) is the Coulomb correction function, \(\Phi_1\) and \(\Phi_2\) are screening functions, and \(\epsilon = (E_{-} + m_e c^2)/E\) is the electron reduced energy (i.e., the fraction of the photon energy given to the electron). \(\epsilon\) can take values between \(\epsilon_{\text{min}} = k^{-1}\) (when the kinetic energy of the electron is zero) and \(\epsilon_{\text{max}} = 1 - k^{-1}\) (when the kinetic energy of the positron is zero).

The Coulomb correction, given by

(26)\[\begin{aligned} f_C = \alpha^{2}Z^{2} \big[&(1 + \alpha^{2}Z^{2})^{-1} + 0.202059 - 0.03693\alpha^{2}Z^{2} + 0.00835\alpha^{4}Z^{4} \\ &- 0.00201\alpha^{6}Z^{6} + 0.00049\alpha^{8}Z^{8} - 0.00012\alpha^{10}Z^{10} + 0.00003\alpha^{12}Z^{12}\big] \end{aligned}\]

is introduced to correct for the fact that the Bethe-Heitler differential cross section was derived using the Born approximation, which treats the Coulomb interaction as a small perturbation.

The screening functions \(\Phi_1\) and \(\Phi_2\) account for the screening of the Coulomb field of the atomic nucleus by outer electrons. Since they are given by integrals which include the atomic form factor, they must be computed numerically for a realistic form factor. However, by assuming exponential screening and using a simplified form factor, analytical approximations of the screening functions can be derived:

(27)\[\begin{aligned} \Phi_1 &= 2 - 2\ln(1 + b^2) - 4b\arctan(b^{-1}) + 4\ln(Rm_{e}c/\hbar) \\ \Phi_2 &= \frac{4}{3} - 2\ln(1 + b^2) + 2b^2 \left[ 4 - 4b\arctan(b^{-1}) - 3\ln(1 + b^{-2}) \right] + 4\ln(Rm_{e}c/\hbar) \end{aligned}\]

where

(28)\[b = \frac{Rm_{e}c}{2k\epsilon(1 - \epsilon)\hbar}.\]

and \(R\) is the screening radius.

The differential cross section in (25) with the approximations described above will not be accurate at low energies: the lower boundary of \(\epsilon\) will be shifted above \(\epsilon_{\text{min}}\) and the upper boundary of \(\epsilon\) will be shifted below \(\epsilon_{\text{max}}\). To offset this behavior, a correcting factor \(F_0(k, Z)\) is used:

(29)\[\begin{aligned} F_0(k, Z) =~& (0.1774 + 12.10\alpha Z - 11.18\alpha^{2}Z^{2})(2/k)^{1/2} \\ &+ (8.523 + 73.26\alpha Z - 44.41\alpha^{2}Z^{2})(2/k) \\ &- (13.52 + 121.1\alpha Z - 96.41\alpha^{2}Z^{2})(2/k)^{3/2} \\ &+ (8.946 + 62.05\alpha Z - 63.41\alpha^{2}Z^{2})(2/k)^{2}. \end{aligned}\]

To aid sampling, the differential cross section used to sample \(\epsilon\) (minus the normalization constant) can now be expressed in the form

(30)\[\frac{d\sigma_{\text{pp}}}{d\epsilon} = u_1 \frac{\phi_1(\epsilon)}{\phi_1(1/2)} \pi_1(\epsilon) + u_2 \frac{\phi_2(\epsilon)}{\phi_2(1/2)} \pi_2(\epsilon)\]

where

(31)\[\begin{aligned} u_1 &= \frac{2}{3} \left(\frac{1}{2} - \frac{1}{k}\right)^2 \phi_1(1/2), \\ u_2 &= \phi_2(1/2), \end{aligned}\]
(32)\[\begin{aligned} \phi_1(\epsilon) &= \frac{1}{2}(3\Phi_1 - \Phi_2) - 4f_{C}(Z) + F_0(k, Z), \\ \phi_2(\epsilon) &= \frac{1}{4}(3\Phi_1 + \Phi_2) - 4f_{C}(Z) + F_0(k, Z), \end{aligned}\]

and

(33)\[\begin{aligned} \pi_1(\epsilon) &= \frac{3}{2} \left(\frac{1}{2} - \frac{1}{k}\right)^{-3} \left(\frac{1}{2} - \epsilon\right)^2, \\ \pi_2(\epsilon) &= \frac{1}{2} \left(\frac{1}{2} - \frac{1}{k}\right)^{-1}. \end{aligned}\]

The functions in (32) are non-negative and maximum at \(\epsilon = 1/2\). In the interval \((\epsilon_{\text{min}}, \epsilon_{\text{max}})\), the functions in (33) are normalized PDFs and \(\phi_i(\epsilon)/\phi_i(1/2)\) satisfies the condition \(0 < \phi_i(\epsilon)/\phi_i(1/2) < 1\). The following algorithm can now be used to sample the reduced electron energy \(\epsilon\):

  1. Sample \(i\) according to the point probabilities \(p(i=1) = u_1/(u_1 + u_2)\) and \(p(i=2) = u_2/(u_1 + u_2)\).

  2. Using the inverse transform method, sample \(\epsilon\) from \(\pi_i(\epsilon)\) using the sampling formula

    \[\begin{aligned} \epsilon &= \frac{1}{2} + \left(\frac{1}{2} - \frac{1}{k}\right) (2\xi_1 - 1)^{1/3} ~~~~&\text{if}~~ i = 1 \\ \epsilon &= \frac{1}{k} + \left(\frac{1}{2} - \frac{1}{k}\right) 2\xi_1 ~~~~&\text{if}~~ i = 2. \end{aligned}\]
  3. If \(\xi_2 \le \phi_i(\epsilon)/\phi_i(1/2)\), accept \(\epsilon\). Otherwise, repeat the sampling from step 1.

Because charged particles have a much smaller range than the mean free path of photons and because they immediately undergo multiple scattering events which randomize their direction, it is sufficient to use a simplified model to sample the direction of the electron and positron. The cosines of the polar angles are sampled using the leading order term of the Sauter–Gluckstern–Hull distribution,

(34)\[p(\mu_{\pm}) = C(1 - \beta_{\pm}\mu_{\pm})^{-2},\]

where \(C\) is a normalization constant and \(\beta_{\pm}\) is the ratio of the velocity of the charged particle to the speed of light given in (22).

The inverse transform method is used to sample \(\mu_{-}\) and \(\mu_{+}\) from (34), using the sampling formula

(35)\[\mu_{\pm} = \frac{2\xi - 1 + \beta_{\pm}}{(2\xi - 1)\beta_{\pm} + 1}.\]

The azimuthal angles for the electron and positron are sampled independently and uniformly on \([0, 2\pi)\).

Secondary Processes

New photons may be produced in secondary processes related to the main photon interactions discussed above. A Compton-scattered photon transfers a portion of its energy to the kinetic energy of the recoil electron, which in turn may lose the energy as bremsstrahlung radiation. The vacancy left in the shell by the ejected electron is filled through atomic relaxation, creating a shower of electrons and fluorescence photons. Similarly, the vacancy left by the electron emitted in the photoelectric effect is filled through atomic relaxation. Pair production generates an electron and a positron, both of which can emit bremsstrahlung radiation before the positron eventually collides with an electron, resulting in annihilation of the pair and the creation of two additional photons.

Atomic Relaxation

When an electron is ejected from an atom and a vacancy is left in an inner shell, an electron from a higher energy level will fill the vacancy. This results in either a radiative transition, in which a photon with a characteristic energy (fluorescence photon) is emitted, or non-radiative transition, in which an electron from a shell that is farther out (Auger electron) is emitted. If a non-radiative transition occurs, the new vacancy is filled in the same manner, and as the process repeats a shower of photons and electrons can be produced.

The energy of a fluorescence photon is the equal to the energy difference between the transition states, i.e.,

(36)\[E = E_{b,v} - E_{b,i},\]

where \(E_{b,v}\) is the binding energy of the vacancy shell and \(E_{b,i}\) is the binding energy of the shell from which the electron transitioned. The energy of an Auger electron is given by

(37)\[E_{-} = E_{b,v} - E_{b,i} - E_{b,a},\]

where \(E_{b,a}\) is the binding energy of the shell from which the Auger electron is emitted. While Auger electrons are low-energy so their range and bremsstrahlung yield is small, fluorescence photons can travel far before depositing their energy, so the relaxation process should be modeled in detail.

Transition energies and probabilities are needed for each subshell to simulate atomic relaxation. Starting with the initial shell vacancy, the following recursive algorithm is used to fill vacancies and create fluorescence photons and Auger electrons:

  1. If there are no transitions for the vacancy shell, create a fluorescence photon assuming it is from a captured free electron and terminate.
  2. Sample a transition using the transition probabilities for the vacancy shell as the probability mass function.
  3. Create either a fluorescence photon or Auger electron, sampling the direction of the particle isotropically.
  4. If a non-radiative transition occurred, repeat from step 1 for the vacancy left by the emitted Auger electron.
  5. Repeat from step 1 for vacancy left by the transition electron.
Electron-Positron Annihilation

When a positron collides with an electron, both particles are annihilated and generally two photons with equal energy are created. If the kinetic energy of the positron is high enough, the two photons can have different energies, and the higher-energy photon is emitted preferentially in the direction of flight of the positron. It is also possible to produce a single photon if the interaction occurs with a bound electron, and in some cases three (or, rarely, even more) photons can be emitted. However, the annihilation cross section is largest for low-energy positrons, and as the positron energy decreases, the angular distribution of the emitted photons becomes isotropic.

In OpenMC, we assume the most likely case in which a low-energy positron (which has already lost most of its energy to bremsstrahlung radiation) interacts with an electron which is free and at rest. Two photons with energy equal to the electron rest mass energy \(m_e c^2 = 0.511\) MeV are emitted isotropically in opposite directions.

Bremsstrahlung

When a charged particle is decelerated in the field of an atom, some of its kinetic energy is converted into electromagnetic radiation known as bremsstrahlung, or ‘braking radiation’. In each event, an electron or positron with kinetic energy \(T\) generates a photon with an energy \(E\) between \(0\) and \(T\). Bremsstrahlung is described by a cross section that is differential in photon energy, in the direction of the emitted photon, and in the final direction of the charged particle. However, in Monte Carlo simulations it is typical to integrate over the angular variables to obtain a single differential cross section with respect to photon energy, which is often expressed in the form

(38)\[\frac{d\sigma_{\text{br}}}{dE} = \frac{Z^2}{\beta^2} \frac{1}{E} \chi(Z, T, \kappa),\]

where \(\kappa = E/T\) is the reduced photon energy and \(\chi(Z, T, \kappa)\) is the scaled bremsstrahlung cross section, which is experimentally measured.

Because electrons are attracted to atomic nuclei whereas positrons are repulsed, the cross section for positrons is smaller, though it approaches that of electrons in the high energy limit. To obtain the positron cross section, we multiply (38) by the \(\kappa\)-independent factor used in Salvat,

(39)\[\begin{aligned} F_{\text{p}}(Z,T) = & 1 - \text{exp}(-1.2359\times 10^{-1}t + 6.1274\times 10^{-2}t^2 - 3.1516\times 10^{-2}t^3 \\ & + 7.7446\times 10^{-3}t^4 - 1.0595\times 10^{-3}t^5 + 7.0568\times 10^{-5}t^6 \\ & - 1.8080\times 10^{-6}t^7), \end{aligned}\]

where

(40)\[t = \ln\left(1 + \frac{10^6}{Z^2}\frac{T}{\text{m}_\text{e}c^2} \right).\]

\(F_{\text{p}}(Z,T)\) is the ratio of the radiative stopping powers for positrons and electrons. Stopping power describes the average energy loss per unit path length of a charged particle as it passes through matter:

(41)\[-\frac{dT}{ds} = n \int E \frac{d\sigma}{dE} dE \equiv S(T),\]

where \(n\) is the number density of the material and \(d\sigma/dE\) is the cross section differential in energy loss. The total stopping power \(S(T)\) can be separated into two components: the radiative stopping power \(S_{\text{rad}}(T)\), which refers to energy loss due to bremsstrahlung, and the collision stopping power \(S_{\text{col}}(T)\), which refers to the energy loss due to inelastic collisions with bound electrons in the material that result in ionization and excitation. The radiative stopping power for electrons is given by

(42)\[S_{\text{rad}}(T) = n \frac{Z^2}{\beta^2} T \int_0^1 \chi(Z,T,\kappa) d\kappa.\]

To obtain the radiative stopping power for positrons, (42) is multiplied by (39).

While the models for photon interactions with matter described above can safely assume interactions occur with free atoms, sampling the target atom based on the macroscopic cross sections, molecular effects cannot necessarily be disregarded for charged particle treatment. For compounds and mixtures, the bremsstrahlung cross section is calculated using Bragg’s additivity rule as

(43)\[\frac{d\sigma_{\text{br}}}{dE} = \frac{1}{\beta^2 E} \sum_i \gamma_i Z^2_i \chi(Z_i, T, \kappa),\]

where the sum is over the constituent elements and \(\gamma_i\) is the atomic fraction of the \(i\)-th element. Similarly, the radiative stopping power is calculated using Bragg’s additivity rule as

(44)\[S_{\text{rad}}(T) = \sum_i w_i S_{\text{rad},i}(T),\]

where \(w_i\) is the mass fraction of the \(i\)-th element and \(S_{\text{rad},i}(T)\) is found for element \(i\) using (42). The collision stopping power, however, is a function of certain quantities such as the mean excitation energy \(I\) and the density effect correction \(\delta_F\) that depend on molecular properties. These quantities cannot simply be summed over constituent elements in a compound, but should instead be calculated for the material. The Bethe formula can be used to find the collision stopping power of the material:

(45)\[S_{\text{col}}(T) = \frac{2 \pi r_e^2 m_e c^2}{\beta^2} N_A \frac{Z}{A_M} [\ln(T^2/I^2) + \ln(1 + \tau/2) + F(\tau) - \delta_F(T)],\]

where \(N_A\) is Avogadro’s number, \(A_M\) is the molar mass, \(\tau = T/m_e\), and \(F(\tau)\) depends on the particle type. For electrons,

(46)\[F_{-}(\tau) = (1 - \beta^2)[1 + \tau^2/8 - (2\tau + 1) \ln2],\]

while for positrons

(47)\[F_{+}(\tau) = 2\ln2 - (\beta^2/12)[23 + 14/(\tau + 2) + 10/(\tau + 2)^2 + 4/(\tau + 2)^3].\]

The density effect correction \(\delta_F\) takes into account the reduction of the collision stopping power due to the polarization of the material the charged particle is passing through by the electric field of the particle. It can be evaluated using the method described by Sternheimer, where the equation for \(\delta_F\) is

(48)\[\delta_F(\beta) = \sum_{i=1}^n f_i \ln[(l_i^2 + l^2)/l_i^2] - l^2(1-\beta^2).\]

Here, \(f_i\) is the oscillator strength of the \(i\)-th transition, given by \(f_i = n_i/Z\), where \(n_i\) is the number of electrons in the \(i\)-th subshell. The frequency \(l\) is the solution of the equation

(49)\[\frac{1}{\beta^2} - 1 = \sum_{i=1}^{n} \frac{f_i}{\bar{\nu}_i^2 + l^2},\]

where \(\bar{v}_i\) is defined as

(50)\[\bar{\nu}_i = h\nu_i \rho / h\nu_p.\]

The plasma energy \(h\nu_p\) of the medium is given by

(51)\[h\nu_p = \sqrt{\frac{(hc)^2 r_e \rho_m N_A Z}{\pi A}},\]

where \(A\) is the atomic weight and \(\rho_m\) is the density of the material. In (50), \(h\nu_i\) is the oscillator energy, and \(\rho\) is an adjustment factor introduced to give agreement between the experimental values of the oscillator energies and the mean excitation energy. The \(l_i\) in (48) are defined as

(52)\[\begin{aligned} l_i &= (\bar{\nu}_i^2 + 2/3f_i)^{1/2} ~~~~&\text{for}~~ \bar{\nu}_i > 0 \\ l_n &= f_n^{1/2} ~~~~&\text{for}~~ \bar{\nu}_n = 0, \end{aligned}\]

where the second case applies to conduction electrons. For a conductor, \(f_n\) is given by \(n_c/Z\), where \(n_c\) is the effective number of conduction electrons, and \(v_n = 0\). The adjustment factor \(\rho\) is determined using the equation for the mean excitation energy:

(53)\[\ln I = \sum_{i=1}^{n-1} f_i \ln[(h\nu_i\rho)^2 + 2/3f_i(h\nu_p)^2]^{1/2} + f_n \ln (h\nu_pf_n^{1/2}).\]
Thick-Target Bremsstrahlung Approximation

Since charged particles lose their energy on a much shorter distance scale than neutral particles, not much error should be introduced by neglecting to transport electrons. However, the bremsstrahlung emitted from high energy electrons and positrons can travel far from the interaction site. Thus, even without a full electron transport mode it is necessary to model bremsstrahlung. We use a thick-target bremsstrahlung (TTB) approximation based on the models in Salvat and Kaltiaisenaho for generating bremsstrahlung photons, which assumes the charged particle loses all its energy in a single homogeneous material region.

To model bremsstrahlung using the TTB approximation, we need to know the number of photons emitted by the charged particle and the energy distribution of the photons. These quantities can be calculated using the continuous slowing down approximation (CSDA). The CSDA assumes charged particles lose energy continuously along their trajectory with a rate of energy loss equal to the total stopping power, ignoring fluctuations in the energy loss. The approximation is useful for expressing average quantities that describe how charged particles slow down in matter. For example, the CSDA range approximates the average path length a charged particle travels as it slows to rest:

(54)\[R(T) = \int^T_0 \frac{dT'}{S(T')}.\]

Actual path lengths will fluctuate around \(R(T)\). The average number of photons emitted per unit path length is given by the inverse bremsstrahlung mean free path:

(55)\[\lambda_{\text{br}}^{-1}(T,E_{\text{cut}}) = n\int_{E_{\text{cut}}}^T\frac{d\sigma_{\text{br}}}{dE}dE = n\frac{Z^2}{\beta^2}\int_{\kappa_{\text{cut}}}^1\frac{1}{\kappa} \chi(Z,T,\kappa)d\kappa.\]

The lower limit of the integral in (55) is non-zero because the bremsstrahlung differential cross section diverges for small photon energies but is finite for photon energies above some cutoff energy \(E_{\text{cut}}\). The mean free path \(\lambda_{\text{br}}^{-1}(T,E_{\text{cut}})\) is used to calculate the photon number yield, defined as the average number of photons emitted with energy greater than \(E_{\text{cut}}\) as the charged particle slows down from energy \(T\) to \(E_{\text{cut}}\). The photon number yield is given by

(56)\[Y(T,E_{\text{cut}}) = \int^{R(T)}_{R(E_{\text{cut}})} \lambda_{\text{br}}^{-1}(T',E_{\text{cut}})ds = \int_{E_{\text{cut}}}^T \frac{\lambda_{\text{br}}^{-1}(T',E_{\text{cut}})}{S(T')}dT'.\]

\(Y(T,E_{\text{cut}})\) can be used to construct the energy spectrum of bremsstrahlung photons: the number of photons created with energy between \(E_1\) and \(E_2\) by a charged particle with initial kinetic energy \(T\) as it comes to rest is given by \(Y(T,E_1) - Y(T,E_2)\).

To simulate the emission of bremsstrahlung photons, the total stopping power and bremsstrahlung differential cross section for positrons and electrons must be calculated for a given material using (43) and (44). These quantities are used to build the tabulated bremsstrahlung energy PDF and CDF for that material for each incident energy \(T_k\) on the energy grid. The following algorithm is then applied to sample the photon energies:

  1. For an incident charged particle with energy \(T\), sample the number of emitted photons as

    \[N = \lfloor Y(T,E_{\text{cut}}) + \xi_1 \rfloor.\]
  2. Rather than interpolate the PDF between indices \(k\) and \(k+1\) for which \(T_k < T < T_{k+1}\), which is computationally expensive, use the composition method and sample from the PDF at either \(k\) or \(k+1\). Using linear interpolation on a logarithmic scale, the PDF can be expressed as

    \[p_{\text{br}}(T,E) = \pi_k p_{\text{br}}(T_k,E) + \pi_{k+1} p_{\text{br}}(T_{k+1},E),\]

    where the interpolation weights are

    \[\pi_k = \frac{\ln T_{k+1} - \ln T}{\ln T_{k+1} - \ln T_k},~~~ \pi_{k+1} = \frac{\ln T - \ln T_k}{\ln T_{k+1} - \ln T_k}.\]

    Sample either the index \(i = k\) or \(i = k+1\) according to the point probabilities \(\pi_{k}\) and \(\pi_{k+1}\).

  3. Determine the maximum value of the CDF \(P_{\text{br,max}}\).

  1. Sample the photon energies using the inverse transform method with the tabulated CDF \(P_{\text{br}}(T_i, E)\) i.e.,

    \[E = E_j \left[ (1 + a_j) \frac{\xi_2 P_{\text{br,max}} - P_{\text{br}}(T_i, E_j)} {E_j p_{\text{br}}(T_i, E_j)} + 1 \right]^{\frac{1}{1 + a_j}}\]

    where the interpolation factor \(a_j\) is given by

    \[a_j = \frac{\ln p_{\text{br}}(T_i,E_{j+1}) - \ln p_{\text{br}}(T_i,E_j)} {\ln E_{j+1} - \ln E_j}\]

    and \(P_{\text{br}}(T_i, E_j) \le \xi_2 P_{\text{br,max}} \le P_{\text{br}}(T_i, E_{j+1})\).

We ignore the range of the electron or positron, i.e., the bremsstrahlung photons are produced in the same location that the charged particle was created. The direction of the photons is assumed to be the same as the direction of the incident charged particle, which is a reasonable approximation at higher energies when the bremsstrahlung radiation is emitted at small angles.

Photon Production

In coupled neutron-photon transport, a source neutron is tracked, and photons produced from neutron reactions are transported after the neutron’s history has terminated. Since these secondary photons form the photon source for the problem, it is important to correctly describe their energy and angular distributions as the accuracy of the calculation relies on the accuracy of this source. The photon production cross section for a particular reaction \(i\) and incident neutron energy \(E\) is defined as

(57)\[\sigma_{\gamma, i}(E) = y_i(E)\sigma_i(E),\]

where \(y_i(E)\) is the photon yield corresponding to an incident neutron reaction having cross section \(\sigma_i(E)\).

The yield of photons during neutron transport is determined as the sum of the photon yields from each individual reaction. In OpenMC, production of photons is treated in an average sense. That is, the total photon production cross section is used at a collision site to determine how many photons to produce rather than the photon production from the reaction that actually took place. This is partly done for convenience but also because the use of variance reduction techniques such as implicit capture make it difficult in practice to directly sample photon production from individual reactions.

In OpenMC, secondary photons are created after a nuclide has been sampled in a neutron collision. The expected number of photons produced is

(58)\[n = w\frac{\sigma_{\gamma}(E)}{\sigma_T(E)},\]

where \(w\) is the weight of the neutron, \(\sigma_{\gamma}\) is the photon production cross section for the sampled nuclide, and \(\sigma_T\) is the total cross section for the nuclide. \(\lfloor n \rfloor\) photons are created with an additional photon produced with probability \(n - \lfloor n \rfloor\). Next, a reaction is sampled for each secondary photon. The probability of sampling the \(i\)-th reaction is given by \(\sigma_{\gamma, i}(E)/\sum_j\sigma_{\gamma, j}(E)\), where \(\sum_j\sigma_{\gamma, j} = \sigma_{\gamma}\) is the total photon production cross section. The secondary angle and energy distributions associated with the reaction are used to sample the angle and energy of the emitted photon.

Tallies

Note that the methods discussed in this section are written specifically for continuous-energy mode but equivalent apply to the multi-group mode if the particle’s energy is replaced with the particle’s group

Filters and Scores

The tally capability in OpenMC takes a similar philosophy as that employed in the MC21 Monte Carlo code to give maximum flexibility in specifying tallies while still maintaining scalability. Any tally in a Monte Carlo simulation can be written in the following form:

(1)\[X = \underbrace{\int d\mathbf{r} \int d\mathbf{\Omega} \int dE}_{\text{filters}} \underbrace{f(\mathbf{r}, \mathbf{\Omega}, E)}_{\text{scores}} \psi (\mathbf{r}, \mathbf{\Omega}, E)\]

A user can specify one or more filters which identify which regions of phase space should score to a given tally (the limits of integration as shown in equation (1)) as well as the scoring function (\(f\) in equation (1)). For example, if the desired tally was the \((n,\gamma)\) reaction rate in a fuel pin, the filter would specify the cell which contains the fuel pin and the scoring function would be the radiative capture macroscopic cross section. The following quantities can be scored in OpenMC: flux, total reaction rate, scattering reaction rate, neutron production from scattering, higher scattering moments, \((n,xn)\) reaction rates, absorption reaction rate, fission reaction rate, neutron production rate from fission, and surface currents. The following variables can be used as filters: universe, material, cell, birth cell, surface, mesh, pre-collision energy, post-collision energy, polar angle, azimuthal angle, and the cosine of the change-in-angle due to a scattering event.

With filters for pre- and post-collision energy and scoring functions for scattering and fission production, it is possible to use OpenMC to generate cross sections with user-defined group structures. These multigroup cross sections can subsequently be used in deterministic solvers such as coarse mesh finite difference (CMFD) diffusion.

Using Maps for Filter-Matching

Some Monte Carlo codes suffer severe performance penalties when tallying a large number of quantities. Care must be taken to ensure that a tally system scales well with the total number of tally bins. In OpenMC, a mapping technique is used that allows for a fast determination of what tally/bin combinations need to be scored to a given particle’s phase space coordinates. For each discrete filter variable, a list is stored that contains the tally/bin combinations that could be scored to for each value of the filter variable. If a particle is in cell \(n\), the mapping would identify what tally/bin combinations specify cell \(n\) for the cell filter variable. In this manner, it is not necessary to check the phase space variables against each tally. Note that this technique only applies to discrete filter variables and cannot be applied to energy, angle, or change-in-angle bins. For these filters, it is necessary to perform a binary search on the specified energy grid.

Volume-Integrated Flux and Reaction Rates

One quantity we may wish to compute during the course of a Monte Carlo simulation is the flux or a reaction rate integrated over a finite volume. The volume may be a particular cell, a collection of cells, or the entire geometry. There are various methods by which we can estimate reaction rates

Analog Estimator

The analog estimator is the simplest type of estimator for reaction rates. The basic idea is that we simply count the number of actual reactions that take place and use that as our estimate for the reaction rate. This can be written mathematically as

(2)\[R_x = \frac{1}{W} \sum_{i \in A} w_i\]

where \(R_x\) is the reaction rate for reaction \(x\), \(i\) denotes an index for each event, \(A\) is the set of all events resulting in reaction \(x\), and \(W\) is the total starting weight of the particles, and \(w_i\) is the pre-collision weight of the particle as it enters event \(i\). One should note that equation (2) is volume-integrated so if we want a volume-averaged quantity, we need to divided by the volume of the region of integration. If survival biasing is employed, the analog estimator cannot be used for any reactions with zero neutrons in the exit channel.

Collision Estimator

While the analog estimator is conceptually very simple and easy to implement, it can suffer higher variance due to the fact low probability events will not occur often enough to get good statistics if they are being tallied. Thus, it is desirable to use a different estimator that allows us to score to the tally more often. One such estimator is the collision estimator. Instead of tallying a reaction only when it happens, the idea is to make a contribution to the tally at every collision.

We can start by writing a formula for the collision estimate of the flux. Since \(R = \Sigma_t \phi\) where \(R\) is the total reaction rate, \(\Sigma_t\) is the total macroscopic cross section, and \(\phi\) is the scalar flux, it stands to reason that we can estimate the flux by taking an estimate of the total reaction rate and dividing it by the total macroscopic cross section. This gives us the following formula:

(3)\[\phi = \frac{1}{W} \sum_{i \in C} \frac{w_i}{\Sigma_t (E_i)}\]

where \(W\) is again the total starting weight of the particles, \(C\) is the set of all events resulting in a collision with a nucleus, and \(\Sigma_t (E)\) is the total macroscopic cross section of the target material at the incoming energy of the particle \(E_i\).

If we multiply both sides of equation (3) by the macroscopic cross section for some reaction \(x\), then we get the collision estimate for the reaction rate for that reaction:

(4)\[R_x = \frac{1}{W} \sum_{i \in C} \frac{w_i \Sigma_x (E_i)}{\Sigma_t (E_i)}\]

where \(\Sigma_x (E_i)\) is the macroscopic cross section for reaction \(x\) at the incoming energy of the particle \(E_i\). In comparison to equation (2), we see that the collision estimate will result in a tally with a larger number of events that score to it with smaller contributions (since we have multiplied it by \(\Sigma_x / \Sigma_t\)).

Track-length Estimator

One other method we can use to increase the number of events that scores to tallies is to use an estimator the scores contributions to a tally at every track for the particle rather than every collision. This is known as a track-length estimator, sometimes also called a path-length estimator. We first start with an expression for the volume integrated flux, which can be written as

(5)\[V \phi = \int d\mathbf{r} \int dE \int d\mathbf{\Omega} \int dt \, \psi(\mathbf{r}, \mathbf{\hat{\Omega}}, E, t)\]

where \(V\) is the volume, \(\psi\) is the angular flux, \(\mathbf{r}\) is the position of the particle, \(\mathbf{\hat{\Omega}}\) is the direction of the particle, \(E\) is the energy of the particle, and \(t\) is the time. By noting that \(\psi(\mathbf{r}, \mathbf{\hat{\Omega}}, E, t) = v n(\mathbf{r}, \mathbf{\hat{\Omega}}, E, t)\) where \(n\) is the angular neutron density, we can rewrite equation (5) as

(6)\[V \phi = \int d\mathbf{r} \int dE \int dt v \int d\mathbf{\Omega} \, n(\mathbf{r}, \mathbf{\hat{\Omega}}, E, t)).\]

Using the relations \(N(\mathbf{r}, E, t) = \int d\mathbf{\Omega} n(\mathbf{r}, \mathbf{\hat{\Omega}}, E, t)\) and \(d\ell = v \, dt\) where \(d\ell\) is the differential unit of track length, we then obtain

(7)\[V \phi = \int d\mathbf{r} \int dE \int d\ell N(\mathbf{r}, E, t).\]

Equation (7) indicates that we can use the length of a particle’s trajectory as an estimate for the flux, i.e. the track-length estimator of the flux would be

(8)\[\phi = \frac{1}{W} \sum_{i \in T} w_i \ell_i\]

where \(T\) is the set of all the particle’s trajectories within the desired volume and \(\ell_i\) is the length of the \(i\)-th trajectory. In the same vein as equation (4), the track-length estimate of a reaction rate is found by multiplying equation (8) by a macroscopic reaction cross section:

(9)\[R_x = \frac{1}{W} \sum_{i \in T} w_i \ell_i \Sigma_x (E_i).\]

One important fact to take into consideration is that the use of a track-length estimator precludes us from using any filter that requires knowledge of the particle’s state following a collision because by definition, it will not have had a collision at every event. Thus, for tallies with outgoing-energy filters (which require the post-collision energy), scattering change-in-angle filters, or for tallies of scattering moments (which require the scattering cosine of the change-in-angle), we must use an analog estimator.

Statistics

As was discussed briefly in Introduction, any given result from a Monte Carlo calculation, colloquially known as a “tally”, represents an estimate of the mean of some random variable of interest. This random variable typically corresponds to some physical quantity like a reaction rate, a net current across some surface, or the neutron flux in a region. Given that all tallies are produced by a stochastic process, there is an associated uncertainty with each value reported. It is important to understand how the uncertainty is calculated and what it tells us about our results. To that end, we will introduce a number of theorems and results from statistics that should shed some light on the interpretation of uncertainties.

Law of Large Numbers

The law of large numbers is an important statistical result that tells us that the average value of the result a large number of repeated experiments should be close to the expected value. Let \(X_1, X_2, \dots, X_n\) be an infinite sequence of independent, identically-distributed random variables with expected values \(E(X_1) = E(X_2) = \mu\). One form of the law of large numbers states that the sample mean \(\bar{X_n} = \frac{X_1 + \dots + X_n}{n}\) converges in probability to the true mean, i.e. for all \(\epsilon > 0\)

\[\lim\limits_{n\rightarrow\infty} P \left ( \left | \bar{X}_n - \mu \right | \ge \epsilon \right ) = 0.\]
Central Limit Theorem

The central limit theorem (CLT) is perhaps the most well-known and ubiquitous statistical theorem that has far-reaching implications across many disciplines. The CLT is similar to the law of large numbers in that it tells us the limiting behavior of the sample mean. Whereas the law of large numbers tells us only that the value of the sample mean will converge to the expected value of the distribution, the CLT says that the distribution of the sample mean will converge to a normal distribution. As we defined before, let \(X_1, X_2, \dots, X_n\) be an infinite sequence of independent, identically-distributed random variables with expected values \(E(X_i) = \mu\) and variances \(\text{Var} (X_i) = \sigma^2 < \infty\). Note that we don’t require that these random variables take on any particular distribution – they can be normal, log-normal, Weibull, etc. The central limit theorem states that as \(n \rightarrow \infty\), the random variable \(\sqrt{n} (\bar{X}_n - \mu)\) converges in distribution to the standard normal distribution:

(10)\[\sqrt{n} \left ( \frac{1}{n} \sum_{i=1}^n X_i - \mu \right ) \xrightarrow{d} \mathcal{N} (0, \sigma^2)\]
Estimating Statistics of a Random Variable
Mean

Given independent samples drawn from a random variable, the sample mean is simply an estimate of the average value of the random variable. In a Monte Carlo simulation, the random variable represents physical quantities that we want tallied. If \(X\) is the random variable with \(N\) observations \(x_1, x_2, \dots, x_N\), then an unbiased estimator for the population mean is the sample mean, defined as

(11)\[\bar{x} = \frac{1}{N} \sum_{i=1}^N x_i.\]
Variance

The variance of a population indicates how spread out different members of the population are. For a Monte Carlo simulation, the variance of a tally is a measure of how precisely we know the tally value, with a lower variance indicating a higher precision. There are a few different estimators for the population variance. One of these is the second central moment of the distribution also known as the biased sample variance:

(12)\[s_N^2 = \frac{1}{N} \sum_{i=1}^N \left ( x_i - \bar{x} \right )^2 = \left ( \frac{1}{N} \sum_{i=1}^N x_i^2 \right ) - \bar{x}^2.\]

This estimator is biased because its expected value is actually not equal to the population variance:

(13)\[E[s_N^2] = \frac{N - 1}{N} \sigma^2\]

where \(\sigma^2\) is the actual population variance. As a result, this estimator should not be used in practice. Instead, one can use Bessel’s correction to come up with an unbiased sample variance estimator:

(14)\[s^2 = \frac{1}{N - 1} \sum_{i=1}^N \left ( x_i - \bar{x} \right )^2 = \frac{1}{N - 1} \left ( \sum_{i=1}^N x_i^2 - N\bar{x}^2 \right ).\]

This is the estimator normally used to calculate sample variance. The final form in equation (14) is especially suitable for computation since we do not need to store the values at every realization of the random variable as the simulation proceeds. Instead, we can simply keep a running sum and sum of squares of the values at each realization of the random variable and use that to calculate the variance.

Variance of the Mean

The previous sections discussed how to estimate the mean and variance of a random variable using statistics on a finite sample. However, we are generally not interested in the variance of the random variable itself; we are more interested in the variance of the estimated mean. The sample mean is the result of our simulation, and the variance of the sample mean will tell us how confident we should be in our answers.

Fortunately, it is quite easy to estimate the variance of the mean if we are able to estimate the variance of the random variable. We start with the observation that if we have a series of uncorrelated random variables, we can write the variance of their sum as the sum of their variances:

(15)\[\text{Var} \left ( \sum_{i=1}^N X_i \right ) = \sum_{i=1}^N \text{Var} \left ( X_i \right )\]

This result is known as the Bienaymé formula. We can use this result to determine a formula for the variance of the sample mean. Assuming that the realizations of our random variable are again identical, independently-distributed samples, then we have that

(16)\[\text{Var} \left ( \bar{X} \right ) = \text{Var} \left ( \frac{1}{N} \sum_{i=1}^N X_i \right ) = \frac{1}{N^2} \sum_{i=1}^N \text{Var} \left ( X_i \right ) = \frac{1}{N^2} \left ( N\sigma^2 \right ) = \frac{\sigma^2}{N}.\]

We can combine this result with equation (14) to come up with an unbiased estimator for the variance of the sample mean:

(17)\[s_{\bar{X}}^2 = \frac{1}{N - 1} \left ( \frac{1}{N} \sum_{i=1}^N x_i^2 - \bar{x}^2 \right ).\]

At this point, an important distinction should be made between the estimator for the variance of the population and the estimator for the variance of the mean. As the number of realizations increases, the estimated variance of the population based on equation (14) will tend to the true population variance. On the other hand, the estimated variance of the mean will tend to zero as the number of realizations increases. A practical interpretation of this is that the longer you run a simulation, the better you know your results. Therefore, by running a simulation long enough, it is possible to reduce the stochastic uncertainty to arbitrarily low levels.

Confidence Intervals

While the sample variance and standard deviation gives us some idea about the variability of the estimate of the mean of whatever quantities we’ve tallied, it does not help us interpret how confidence we should be in the results. To quantify the reliability of our estimates, we can use confidence intervals based on the calculated sample variance.

A \(1-\alpha\) confidence interval for a population parameter is defined as such: if we repeat the same experiment many times and calculate the confidence interval for each experiment, then \(1 - \alpha\) percent of the calculated intervals would encompass the true population parameter. Let \(x_1, x_2, \dots, x_N\) be samples from a set of independent, identically-distributed random variables each with population mean \(\mu\) and variance \(\sigma^2\). The t-statistic is defined as

(18)\[t = \frac{\bar{x} - \mu}{s/\sqrt{N}}\]

where \(\bar{x}\) is the sample mean from equation (11) and \(s\) is the standard deviation based on equation (14). If the random variables \(X_i\) are normally-distributed, then the t-statistic has a Student’s t-distribution with \(N-1\) degrees of freedom. This implies that

(19)\[Pr \left ( -t_{1 - \alpha/2, N - 1} \le \frac{\bar{x} - \mu}{s/\sqrt{N}} \le t_{1 - \alpha/2, N - 1} \right ) = 1 - \alpha\]

where \(t_{1-\alpha/2, N-1}\) is the \(1 - \alpha/2\) percentile of a t-distribution with \(N-1\) degrees of freedom. Thus, the \(1 - \alpha\) two sided confidence interval for the sample mean is

(20)\[\bar{x} \pm t_{1 - \alpha/2, N-1} \frac{s}{\sqrt{N}}.\]

One should be cautioned that equation (20) only applies if the underlying random variables are normally-distributed. In general, this may not be true for a tally random variable — the central limit theorem guarantees only that the sample mean is normally distributed, not the underlying random variable. If batching is used, then the underlying random variable, which would then be the averages from each batch, will be normally distributed as long as the conditions of the central limit theorem are met.

Let us now outline the method used to calculate the percentile of the Student’s t-distribution. For one or two degrees of freedom, the percentile can be written analytically. For one degree of freedom, the t-distribution becomes a standard Cauchy distribution whose cumulative distribution function is

(21)\[c(x) = \frac{1}{\pi} \arctan x + \frac{1}{2}.\]

Thus, inverting the cumulative distribution function, we find the \(x\) percentile of the standard Cauchy distribution to be

(22)\[t_{x,1} = \tan \left ( \pi \left ( x - \frac{1}{2} \right ) \right ).\]

For two degrees of freedom, the cumulative distribution function is the second-degree polynomial

(23)\[c(x) = \frac{1}{2} + \frac{x}{2\sqrt{x^2 + 2}}\]

Solving for \(x\), we find the \(x\) percentile to be

(24)\[t_{x,2} = \frac{2\sqrt{2} (x - 1/2)}{\sqrt{1 - 4 (x - 1/2)^2}}\]

For degrees of freedom greater than two, it is not possible to obtain an analytical formula for the inverse of the cumulative distribution function. We must resort to either numerically solving for the inverse or to an approximation. Approximations for percentiles of the t-distribution have been found with high levels of accuracy. OpenMC uses the following approximation:

(25)\[t_{x,n} = \sqrt{\frac{n}{n-2}} \left ( z_x + \frac{1}{4} \frac{z_x^3 - 3z_x}{n-2} + \frac{1}{96} \frac{5z_x^5 - 56z_x^3 + 75z_x}{(n-2)^2} + \frac{1}{384} \frac{3z_x^7 - 81z_x^5 + 417z_x^3 - 315z_x}{(n-2)^3} \right )\]

where \(z_x\) is the \(x\) percentile of the standard normal distribution. In order to determine an arbitrary percentile of the standard normal distribution, we use an unpublished rational approximation. After using the rational approximation, one iteration of Newton’s method is applied to improve the estimate of the percentile.

References

Eigenvalue Calculations

An eigenvalue calculation, also referred to as a criticality calculation, is a transport simulation wherein the source of neutrons includes a fissionable material. Some common eigenvalue calculations include the simulation of nuclear reactors, spent fuel pools, nuclear weapons, and other fissile systems. The reason they are called eigenvalue calculations is that the transport equation becomes an eigenvalue equation if a fissionable source is present since then the source of neutrons will depend on the flux of neutrons itself. Eigenvalue simulations using Monte Carlo methods are becoming increasingly common with the advent of high-performance computing.

This section will explore the theory behind and implementation of eigenvalue calculations in a Monte Carlo code.

Method of Successive Generations

The method used to converge on the fission source distribution in an eigenvalue calculation, known as the method of successive generations, was first introduced by [Lieberoth]. In this method, a finite number of neutron histories, \(N\), are tracked through their lifetime iteratively. If fission occurs, rather than tracking the resulting fission neutrons, the spatial coordinates of the fission site, the sampled outgoing energy and direction of the fission neutron, and the weight of the neutron are stored for use in the subsequent generation. In OpenMC, the array used for storing the fission site information is called the fission bank. At the end of each fission generation, \(N\) source sites for the next generation must be randomly sampled from the \(M\) fission sites that were stored to ensure that the neutron population does not grow exponentially. The sampled source sites are stored in an array called the source bank and can be retrieved during the subsequent generation.

It’s important to recognize that in the method of successive generations, we must start with some assumption on how the fission source sites are distributed since the distribution is not known a priori. Typically, a user will make a guess as to what the distribution is – this guess could be a uniform distribution over some region of the geometry or simply a point source. Fortunately, regardless of the choice of initial source distribution, the method is guaranteed to converge to the true source distribution. Until the source distribution converges, tallies should not be scored to since they will otherwise include contributions from an unconverged source distribution.

The method by which the fission source iterations are parallelized can have a large impact on the achievable parallel scaling. This topic is discussed at length in Fission Bank Algorithms.

Source Convergence Issues

Diagnosing Convergence with Shannon Entropy

As discussed earlier, it is necessary to converge both \(k_{eff}\) and the source distribution before any tallies can begin. Moreover, the convergence rate of the source distribution is in general slower than that of \(k_{eff}\). One should thus examine not only the convergence of \(k_{eff}\) but also the convergence of the source distribution in order to make decisions on when to start active batches.

However, the representation of the source distribution makes it a bit more difficult to analyze its convergence. Since \(k_{eff}\) is a scalar quantity, it is easy to simply look at a line plot of \(k_{eff}\) versus the number of batches and this should give the user some idea about whether it has converged. On the other hand, the source distribution at any given batch is a finite set of coordinates in Euclidean space. In order to analyze the convergence, we would either need to use a method for assessing convergence of an N-dimensional quantity or transform our set of coordinates into a scalar metric. The latter approach has been developed considerably over the last decade and a method now commonly used in Monte Carlo eigenvalue calculations is to use a metric called the Shannon entropy, a concept borrowed from information theory.

To compute the Shannon entropy of the source distribution, we first need to discretize the source distribution rather than having a set of coordinates in Euclidean space. This can be done by superimposing a structured mesh over the geometry (containing at least all fissionable materials). Then, the fraction of source sites that are present in each mesh element is counted:

(1)\[S_i = \frac{\text{Source sites in $i$-th mesh element}}{\text{Total number of source sites}}\]

The Shannon entropy is then computed as

(2)\[H = - \sum_{i=1}^N S_i \log_2 S_i\]

where \(N\) is the number of mesh elements. With equation (2), we now have a scalar metric that we can use to assess the convergence of the source distribution by observing line plots of the Shannon entropy versus the number of batches.

In recent years, researchers have started looking at ways of automatically assessing source convergence to relieve the burden on the user of having to look at plots of \(k_{eff}\) and the Shannon entropy. A number of methods have been proposed (see e.g. [Romano], [Ueki]), but each of these is not without problems.

Uniform Fission Site Method

Generally speaking, the variance of a Monte Carlo tally will be inversely proportional to the number of events that score to the tally. In a reactor problem, this implies that regions with low relative power density will have higher variance that regions with high relative power density. One method to circumvent the uneven distribution of relative errors is the uniform fission site (UFS) method introduced by [Sutton]. In this method, the portion of the problem containing fissionable material is subdivided into a number of cells (typically using a structured mesh). Rather than producing

\[m = \frac{w}{k} \frac{\nu\Sigma_f}{\Sigma_t}\]

fission sites at each collision where \(w\) is the weight of the neutron, \(k\) is the previous-generation estimate of the neutron multiplication factor, \(\nu\Sigma_f\) is the neutron production cross section, and \(\Sigma_t\) is the total cross section, in the UFS method we produce

\[m_{UFS} = \frac{w}{k} \frac{\nu\Sigma_f}{\Sigma_t} \frac{v_i}{s_i}\]

fission sites at each collision where \(v_i\) is the fraction of the total volume occupied by cell \(i\) and \(s_i\) is the fraction of the fission source contained in cell \(i\). To ensure that no bias is introduced, the weight of each fission site stored in the fission bank is \(s_i/v_i\) rather than unity. By ensuring that the expected number of fission sites in each mesh cell is constant, the collision density across all cells, and hence the variance of tallies, is more uniform than it would be otherwise.

[Lieberoth]J. Lieberoth, “A Monte Carlo Technique to Solve the Static Eigenvalue Problem of the Boltzmann Transport Equation,” Nukleonik, 11, 213-219 (1968).
[Romano]Paul K. Romano, “Application of the Stochastic Oscillator to Assess Source Convergence in Monte Carlo Criticality Calculations,” Proc. International Conference on Mathematics, Computational Methods, and Reactor Physics, Saratoga Springs, New York (2009).
[Sutton]Daniel J. Kelly, Thomas M. Sutton, and Stephen C. Wilson, “MC21 Analysis of the Nuclear Energy Agency Monte Carlo Performance Benchmark Problem,” Proc. PHYSOR 2012, Knoxville, Tennessee, Apr. 15–20 (2012).
[Ueki]Taro Ueki, “On-the-Fly Judgments of Monte Carlo Fission Source Convergence,” Trans. Am. Nucl. Soc., 98, 512 (2008).

Depletion

When materials in a system are subject to irradiation over a long period of time, nuclides within the material will transmute due to nuclear reactions as well as spontaneous radioactive decay. The time-dependent process by which nuclides transmute under irradiation is known as depletion or burnup. To accurately analyze nuclear systems, it is often necessary to predict how the composition of materials will change since this change results in a corresponding change in the solution of the transport equation. The equation that governs the transmutation and decay of nuclides inside of an irradiated environment can be written as

\[\begin{aligned} \frac{dN_i(t)}{dt} = &\sum\limits_j \underbrace{\left [ \underbrace{f_{j \rightarrow i} \int_0^\infty dE \; \sigma_j (E, t) \phi(E,t)}_\text{transmutation} + \underbrace{\lambda_{j\rightarrow i}}_\text{decay} \right ] N_j(t)}_{\text{Production of nuclide }i\text{ from nuclide }j} \\ &- \underbrace{\left [\underbrace{\int_0^\infty dE \; \sigma_i (E,t) \phi(E,t)}_\text{transmutation} + \underbrace{\sum\limits_j \lambda_{i\rightarrow j}}_\text{decay} \right ] N_i(t)}_{\text{Loss of nuclide }i} \end{aligned}\]

where \(N_i\) is the density of nuclide \(i\) at time \(t\), \(\sigma_i\) is the transmutation cross section for nuclide \(i\) at energy \(E\), \(f_{j \rightarrow i}\) is the fraction of transmutation reactions in nuclide \(j\) that produce nuclide \(i\), and \(\lambda_{j \rightarrow i}\) is the decay constant for decay modes in nuclide \(j\) that produce nuclide \(i\). Note that we have not included the spatial dependence of the flux or cross sections. As one can see, the equation simply states that the rate of change of \(N_i\) is equal to the production rate minus the loss rate. Because the equation for nuclide \(i\) depends on the nuclide density for possibly many other nuclides, we have a system of first-order differential equations. To form a proper initial value problem, we also need the nuclide densities at time 0:

\[N_i(0) = N_{i,0}.\]

These equations can be written more compactly in matrix notation as

(1)\[\frac{d\mathbf{n}}{dt} = \mathbf{A}(\mathbf{n},t)\mathbf{n}, \quad \mathbf{n}(0) = \mathbf{n}_0\]

where \(\mathbf{n} \in \mathbb{R}^n\) is the nuclide density vector, \(\mathbf{A}(\mathbf{n},t) \in \mathbb{R}^{n\times n}\) is the burnup matrix containing the decay and transmutation coefficients, and \(\mathbf{n}_0\) is the initial density vector. Note that the burnup matrix depends on \(\mathbf{n}\) because the solution to the transport equation depends on the nuclide densities.

Numerical Integration

A variety of numerical methods exist for solving Eq. (1). The simplest such method, known as the “predictor” method, is to divide the overall time interval of interest \([0,t]\) into smaller timesteps over which it is assumed that the burnup matrix is constant. Let \(t \in [t_i, t_i + h]\) be one such timestep. Over the timestep, the solution to Eq. (1) can be written analytically using the matrix exponential

\[\mathbf{A}_i = \mathbf{A}(\mathbf{n}_i, t_i) \\ \mathbf{n}_{i+1} = e^{\mathbf{A}_i h} \mathbf{n}_i\]

where \(\mathbf{n}_i \equiv \mathbf{n}(t_i)\). The exponential of a matrix \(\mathbf{X}\) is defined by the power series expansion

\[e^{\mathbf{X}} = \sum\limits_{k=0}^\infty \frac{1}{k!} \left ( \mathbf{X} \right )^k\]

where \(\mathbf{X}^0 = \mathbf{I}\). A series of so-called predictor-corrector methods that use multiple stages offer improved accuracy over the predictor method. The simplest of these methods, the CE/CM algorithm, is defined as

\[\mathbf{n}_{i+1/2} = e^{\frac{h}{2}\mathbf{A}(\mathbf{n}_i, t_i)} \mathbf{n}_i \\ \mathbf{n}_{i+1} = e^{h \mathbf{A}(\mathbf{n}_{i+1/2},t_{i+1/2})} \mathbf{n}_i\]

Here, the value of \(\mathbf{n}\) at the midpoint is estimated using \(\mathbf{A}\) evaluated at the beginning of the timestep. Then, \(\mathbf{A}\) is evaluated using the densities at the midpoint and used to integrate over the entire timestep.

Our aim here is not to exhaustively describe all integration methods but rather to give a few examples that elucidate the main considerations one must take into account when choosing a method. Generally, there is a tradeoff between the accuracy of the method and its computational expense. The expense is driven almost entirely by the time to compute a transport solution, i.e., to evaluate \(\mathbf{A}\) for a given \(\mathbf{n}\). Thus, the cost of a method scales with the number of \(\mathbf{A}\) evaluations that are performed per timestep. On the other hand, methods that require more evaluations generally achieve higher accuracy. The predictor method only requires one evaluation and its error converges as \(\mathcal{O}(h)\). The CE/CM method requires two evaluations and is thus twice as expensive as the predictor method, but achieves an error of \(\mathcal{O}(h^2)\). An exhaustive description of time integration methods and their merits can be found in the thesis of Colin Josey.

OpenMC does not rely on a single time integration method but rather has several classes that implement different algorithms. For example, the openmc.deplete.PredictorIntegrator class implements the predictor method, and the openmc.deplete.CECMIntegrator class implements the CE/CM method. A full list of the integrator classes available can be found in the documentation for the openmc.deplete module.

Matrix Exponential

As we saw in the previous section, numerically integrating Eq. (1) requires evaluating one or more matrix exponentials. OpenMC uses the Chebyshev rational approximation method (CRAM), which was introduced in a series of papers by Pusa (1, 2), to evaluate matrix exponentials. In particular, OpenMC utilizes an incomplete partial fraction (IPF) form of CRAM that provides a good balance of numerical stability and efficiency. In this representation the matrix exponential is approximated as

\[e^{\mathbf{A}t} \approx \alpha_0 \prod\limits_{\ell=1}^{k/2} \left ( \mathbf{I} + 2 \text{Re} \left ( \widetilde{\alpha}_\ell \left (\mathbf{A}t - \theta_\ell \mathbf{I} \right )^{-1} \right ) \right )\]

where \(k\) is the order of the approximation and \(\alpha_0\), \(\widetilde{\alpha}_\ell\), and \(\theta_\ell\) are coefficients that have been tabulated for orders up to \(k=48\). Rather than computing the full approximation and then multiplying it by a vector, the following algorithm is used to incrementally apply the terms within the product (note that the original description of the algorithm presented by Pusa contains a typo):

  1. \(\mathbf{n} \gets \mathbf{n_0}\)
  2. For \(\ell = 1, 2, \dots, k/2\)
    • \(\mathbf{n} \gets \mathbf{n} + 2\text{Re}(\widetilde{\alpha}_\ell (\mathbf{A}t - \theta_\ell)^{-1})\mathbf{n}\)
  3. \(\mathbf{n} \gets \alpha_0 \mathbf{n}\)

The \(k\)th order approximation for CRAM requires solving \(k/2\) sparse linear systems. OpenMC relies on functionality from scipy.sparse.linalg for solving the linear systems.

Data Considerations

In principle, solving Eq. (1) using CRAM is fairly simple: just construct the burnup matrix at various times and solve a set of sparse linear systems. However, constructing the burnup matrix itself involves not only solving the transport equation to estimate transmutation reaction rates but also a series of choices about what data to include. In OpenMC, the burnup matrix is constructed based on data inside of a depletion chain file, which includes fundamental data gathered from ENDF incident neutron, decay, and fission product yield sublibraries. For each nuclide, this file includes:

  • What transmutation reactions are possible, their Q values, and their products;
  • If a nuclide is not stable, what decay modes are possible, their branching ratios, and their products; and
  • If a nuclide is fissionable, the fission products yields at any number of incident neutron energies.
Transmutation Reactions

OpenMC will setup tallies in a problem based on what transmutation reactions are available in a depletion chain file, so any arbitrary number of transmutation reactions can be tracked. The pregenerated chain files that are available on https://openmc.org include the following transmutation reactions: fission, (n,\(\gamma\)), (n,2n), (n,3n), (n,4n), (n,p), and (n,\(\alpha\)).

Capture Branching Ratios

Some (n,\(\gamma\)) reactions may result in a product being in either the ground or a metastable state. The most well-known example is capture in Am241, which can produce either Am242 or Am242m. Because the metastable state of Am242m has a significantly longer half-life than the ground state, it is important to accurately model the branching of the capture reaction in Am241. This is complicated by the fact that the branching ratio may depend on the incident neutron energy causing capture.

OpenMC does not currently allow energy-dependent capture branching ratios. However, the depletion chain file does allow a transmutation reaction to be listed multiple times with different branching ratios resulting in different products. Spectrum-averaged capture branching ratios have been computed in LWR and SFR spectra and are available at https://openmc.org/depletion-chains.

Fission Product Yields

Fission product yields (FPY) are also energy-dependent in general. ENDF fission product yield sublibraries typically include yields tabulated at 2 or 3 energies. It is an open question as to what the best way to handle this energy dependence is. OpenMC includes three methods for treating the energy dependence of FPY:

  1. Use FPY data corresponding to a specified energy.
  2. Tally fission rates above and below a specified cutoff energy. Assume that all fissions below the cutoff energy correspond to thermal FPY data and all fission above the cutoff energy correspond to fast FPY data.
  3. Compute the average energy at which fission events occur and use an effective FPY by linearly interpolating between FPY provided at neighboring energies.

The method can be selected through the fission_yield_mode argument to the openmc.deplete.Operator constructor.

Power Normalization

The reaction rates provided OpenMC are given in units of reactions per source particle. For depletion, it is necessary to compute an absolute reaction rate in reactions per second. To do so, the reaction rates are normalized based on a specified power. A complete description of how this normalization can be performed is described in Normalization of Tally Results. Here, we simply note that the main depletion class, openmc.deplete.Operator, allows the user to choose one of two methods for estimating the heating rate, including:

  1. Using fixed Q values from a depletion chain file (useful for comparisons to other codes that use fixed Q values), or
  2. Using the heating or heating-local scores to obtain an nuclide- and energy-dependent estimate of the true heating rate.

The method for normalization can be chosen through the normalization_mode argument to the openmc.deplete.Operator class.

Heating and Energy Deposition

As particles traverse a problem, some portion of their energy is deposited at collision sites. This energy is deposited when charged particles, including electrons and recoil nuclei, undergo electromagnetic interactions with surrounding electons and ions. The information describing how much energy is deposited for a specific reaction is referred to as “heating numbers” and can be computed using a program like NJOY with the heatr module.

The heating rate is the product of reaction-specific coefficients and a reaction cross section

\[H(E) = \phi(E)\sum_i\rho_i\sum_rk_{i, r}(E),\]

and has units energy per time, typically eV/s. Here, \(k_{i, r}\) are the KERMA (Kinetic Energy Release in Materials) [Mack97] coefficients for reaction \(r\) of isotope \(i\). The KERMA coefficients have units of energy \(\times\) cross-section (e.g., eV-barn) and can be used much like a reaction cross section for the purpose of tallying energy deposition.

KERMA coefficients can be computed using the energy-balance method with a nuclear data processing code like NJOY, which performs the following iteration over all reactions \(r\) for all isotopes \(i\) requested

\[k_{i, r}(E) = \left(E + Q_{i, r} - \bar{E}_{i, r, n} - \bar{E}_{i, r, \gamma}\right)\sigma_{i, r}(E),\]

removing the energy of neutral particles (neutrons and photons) that are transported away from the reaction site \(\bar{E}\), and the reaction \(Q\) value.

Fission

During a fission event, there are potentially many secondary particles, and all must be considered. The total energy released in a fission event is typically broken up into the following categories:

  • \(E_{fr}\) - kinetic energy of fission fragments
  • \(E_{n,p}\) - energy of prompt fission neutrons
  • \(E_{n,d}\) - energy of delayed fission neutrons
  • \(E_{\gamma,p}\) - energy of prompt fission photons
  • \(E_{\gamma,d}\) - energy of delayed fission photons
  • \(E_{\beta}\) - energy of released \(\beta\) particles
  • \(E_{\nu}\) - energy of neutrinos

These components are defined in MF=1, MT=458 data in a standard ENDF-6 formatted file. All these quantities may depend upon incident neutron energy, but this dependence is not shown to make the following demonstrations cleaner. As neutrinos scarcely interact with matter, the recoverable energy from fission is defined as

\[E_r\equiv E_{fr} + E_{n,p} + E_{n, d} + E_{\gamma, p} + E_{\gamma, d} + E_{\beta}\]

Furthermore, the energy of the secondary neutrons and photons is given as \(E_{n, p}\) and \(E_{\gamma, p}\), respectively.

NJOY computes the fission KERMA coefficient using this energy-balance method to be

\[k_{i, f}(E) = \left[E + Q(E) - \bar{E}(E)\right]\sigma_{i, f}(E) = \left[E_{fr} + E_{\gamma, p}\right]\sigma_{i, j}(E)\]

Note

The energy from delayed neutrons and photons and beta particles is intentionally left out from the NJOY calculations.

OpenMC Implementation

For fissile isotopes, OpenMC makes modifications to the heating reaction to include all relevant components of fission energy release. These modifications are made to the total heating reaction, MT=301. Breaking the total heating KERMA into a fission and non-fission section, one can write

\[k_i(E) = k_{i, nf}(E) + \left[E_{fr}(E) + E_{\gamma, p}\right]\sigma_{i, f}(E)\]

OpenMC seeks to modify the total heating data to include energy from \(\beta\) particles and, conditionally, delayed photons. This conditional inclusion depends on the simulation mode: neutron transport, or coupled neutron-photon transport. The heating due to fission is removed using MT=318 data, and then re-built using the desired components of fission energy release from MF=1,MT=458 data.

Neutron Transport

For this case, OpenMC instructs heatr to produce heating coefficients assuming that energy from photons, \(E_{\gamma, p}\) and \(E_{\gamma, d}\), is deposited at the fission site. Let \(N901\) represent the total heating number returned from this heatr run with \(N918\) reflecting fission heating computed from NJOY. \(M901\) represent the following modification

\[M901_{i}(E)\equiv N901_{i}(E) - N918_{i}(E) + \left[E_{i, fr} + E_{i, \beta} + E_{i, \gamma, p} + E_{i, \gamma, d}\right]\sigma_{i, f}(E).\]

This modified heating data is stored as the MT=901 reaction and will be scored if heating-local is included in openmc.Tally.scores.

Coupled neutron-photon transport

Here, OpenMC instructs heatr to assume that energy from photons is not deposited locally. However, the definitions provided in the NJOY manual indicate that, regardless of this mode, the prompt photon energy is still included in \(k_{i, f}\), and therefore must be manually removed. Let \(N301\) represent the total heating number returned from this heatr run and \(M301\) be

\[M301_{i}(E)\equiv N301_{i}(E) - N318_{i}(E) + \left[E_{i, fr}(E) + E_{i, \beta}(E)\right]\sigma_{i, f}(E).\]

This modified heating data is stored as the MT=301 reaction and will be scored if heating is included in openmc.Tally.scores.

References

[Mack97]Abdou, M.A., Maynard, C.W., and Wright, R.Q. MACK: computer program to calculate neutron energy release parameters (fluence-to-kerma factors) and multigroup neutron reaction cross sections from nuclear data in ENDF Format. Oak Ridge National Laboratory report ORNL-TM-3994.

Parallelization

Due to the computationally-intensive nature of Monte Carlo methods, there has been an ever-present interest in parallelizing such simulations. Even in the first paper on the Monte Carlo method, John Metropolis and Stanislaw Ulam recognized that solving the Boltzmann equation with the Monte Carlo method could be done in parallel very easily whereas the deterministic counterparts for solving the Boltzmann equation did not offer such a natural means of parallelism. With the introduction of vector computers in the early 1970s, general-purpose parallel computing became a reality. In 1972, Troubetzkoy et al. designed a Monte Carlo code to be run on the first vector computer, the ILLIAC-IV [Troubetzkoy]. The general principles from that work were later refined and extended greatly through the work of Forrest Brown in the 1980s. However, as Brown’s work shows, the single-instruction multiple-data (SIMD) parallel model inherent to vector processing does not lend itself to the parallelism on particles in Monte Carlo simulations. Troubetzkoy et al. recognized this, remarking that “the order and the nature of these physical events have little, if any, correlation from history to history,” and thus following independent particle histories simultaneously using a SIMD model is difficult.

The difficulties with vector processing of Monte Carlo codes led to the adoption of the single program multiple data (SPMD) technique for parallelization. In this model, each different process tracks a particle independently of other processes, and between fission source generations the processes communicate data through a message-passing interface. This means of parallelism was enabled by the introduction of message-passing standards in the late 1980s and early 1990s such as PVM and MPI. The SPMD model proved much easier to use in practice and took advantage of the inherent parallelism on particles rather than instruction-level parallelism. As a result, it has since become ubiquitous for Monte Carlo simulations of transport phenomena.

Thanks to the particle-level parallelism using SPMD techniques, extremely high parallel efficiencies could be achieved in Monte Carlo codes. Until the last decade, even the most demanding problems did not require transmitting large amounts of data between processors, and thus the total amount of time spent on communication was not significant compared to the amount of time spent on computation. However, today’s computing power has created a demand for increasingly large and complex problems, requiring a greater number of particles to obtain decent statistics (and convergence in the case of criticality calculations). This results in a correspondingly higher amount of communication, potentially degrading the parallel efficiency. Thus, while Monte Carlo simulations may seem embarrassingly parallel, obtaining good parallel scaling with large numbers of processors can be quite difficult to achieve in practice.

Fission Bank Algorithms

Master-Slave Algorithm

Monte Carlo particle transport codes commonly implement a SPMD model by having one master process that controls the scheduling of work and the remaining processes wait to receive work from the master, process the work, and then send their results to the master at the end of the simulation (or a source iteration in the case of an eigenvalue calculation). This idea is illustrated in Communication pattern in master-slave algorithm..

_images/master-slave.png

Figure 10: Communication pattern in master-slave algorithm.

Eigenvalue calculations are slightly more difficult to parallelize than fixed source calculations since it is necessary to converge on the fission source distribution and eigenvalue before tallying. In the Method of Successive Generations, to ensure that the results are reproducible, one must guarantee that the process by which fission sites are randomly sampled does not depend on the number of processors. What is typically done is the following:

  1. Each compute node sends its fission bank sites to a master process;

2. The master process sorts or orders the fission sites based on a unique identifier;

3. The master process samples \(N\) fission sites from the ordered array of \(M\) sites; and

4. The master process broadcasts all the fission sites to the compute nodes.

The first and last steps of this process are the major sources of communication overhead between cycles. Since the master process must receive \(M\) fission sites from the compute nodes, the first step is necessarily serial. This step can be completed in \(O(M)\) time. The broadcast step can benefit from parallelization through a tree-based algorithm. Despite this, the communication overhead is still considerable.

To see why this is the case, it is instructive to look at a hypothetical example. Suppose that a calculation is run with \(N = 10,000,000\) neutrons across 64 compute nodes. On average, \(M = 10,000,000\) fission sites will be produced. If the data for each fission site consists of a spatial location (three 8 byte real numbers) and a unique identifier (one 4 byte integer), the memory required per site is 28 bytes. To broadcast 10,000,000 source sites to 64 nodes will thus require transferring 17.92 GB of data. Since each compute node does not need to keep every source site in memory, one could modify the algorithm from a broadcast to a scatter. However, for practical reasons (e.g. work self-scheduling), this is normally not done in production Monte Carlo codes.

Nearest Neighbors Algorithm

To reduce the amount of communication required in a fission bank synchronization algorithm, it is desirable to move away from the typical master-slave algorithm to an algorithm whereby the compute nodes communicate with one another only as needed. This concept is illustrated in Communication pattern in nearest neighbor algorithm..

_images/nearest-neighbor.png

Figure 11: Communication pattern in nearest neighbor algorithm.

Since the source sites for each cycle are sampled from the fission sites banked from the previous cycle, it is a common occurrence for a fission site to be banked on one compute node and sent back to the master only to get sent back to the same compute node as a source site. As a result, much of the communication inherent in the algorithm described previously is entirely unnecessary. By keeping the fission sites local, having each compute node sample fission sites, and sending sites between nodes only as needed, one can cut down on most of the communication. One algorithm to achieve this is as follows:

1. An exclusive scan is performed on the number of sites banked, and the total number of fission bank sites is broadcasted to all compute nodes. By picturing the fission bank as one large array distributed across multiple nodes, one can see that this step enables each compute node to determine the starting index of fission bank sites in this array. Let us call the starting and ending indices on the \(i\)-th node \(a_i\) and \(b_i\), respectively;

2. Each compute node samples sites at random from the fission bank using the same starting seed. A separate array on each compute node is created that consists of sites that were sampled local to that node, i.e. if the index of the sampled site is between \(a_i\) and \(b_i\), it is set aside;

3. If any node sampled more than \(N/p\) fission sites where \(p\) is the number of compute nodes, the extra sites are put in a separate array and sent to all other compute nodes. This can be done efficiently using the allgather collective operation;

4. The extra sites are divided among those compute nodes that sampled fewer than \(N/p\) fission sites.

However, even this algorithm exhibits more communication than necessary since the allgather will send fission bank sites to nodes that don’t necessarily need any extra sites.

One alternative is to replace the allgather with a series of sends. If \(a_i\) is less than \(iN/p\), then send \(iN/p - a_i\) sites to the left adjacent node. Similarly, if \(a_i\) is greater than \(iN/p\), then receive \(a_i - iN/p\) from the left adjacent node. This idea is applied to the fission bank sites at the end of each node’s array as well. If \(b_i\) is less than \((i+1)N/p\), then receive \((i+1)N/p - b_i\) sites from the right adjacent node. If \(b_i\) is greater than \((i+1)N/p\), then send \(b_i - (i+1)N/p\) sites to the right adjacent node. Thus, each compute node sends/receives only two messages under normal circumstances.

The following example illustrates how this algorithm works. Let us suppose we are simulating \(N = 1000\) neutrons across four compute nodes. For this example, it is instructive to look at the state of the fission bank and source bank at several points in the algorithm:

  1. The beginning of a cycle where each node has \(N/p\) source sites;
  2. The end of a cycle where each node has accumulated fission sites;

3. After sampling, where each node has some amount of source sites usually not equal to \(N/p\);

4. After redistribution, each node again has \(N/p\) source sites for the next cycle;

At the end of each cycle, each compute node needs 250 fission bank sites to continue on the next cycle. Let us suppose that \(p_0\) produces 270 fission banks sites, \(p_1\) produces 230, \(p_2\) produces 290, and \(p_3\) produces 250. After each node samples from its fission bank sites, let’s assume that \(p_0\) has 260 source sites, \(p_1\) has 215, \(p_2\) has 280, and \(p_3\) has 245. Note that the total number of sampled sites is 1000 as needed. For each node to have the same number of source sites, \(p_0\) needs to send its right-most 10 sites to \(p_1\), and \(p_2\) needs to send its left-most 25 sites to \(p_1\) and its right-most 5 sites to \(p_3\). A schematic of this example is shown in Example of nearest neighbor algorithm.. The data local to each node is given a different hatching, and the cross-hatched regions represent source sites that are communicated between adjacent nodes.

_images/nearest-neighbor-example.png

Figure 12: Example of nearest neighbor algorithm.

Cost of Master-Slave Algorithm

While the prior considerations may make it readily apparent that the novel algorithm should outperform the traditional algorithm, it is instructive to look at the total communication cost of the novel algorithm relative to the traditional algorithm. This is especially so because the novel algorithm does not have a constant communication cost due to stochastic fluctuations. Let us begin by looking at the cost of communication in the traditional algorithm

As discussed earlier, the traditional algorithm is composed of a series of sends and typically a broadcast. To estimate the communication cost of the algorithm, we can apply a simple model that captures the essential features. In this model, we assume that the time that it takes to send a message between two nodes is given by \(\alpha + (sN)\beta\), where \(\alpha\) is the time it takes to initiate the communication (commonly called the latency), \(\beta\) is the transfer time per unit of data (commonly called the bandwidth), \(N\) is the number of fission sites, and \(s\) is the size in bytes of each fission site.

The first step of the traditional algorithm is to send \(p\) messages to the master node, each of size \(sN/p\). Thus, the total time to send these messages is

(1)\[t_{\text{send}} = p\alpha + sN\beta.\]

Generally, the best parallel performance is achieved in a weak scaling scheme where the total number of histories is proportional to the number of processors. However, we see that when \(N\) is proportional to \(p\), the time to send these messages increases proportionally with \(p\).

Estimating the time of the broadcast is complicated by the fact that different MPI implementations may use different algorithms to perform collective communications. Worse yet, a single implementation may use a different algorithm depending on how many nodes are communicating and the size of the message. Using multiple algorithms allows one to minimize latency for small messages and minimize bandwidth for long messages.

We will focus here on the implementation of broadcast in the MPICH implementation. For short messages, MPICH uses a binomial tree algorithm. In this algorithm, the root process sends the data to one node in the first step, and then in the subsequent, both the root and the other node can send the data to other nodes. Thus, it takes a total of \(\lceil \log_2 p \rceil\) steps to complete the communication. The time to complete the communication is

(2)\[t_{\text{short}} = \lceil \log_2 p \rceil \left ( \alpha + sN\beta \right ).\]

This algorithm works well for short messages since the latency term scales logarithmically with the number of nodes. However, for long messages, an algorithm that has lower bandwidth has been proposed by Barnett and implemented in MPICH. Rather than using a binomial tree, the broadcast is divided into a scatter and an allgather. The time to complete the scatter is :math:` log_2 p : alpha + frac{p-1}{p} Nbeta` using a binomial tree algorithm. The allgather is performed using a ring algorithm that completes in \(p-1) \alpha + \frac{p-1}{p} N\beta\). Thus, together the time to complete the broadcast is

(3)\[t_{\text{long}} = \left ( \log_2 p + p - 1 \right ) \alpha + 2 \frac{p-1}{p} sN\beta.\]

The fission bank data will generally exceed the threshold for switching from short to long messages (typically 8 kilobytes), and thus we will use the equation for long messages. Adding equations (1) and (3), the total cost of the series of sends and the broadcast is

(4)\[t_{\text{old}} = \left ( \log_2 p + 2p - 1 \right ) \alpha + \frac{3p-2}{p} sN\beta.\]
Cost of Nearest Neighbor Algorithm

With the communication cost of the traditional fission bank algorithm quantified, we now proceed to discuss the communicatin cost of the proposed algorithm. Comparing the cost of communication of this algorithm with the traditional algorithm is not trivial due to fact that the cost will be a function of how many fission sites are sampled on each node. If each node samples exactly \(N/p\) sites, there will not be communication between nodes at all. However, if any one node samples more or less than \(N/p\) sites, the deviation will result in communication between logically adjacent nodes. To determine the expected deviation, one can analyze the process based on the fundamentals of the Monte Carlo process.

The steady-state neutron transport equation for a multiplying medium can be written in the form of an eigenvalue problem,

(5)\[S(\mathbf{r})= \frac{1}{k} \int F(\mathbf{r}' \rightarrow \mathbf{r})S(\mathbf{r}')\: d\mathbf{r},\]

where \(\mathbf{r}\) is the spatial coordinates of the neutron, \(S(\mathbf{r})\) is the source distribution defined as the expected number of neutrons born from fission per unit phase-space volume at \(\mathbf{r}\), \(F( \mathbf{r}' \rightarrow \mathbf{r})\) is the expected number of neutrons born from fission per unit phase space volume at \(\mathbf{r}\) caused by a neutron at \(\mathbf{r}\), and \(k\) is the eigenvalue. The fundamental eigenvalue of equation (5) is known as \(k_{eff}\), but for simplicity we will simply refer to it as \(k\).

In a Monte Carlo criticality simulation, the power iteration method is applied iteratively to obtain stochastic realizations of the source distribution and estimates of the \(k\)-eigenvalue. Let us define \(\hat{S}^{(m)}\) to be the realization of the source distribution at cycle \(m\) and \(\hat{\epsilon}^{(m)}\) be the noise arising from the stochastic nature of the tracking process. We can write the stochastic realization in terms of the fundamental source distribution and the noise component as (see Brissenden and Garlick):

(6)\[\hat{S}^{(m)}(\mathbf{r})= N S(\mathbf{r}) + \sqrt{N} \hat{\epsilon}^{(m)}(\mathbf{r}),\]

where \(N\) is the number of particle histories per cycle. Without loss of generality, we shall drop the superscript notation indicating the cycle as it is understood that the stochastic realization is at a particular cycle. The expected value of the stochastic source distribution is simply

(7)\[E \left[ \hat{S}(\mathbf{r})\right] = N S (\mathbf{r})\]

since \(E \left[ \hat{\epsilon}(\mathbf{r})\right] = 0\). The noise in the source distribution is due only to \(\hat{\epsilon}(\mathbf{r})\) and thus the variance of the source distribution will be

(8)\[\text{Var} \left[ \hat{S}(\mathbf{r})\right] = N \text{Var} \left[ \hat{\epsilon}(\mathbf{r}) \right].\]

Lastly, the stochastic and true eigenvalues can be written as integrals over all phase space of the stochastic and true source distributions, respectively, as

(9)\[\hat{k} = \frac{1}{N} \int \hat{S}(\mathbf{r}) \: d\mathbf{r} \quad \text{and} \quad k = \int S(\mathbf{r}) \: d\mathbf{r},\]

noting that \(S(\mathbf{r})\) is \(O(1)\). One should note that the expected value \(k\) calculated by Monte Carlo power iteration (i.e. the method of successive generations) will be biased from the true fundamental eigenvalue of equation (5) by \(O(1/N)\) (see Brissenden and Garlick), but we will assume henceforth that the number of particle histories per cycle is sufficiently large to neglect this bias.

With this formalism, we now have a framework within which we can determine the properties of the distribution of expected number of fission sites. The explicit form of the source distribution can be written as

(10)\[\hat{S}(\mathbf{r}) = \sum_{i=1}^{M} w_i \delta( \mathbf{r} - \mathbf{r}_i )\]

where \(\mathbf{r}_i\) is the spatial location of the \(i\)-th fission site, \(w_i\) is the statistical weight of the fission site at \(\mathbf{r}_i\), and \(M\) is the total number of fission sites. It is clear that the total weight of the fission sites is simply the integral of the source distribution. Integrating equation (6) over all space, we obtain

(11)\[\int \hat{S}(\mathbf{r}) \: d\mathbf{r} = N \int S(\mathbf{r}) \: d\mathbf{r} + \sqrt{N} \int \hat{\epsilon}(\mathbf{r}) \: d\mathbf{r} .\]

Substituting the expressions for the stochastic and true eigenvalues from equation (9), we can relate the stochastic eigenvalue to the integral of the noise component of the source distribution as

(12)\[N\hat{k} = Nk + \sqrt{N} \int \hat{\epsilon}(\mathbf{r}) \: d\mathbf{r}.\]

Since the expected value of \(\hat{\epsilon}\) is zero, the expected value of its integral will also be zero. We thus see that the variance of the integral of the source distribution, i.e. the variance of the total weight of fission sites produced, is directly proportional to the variance of the integral of the noise component. Let us call this term \(\sigma^2\) for simplicity:

(13)\[\text{Var} \left[ \int \hat{S}(\mathbf{r}) \right ] = N \sigma^2.\]

The actual value of \(\sigma^2\) will depend on the physical nature of the problem, whether variance reduction techniques are employed, etc. For instance, one could surmise that for a highly scattering problem, \(\sigma^2\) would be smaller than for a highly absorbing problem since more collisions will lead to a more precise estimate of the source distribution. Similarly, using implicit capture should in theory reduce the value of \(\sigma^2\).

Let us now consider the case where the \(N\) total histories are divided up evenly across \(p\) compute nodes. Since each node simulates \(N/p\) histories, we can write the source distribution as

(14)\[\hat{S}_i(\mathbf{r})= \frac{N}{p} S(\mathbf{r}) + \sqrt{\frac{N}{p}} \hat{\epsilon}_i(\mathbf{r}) \quad \text{for} \quad i = 1, \dots, p\]

Integrating over all space and simplifying, we can obtain an expression for the eigenvalue on the \(i\)-th node:

(15)\[\hat{k}_i = k + \sqrt{\frac{p}{N}} \int \hat{\epsilon}_i(\mathbf{r}) \: d\mathbf{r}.\]

It is easy to show from this expression that the stochastic realization of the global eigenvalue is merely the average of these local eigenvalues:

(16)\[\hat{k} = \frac{1}{p} \sum_{i=1}^p \hat{k}_i.\]

As was mentioned earlier, at the end of each cycle one must sample \(N\) sites from the \(M\) sites that were created. Thus, the source for the next cycle can be seen as the fission source from the current cycle divided by the stochastic realization of the eigenvalue since it is clear from equation (9) that \(\hat{k} = M/N\). Similarly, the number of sites sampled on each compute node that will be used for the next cycle is

(17)\[M_i = \frac{1}{\hat{k}} \int \hat{S}_i(\mathbf{r}) \: d\mathbf{r} = \frac{N}{p} \frac{\hat{k}_i}{\hat{k}}.\]

While we know conceptually that each compute node will under normal circumstances send two messages, many of these messages will overlap. Rather than trying to determine the actual communication cost, we will instead attempt to determine the maximum amount of data being communicated from one node to another. At any given cycle, the number of fission sites that the \(j\)-th compute node will send or receive (\(\Lambda_j\)) is

(18)\[\Lambda_j = \left | \sum_{i=1}^j M_i - \frac{jN}{p} \right |.\]

Noting that \(jN/p\) is the expected value of the summation, we can write the expected value of \(\Lambda_j\) as the mean absolute deviation of the summation:

(19)\[E \left [ \Lambda_j \right ] = E \left [ \left | \sum_{i=1}^j M_i - \frac{jN}{p} \right | \right ] = \text{MD} \left [ \sum_{i=1}^j M_i \right ]\]

where \(\text{MD}\) indicates the mean absolute deviation of a random variable. The mean absolute deviation is an alternative measure of variability.

In order to ascertain any information about the mean deviation of \(M_i\), we need to know the nature of its distribution. Thus far, we have said nothing of the distributions of the random variables in question. The total number of fission sites resulting from the tracking of \(N\) neutrons can be shown to be normally distributed via the Central Limit Theorem (provided that \(N\) is sufficiently large) since the fission sites resulting from each neutron are “sampled” from independent, identically-distributed random variables. Thus, \(\hat{k}\) and \(\int \hat{S} (\mathbf{r}) \: d\mathbf{r}\) will be normally distributed as will the individual estimates of these on each compute node.

Next, we need to know what the distribution of \(M_i\) in equation (17) is or, equivalently, how \(\hat{k}_i / \hat{k}\) is distributed. The distribution of a ratio of random variables is not easy to calculate analytically, and it is not guaranteed that the ratio distribution is normal if the numerator and denominator are normally distributed. For example, if \(X\) is a standard normal distribution and \(Y\) is also standard normal distribution, then the ratio \(X/Y\) has the standard Cauchy distribution. The reader should be reminded that the Cauchy distribution has no defined mean or variance. That being said, Geary has shown that, for the case of two normal distributions, if the denominator is unlikely to assume values less than zero, then the ratio distribution is indeed approximately normal. In our case, \(\hat{k}\) absolutely cannot assume a value less than zero, so we can be reasonably assured that the distribution of \(M_i\) will be normal.

For a normal distribution with mean \(\mu\) and distribution function \(f(x)\), it can be shown that

(20)\[\int_{-\infty}^{\infty} f(x) \left | x - \mu \right | \: dx = \sqrt{\frac{2}{\pi} \int_{-\infty}^{\infty} f(x) \left ( x - \mu \right )^2 \: dx}\]

and thus the mean absolute deviation is \(\sqrt{2/\pi}\) times the standard deviation. Therefore, to evaluate the mean absolute deviation of \(M_i\), we need to first determine its variance. Substituting equation (16) into equation (17), we can rewrite \(M_i\) solely in terms of \(\hat{k}_1, \dots, \hat{k}_p\):

(21)\[M_i = \frac{N \hat{k}_i}{\sum\limits_{j=1}^p \hat{k}_j}.\]

Since we know the variance of \(\hat{k}_i\), we can use the error propagation law to determine the variance of \(M_i\):

(22)\[\text{Var} \left [ M_i \right ] = \sum_{j=1}^p \left ( \frac{\partial M_i}{\partial \hat{k}_j} \right )^2 \text{Var} \left [ \hat{k}_j \right ] + \sum\limits_{j \neq m} \sum\limits_{m=1}^p \left ( \frac{\partial M_i}{\partial \hat{k}_j} \right ) \left ( \frac{\partial M_i}{\partial \hat{k}_m} \right ) \text{Cov} \left [ \hat{k}_j, \hat{k}_m \right ]\]

where the partial derivatives are evaluated at \(\hat{k}_j = k\). Since \(\hat{k}_j\) and \(\hat{k}_m\) are independent if \(j \neq m\), their covariance is zero and thus the second term cancels out. Evaluating the partial derivatives, we obtain

(23)\[\text{Var} \left [ M_i \right ] = \left ( \frac{N(p-1)}{kp^2} \right )^2 \frac{p\sigma^2}{N} + \sum_{j \neq i} \left ( \frac{-N}{kp^2} \right )^2 \frac{p\sigma^2}{N} = \frac{N(p-1)}{k^2p^2} \sigma^2.\]

Through a similar analysis, one can show that the variance of \(\sum_{i=1}^j M_i\) is

(24)\[\text{Var} \left [ \sum_{i=1}^j M_i \right ] = \frac{Nj(p-j)}{k^2p^2} \sigma^2\]

Thus, the expected amount of communication on node \(j\), i.e. the mean absolute deviation of \(\sum_{i=1}^j M_i\) is proportional to

(25)\[E \left [ \Lambda_j \right ] = \sqrt{\frac{2Nj(p-j)\sigma^2}{\pi k^2p^2}}.\]

This formula has all the properties that one would expect based on intuition:

1. As the number of histories increases, the communication cost on each node increases as well;

2. If \(p=1\), i.e. if the problem is run on only one compute node, the variance will be zero. This reflects the fact that exactly \(N\) sites will be sampled if there is only one node.

3. For \(j=p\), the variance will be zero. Again, this says that when you sum the number of sites from each node, you will get exactly \(N\) sites.

We can determine the node that has the highest communication cost by differentiating equation (25) with respect to \(j\), setting it equal to zero, and solving for \(j\). Doing so yields \(j_{\text{max}} = p/2\). Interestingly, substituting \(j = p/2\) in equation (25) shows us that the maximum communication cost is actually independent of the number of nodes:

(26)\[E \left [ \Lambda_{j_{\text{max}}} \right ] = \sqrt{ \frac{N\sigma^2}{2\pi k^2}}.\]

References

[Troubetzkoy]E. Troubetzkoy, H. Steinberg, and M. Kalos, “Monte Carlo Radiation Penetration Calculations on a Parallel Computer,” Trans. Am. Nucl. Soc., 17, 260 (1973).

Nonlinear Diffusion Acceleration - Coarse Mesh Finite Difference

This page section discusses how nonlinear diffusion acceleration (NDA) using coarse mesh finite difference (CMFD) is implemented into OpenMC. Before we get into the theory, general notation for this section is discussed.

Note that the methods discussed in this section are written specifically for continuous-energy mode but equivalent apply to the multi-group mode if the particle’s energy is replaced with the particle’s group

Notation

Before deriving NDA relationships, notation is explained. If a parameter has a \(\overline{\cdot}\), it is surface area-averaged and if it has a \(\overline{\overline\cdot}\), it is volume-averaged. When describing a specific cell in the geometry, indices \((i,j,k)\) are used which correspond to directions \((x,y,z)\). In most cases, the same operation is performed in all three directions. To compactly write this, an arbitrary direction set \((u,v,w)\) that corresponds to cell indices \((l,m,n)\) is used. Note that \(u\) and \(l\) do not have to correspond to \(x\) and \(i\). However, if \(u\) and \(l\) correspond to \(y\) and \(j\), \(v\) and \(w\) correspond to \(x\) and \(z\) directions. An example of this is shown in the following expression:

(1)\[\sum\limits_{u\in(x,y,z)}\left\langle\overline{J}^{u,g}_{l+1/2,m,n} \Delta_m^v\Delta_n^w\right\rangle\]

Here, \(u\) takes on each direction one at a time. The parameter \(J\) is surface area-averaged over the transverse indices \(m\) and \(n\) located at \(l+1/2\). Usually, spatial indices are listed as subscripts and the direction as a superscript. Energy group indices represented by \(g\) and \(h\) are also listed as superscripts here. The group \(g\) is the group of interest and, if present, \(h\) is all groups. Finally, any parameter surrounded by \(\left\langle\cdot\right\rangle\) represents a tally quantity that can be edited from a Monte Carlo (MC) solution.

Theory

NDA is a diffusion model that has equivalent physics to a transport model. There are many different methods that can be classified as NDA. The CMFD method is a type of NDA that represents second order multigroup diffusion equations on a coarse spatial mesh. Whether a transport model or diffusion model is used to represent the distribution of neutrons, these models must satisfy the neutron balance equation. This balance is represented by the following formula for a specific energy group \(g\) in cell \((l,m,n)\):

(2)\[\sum\limits_{u\in(x,y,z)}\left(\left\langle\overline{J}^{u,g}_{l+1/2,m,n} \Delta_m^v\Delta_n^w\right\rangle - \left\langle\overline{J}^{u,g}_{l-1/2,m,n} \Delta_m^v\Delta_n^w\right\rangle\right) + \left\langle\overline{\overline\Sigma}_{t_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle = \\ \sum\limits_{h=1}^G\left\langle \overline{\overline{\nu_s\Sigma}}_{s_{l,m,n}}^{h\rightarrow g}\overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w \right\rangle + \frac{1}{k_{eff}}\sum\limits_{h=1}^G \left\langle\overline{\overline{\nu_f\Sigma}}_{f_{l,m,n}}^{h\rightarrow g}\overline{\overline\phi}_{l,m,n}^h \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle.\]

In eq. (2) the parameters are defined as:

  • \(\left\langle\overline{J}^{u,g}_{l\pm 1/2,m,n}\Delta_m^v\Delta_n^w\right\rangle\) — surface area-integrated net current over surface \((l\pm 1/2,m,n)\) with surface normal in direction \(u\) in energy group \(g\). By dividing this quantity by the transverse area, \(\Delta_m^v\Delta_n^w\), the surface area-averaged net current can be computed.
  • \(\left\langle\overline{\overline\Sigma}_{t_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) — volume-integrated total reaction rate over energy group \(g\).
  • \(\left\langle\overline{\overline{\nu_s\Sigma}}_{s_{l,m,n}}^{h\rightarrow g} \overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) — volume-integrated scattering production rate of neutrons that begin with energy in group \(h\) and exit reaction in group \(g\). This reaction rate also includes the energy transfer of reactions (except fission) that produce multiple neutrons such as (n, 2n); hence, the need for \(\nu_s\) to represent neutron multiplicity.
  • \(k_{eff}\) — core multiplication factor.
  • \(\left\langle\overline{\overline{\nu_f\Sigma}}_{f_{l,m,n}}^{h\rightarrow g}\overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) — volume-integrated fission production rate of neutrons from fissions in group \(h\) that exit in group \(g\).

Each quantity in \(\left\langle\cdot\right\rangle\) represents a scalar value that is obtained from an MC tally. A good verification step when using an MC code is to make sure that tallies satisfy this balance equation within statistics. No NDA acceleration can be performed if the balance equation is not satisfied.

There are three major steps to consider when performing NDA: (1) calculation of macroscopic cross sections and nonlinear parameters, (2) solving an eigenvalue problem with a system of linear equations, and (3) modifying MC source distribution to align with the NDA solution on a chosen mesh. This process is illustrated as a flow chart below. After a batch of neutrons is simulated, NDA can take place. Each of the steps described above is described in detail in the following sections.

_images/cmfd_flow.png

Figure 1: Flow chart of NDA process. Note “XS” is used for cross section and “DC” is used for diffusion coefficient.

Calculation of Macroscopic Cross Sections

A diffusion model needs macroscopic cross sections and diffusion coefficients to solve for multigroup fluxes. Cross sections are derived by conserving reaction rates predicted by MC tallies. From Eq. (2), total, scattering production and fission production macroscopic cross sections are needed. They are defined from MC tallies as follows:

(3)\[\overline{\overline\Sigma}_{t_{l,m,n}}^g \equiv \frac{\left\langle\overline{\overline\Sigma}_{t_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle} {\left\langle\overline{\overline\phi}_{l,m,n}^g \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle},\]
(4)\[\overline{\overline{\nu_s\Sigma}}_{s_{l,m,n}}^{h\rightarrow g} \equiv \frac{\left\langle\overline{\overline{\nu_s\Sigma}}_{s_{l,m,n}}^{h\rightarrow g}\overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle} {\left\langle\overline{\overline\phi}_{l,m,n}^h \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle}\]

and

(5)\[\overline{\overline{\nu_f\Sigma}}_{f_{l,m,n}}^{h\rightarrow g} \equiv \frac{\left\langle\overline{\overline{\nu_f\Sigma}}_{f_{l,m,n}}^{h\rightarrow g}\overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle} {\left\langle\overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle}.\]

In order to fully conserve neutron balance, leakage rates also need to be preserved. In standard diffusion theory, leakage rates are represented by diffusion coefficients. Unfortunately, it is not easy in MC to calculate a single diffusion coefficient for a cell that describes leakage out of each surface. Luckily, it does not matter what definition of diffusion coefficient is used because nonlinear equivalence parameters will correct for this inconsistency. However, depending on the diffusion coefficient definition chosen, different convergence properties of NDA equations are observed. Here, we introduce a diffusion coefficient that is derived for a coarse energy transport reaction rate. This definition can easily be constructed from MC tallies provided that angular moments of scattering reaction rates can be obtained. The diffusion coefficient is defined as follows:

(6)\[ \overline{\overline D}_{l,m,n}^g = \frac{\left\langle\overline{\overline\phi}_{l,m,n}^g \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle}{3 \left\langle\overline{\overline\Sigma}_{tr_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle},\]

where

(7)\[\left\langle\overline{\overline\Sigma}_{tr_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle = \left\langle\overline{\overline\Sigma}_{t_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle \\ - \left\langle\overline{\overline{\nu_s\Sigma}}_{s1_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle.\]

Note that the transport reaction rate is calculated from the total reaction rate reduced by the \(P_1\) scattering production reaction rate. Equation (6) does not represent the best definition of diffusion coefficients from MC; however, it is very simple and usually fits into MC tally frameworks easily. Different methods to calculate more accurate diffusion coefficients can found in [Herman].

CMFD Equations

The first part of this section is devoted to discussing second-order finite volume discretization of multigroup diffusion equations. This will be followed up by the formulation of CMFD equations that are used in this NDA scheme. When performing second-order finite volume discretization of the diffusion equation, we need information that relates current to flux. In this numerical scheme, each cell is coupled only to its direct neighbors. Therefore, only two types of coupling exist: (1) cell-to-cell coupling and (2) cell-to-boundary coupling. The derivation of this procedure is referred to as finite difference diffusion equations and can be found in literature such as [Hebert]. These current/flux relationships are as follows:

  • cell-to-cell coupling
(8)\[\overline{J}^{u,g}_{l\pm1/2,m,n} = -\frac{2\overline{\overline D}_{l\pm1,m,n}^g\overline{\overline D}_{l,m,n}^g}{\overline{\overline D}_{l\pm1,m,n}^g\Delta_l^u + \overline{\overline D}_{l,m,n}^g\Delta_{l\pm1}^u} \left(\pm\overline{\overline{\phi}}_{l\pm1,m,n}^g\mp \overline{\overline{\phi}}_{l,m,n}^g\right),\]
  • cell-to-boundary coupling
(9)\[\overline{J}^{u,g}_{l\pm1/2,m,n} = \pm\frac{2\overline{\overline D}_{l,m,n}^g\left(1 - \beta_{l\pm1/2,m,n}^{u,g}\right)}{4\overline{\overline D}_{l,m,n}^g\left(1 + \beta_{l\pm1/2,m,n}^{u,g}\right) + \left(1 - \beta_{l\pm1/2,m,n}^{u,g}\right)\Delta_l^u}\overline{\overline{\phi}}_{l,m,n}^{g}.\]

In Eqs. (8) and (9), the \(\pm\) refers to left (\(-x\)) or right (\(+x\)) surface in the \(x\) direction, back (\(-y\)) or front (\(+y\)) surface in the \(y\) direction and bottom (\(-z\)) or top (\(+z\)) surface in the \(z\) direction. For cell-to-boundary coupling, a general albedo, \(\beta_{l\pm1/2,m,n}^{u,g}\), is used. The albedo is defined as the ratio of incoming (\(-\) superscript) to outgoing (\(+\) superscript) partial current on any surface represented as

(10)\[\beta_{l\pm1/2,m,n}^{u,g} = \frac{\overline{J}^{u,g-}_{l\pm1/2,m,n}}{\overline{J}^{u,g+}_{l\pm1/2,m,n}}.\]

Common boundary conditions are: vacuum (\(\beta=0\)), reflective (\(\beta=1\)) and zero flux (\(\beta=-1\)). Both eq. (8) and eq. (9) can be written in this generic form,

(11)\[\overline{J}^{u,g}_{l\pm1/2,m,n} = \widetilde{D}_{l,m,n}^{u,g} \left(\dots\right).\]

The parameter \(\widetilde{D}_{l,m,n}^{u,g}\) represents the linear coupling term between current and flux. These current relationships can be sustituted into eq. (2) to produce a linear system of multigroup diffusion equations for each spatial cell and energy group. However, a solution to these equations is not consistent with a higher order transport solution unless equivalence factors are present. This is because both the diffusion approximation, governed by Fick’s Law, and spatial trunction error will produce differences. Therefore, a nonlinear parameter, \(\widehat{D}_{l,m,n}^{u,g}\), is added to eqs. (8) and (9). These equations are, respectively,

(12)\[\overline{J}^{u,g}_{l\pm1/2,m,n} = -\widetilde{D}_{l,m,n}^{u,g} \left(\pm\overline{\overline{\phi}}_{l\pm1,m,n}^g\mp \overline{\overline{\phi}}_{l,m,n}^g\right) + \widehat{D}_{l,m,n}^{u,g} \left(\overline{\overline{\phi}}_{l\pm1,m,n}^g + \overline{\overline{\phi}}_{l,m,n}^g\right)\]

and

(13)\[\overline{J}^{u,g}_{l\pm1/2,m,n} = \pm\widetilde{D}_{l,m,n}^{u,g} \overline{\overline{\phi}}_{l,m,n}^{g} + \widehat{D}_{l,m,n}^{u,g} \overline{\overline{\phi}}_{l,m,n}^{g}.\]

The only unknown in each of these equations is the equivalence parameter. The current, linear coupling term and flux can either be obtained or derived from MC tallies. Thus, it is called nonlinear because it is dependent on the flux which is updated on the next iteration.

Equations (12) and (13) can be substituted into eq. (2) to create a linear system of equations that is consistent with transport physics. One example of this equation is written for an interior cell,

(14)\[\sum_{u\in x,y,x}\frac{1}{\Delta_l^u}\left[\left(-\tilde{D}_{l-1/2,m,n}^{u,g} - \hat{D}_{l-1/2,m,n}^{u,g}\right)\overline{\overline{\phi}}_{l-1,m,n}^g\right. \\ + \left(\tilde{D}_{l-1/2,m,n}^{u,g} + \tilde{D}_{l+1/2,m,n}^{u,g} - \hat{D}_{l-1/2,m,n}^{u,g} + \hat{D}_{l+1/2,m,n}^{u,g}\right)\overline{\overline{\phi}}_{l,m,n}^g \\ + \left. \left(-\tilde{D}_{l+1/2,m,n}^{u,g} + \hat{D}_{l+1/2,m,n}^{u,g}\right)\overline{\overline{\phi}}_{l+1,m,n}^g \right] \\ + \overline{\overline\Sigma}_{t_{l,m,n}}^g\overline{\overline{\phi}}_{l,m,n}^g - \sum\limits_{h=1}^G\overline{\overline{\nu_s\Sigma}}^{h\rightarrow g}_{s_{l,m,n}}\overline{\overline{\phi}}_{l,m,n}^h = \frac{1}{k}\sum\limits_{h=1}^G\overline{\overline{\nu_f\Sigma}}^{h\rightarrow g}_{f_{l,m,n}}\overline{\overline{\phi}}_{l,m,n}^h.\]

It should be noted that before substitution, eq. (2) was divided by the volume of the cell, \(\Delta_l^u\Delta_m^v\Delta_n^w\). Equation (14) can be represented in operator form as

(15)\[\mathbb{M}\mathbf{\Phi} = \frac{1}{k}\mathbb{F}\mathbf{\Phi},\]

where \(\mathbb{M}\) is the neutron loss matrix operator, \(\mathbb{F}\) is the neutron production matrix operator, \(\mathbf{\Phi}\) is the multigroup flux vector and \(k\) is the eigenvalue. This generalized eigenvalue problem is solved to obtain fundamental mode multigroup fluxes and eigenvalue. In order to produce consistent results with transport theory from these equations, the neutron balance equation must have been satisfied by MC tallies. The desire is that CMFD equations will produce a more accurate source than MC after each fission source generation.

CMFD Feedback

Now that a more accurate representation of the expected source distribution is estimated from CMFD, it needs to be communicated back to MC. The first step in this process is to generate a probability mass function that provides information about how probable it is for a neutron to be born in a given cell and energy group. This is represented as

(16)\[p_{l,m,n}^g = \frac{\sum_{h=1}^{G}\overline{\overline{\nu_f\Sigma}}^{h\rightarrow g}_{f_{l,m,n}}\overline{\overline{\phi}}_{l,m,n}^h\Delta_l^u\Delta_m^v \Delta_n^w}{\sum_n\sum_m\sum_l\sum_{h=1}^{G}\overline{ \overline{\nu_f\Sigma}}^{h\rightarrow g}_{f_{l,m,n}}\overline{\overline{\phi}}_{l,m,n}^h\Delta_l^u\Delta_m^v \Delta_n^w}.\]

This equation can be multiplied by the number of source neutrons to obtain an estimate of the expected number of neutrons to be born in a given cell and energy group. This distribution can be compared to the MC source distribution to generate weight adjusted factors defined as

(17)\[f_{l,m,n}^g = \frac{Np_{l,m,n}^g}{\sum\limits_s w_s};\quad s\in \left(g,l,m,n\right).\]

The MC source distribution is represented on the same coarse mesh as CMFD by summing all neutrons’ weights, \(w_s\), in a given cell and energy group. MC source weights can then be modified by this weight adjustment factor so that it matches the CMFD solution on the coarse mesh,

(18)\[w^\prime_s = w_s\times f_{l,m,n}^g;\quad s\in \left(g,l,m,n\right).\]

It should be noted that heterogeneous information about local coordinates and energy remain constant throughout this modification process.

Implementation in OpenMC

The section describes how CMFD was implemented in OpenMC. Before the simulation begins, a user sets up a CMFD input file that contains the following basic information:

  • CMFD mesh (space and energy),
  • boundary conditions at edge of mesh (albedos),
  • acceleration region (subset of mesh, optional),
  • fission source generation (FSG)/batch that CMFD should begin, and
  • whether CMFD feedback should be applied.

It should be noted that for more difficult simulations (e.g., light water reactors), there are other options available to users such as tally resetting parameters, effective down-scatter usage, tally estimator, etc. For more information please see the openmc.cmfd.CMFDRun class.

Of the options described above, the optional acceleration subset region is an uncommon feature. Because OpenMC only has a structured Cartesian mesh, mesh cells may overlay regions that don’t contain fissionable material and may be so far from the core that the neutron flux is very low. If these regions were included in the CMFD solution, bad estimates of diffusion parameters may result and affect CMFD feedback. To deal with this, a user can carve out an active acceleration region from their structured Cartesian mesh. This is illustrated in diagram below. When placing a CMFD mesh over a geometry, the boundary conditions must be known at the global edges of the mesh. If the geometry is complex like the one below, one may have to cover the whole geometry including the reactor pressure vessel because we know that there is a zero incoming current boundary condition at the outer edge of the pressure vessel. This is not viable in practice because neutrons in simulations may not reach mesh cells that are near the pressure vessel. To circumvent this, one can shrink the mesh to cover just the core region as shown in the diagram. However, one must still estimate the boundary conditions at the global boundaries, but at these locations, they are not readily known. In OpenMC, one can carve out the active core region from the entire structured Cartesian mesh. This is shown in the diagram below by the darkened region over the core. The albedo boundary conditions at the active core/reflector boundary can be tallied indirectly during the MC simulation with incoming and outgoing partial currents. This allows the user to not have to worry about neutrons producing adequate tallies in mesh cells far away from the core.

_images/meshfig.png

Figure 2: Diagram of CMFD acceleration mesh

During an MC simulation, CMFD tallies are accumulated. The basic tallies needed are listed in Table OpenMC CMFD tally list. Each tally is performed on a spatial and energy mesh basis. The surface area-integrated net current is tallied on every surface of the mesh. OpenMC tally objects are created by the CMFD code internally, and cross sections are calculated at each CMFD feedback iteration. The first CMFD iteration, controlled by the user, occurs just after tallies are communicated to the master processor. Once tallies are collapsed, cross sections, diffusion coefficients and equivalence parameters are calculated. This is performed only on the acceleration region if that option has been activated by the user. Once all diffusion parameters are calculated, CMFD matrices are formed where energy groups are the inner most iteration index. In OpenMC, compressed row storage sparse matrices are used due to the sparsity of CMFD operators. An example of this sparsity is shown for the 3-D BEAVRS model in figures 3 and 4 [BEAVRS]. These matrices represent an assembly radial mesh, 24 cell mesh in the axial direction and two energy groups. The loss matrix is 99.92% sparse and the production matrix is 99.99% sparse. Although the loss matrix looks like it is tridiagonal, it is really a seven banded matrix with a block diagonal matrix for scattering. The production matrix is a \(2\times 2\) block diagonal; however, zeros are present because no fission neutrons appear with energies in the thermal group.

OpenMC CMFD tally list
     
tally score filter
\(\left\langle\overline{\overline\phi}_{l,m,n}^g \Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) flux mesh, energy
\(\left\langle\overline{\overline\Sigma}_{t_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) total mesh, energy
\(\left\langle\overline{\overline{\nu_s\Sigma}}_{s1_{l,m,n}}^g \overline{\overline\phi}_{l,m,n}^g\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) nu-scatter-1 mesh, energy
\(\left\langle\overline{\overline{\nu_s\Sigma}}_{s_{l,m,n}}^{h\rightarrow g} \overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) nu-scatter mesh, energy, energyout
\(\left\langle\overline{\overline{\nu_f\Sigma}}_{f_{l,m,n}}^{h\rightarrow g} \overline{\overline\phi}_{l,m,n}^h\Delta_l^u\Delta_m^v\Delta_n^w\right\rangle\) nu-fission mesh, energy, energyout
\(\left\langle\overline{J}^{u,g}_{l\pm 1/2,m,n}\Delta_m^v\Delta_n^w\right\rangle\) current mesh, energy
_images/loss.png

Figure 3: Sparsity of Neutron Loss Operator

_images/prod.png

Figure 4: Sparsity of Neutron Production Operator

To solve the eigenvalue problem with these matrices, different source iteration and linear solvers can be used. The most common source iteration solver used is standard power iteration as described in [Gill]. To accelerate these source iterations, a Wielandt shift scheme can be used as discussed in [Park]. PETSc solvers were first implemented to perform the linear solution in parallel that occurs once per source iteration. When using PETSc, different types of parallel linear solvers and preconditioners can be used. By default, OpenMC uses an incomplete LU preconditioner and a GMRES Krylov solver. After some initial studies of parallelization with PETSc, it was observed that because CMFD matrices are very sparse, solution times do not scale well. An additional Gauss-Seidel linear solver with Chebyshev acceleration was added that is similar to the one used for CMFD in CASMO [Rhodes] and [Smith]. This solver was implemented with a custom section for two energy groups. Because energy group is the inner most index, a block diagonal is formed when using more than one group. For two groups, it is easy to invert this diagonal analytically inside the Gauss-Seidel iterative solver. For more than two groups, this analytic inversion can still be performed, but with more computational effort. A standard Gauss-Seidel solver is used for more than two groups.

Besides a power iteration, a Jacobian-free Newton-Krylov method was also implemented to obtain eigenvalue and multigroup fluxes as described in [Gill] and [Knoll]. This method is not the primary one used, but has gotten recent attention due to its coupling advantages to other physics such as thermal hydraulics. Once multigroup fluxes are obtained, a normalized fission source is calculated in the code using eq. (16) directly.

The next step in the process is to compute weight adjustment factors. These are calculated by taking the ratio of the expected number of neutrons from the CMFD source distribution to the current number of neutrons in each mesh. It is straightforward to compute the CMFD number of neutrons because it is the product between the total starting initial weight of neutrons and the CMFD normalized fission source distribution. To compute the number of neutrons from the current MC source, OpenMC sums the statistical weights of neutrons from the source bank on a given spatial and energy mesh. Once weight adjustment factors were calculated, each neutron’s statistical weight in the source bank was modified according to its location and energy. Examples of CMFD simulations using OpenMC can be found in [HermanThesis].

References

[BEAVRS]Nick Horelik, Bryan Herman. Benchmark for Evaluation And Verification of Reactor Simulations. Massachusetts Institute of Technology, https://crpg.mit.edu/research/beavrs , 2013.
[Gill](1, 2) Daniel F. Gill. Newton-Krylov methods for the solution of the k-eigenvalue problem in multigroup neutronics calculations. Ph.D. thesis, Pennsylvania State University, 2010.
[Hebert]Alain Hebert. Applied reactor physics. Presses Internationales Polytechnique, Montreal, 2009.
[Herman]Bryan R. Herman, Benoit Forget, Kord Smith, and Brian N. Aviles. Improved diffusion coefficients generated from Monte Carlo codes. In Proceedings of M&C 2013, Sun Valley, ID, USA, May 5 - 9, 2013.
[HermanThesis]Bryan R. Herman. Monte Carlo and Thermal Hydraulic Coupling using Low-Order Nonlinear Diffusion Acceleration. Sc.D. thesis, Massachusetts Institute of Technology, 2014.
[Knoll]D.A. Knoll, H. Park, and C. Newman. Acceleration of k-eigenvalue/criticality calculations using the Jacobian-free Newton-Krylov method. Nuclear Science and Engineering, 167:133–140, 2011.
[Park]H. Park, D.A. Knoll, and C.K. Newman. Nonlinear acceleration of transport criticality problems. Nuclear Science and Engineering, 172:52–65, 2012.
[Rhodes]Joel Rhodes and Malte Edenius. CASMO-4 — A Fuel Assembly Burnup Program. User’s Manual. Studsvik of America, ssp-09/443-u rev 0, proprietary edition, 2001.
[Smith]Kord S Smith and Joel D Rhodes III. Full-core, 2-D, LWR core calculations with CASMO-4E. In Proceedings of PHYSOR 2002, Seoul, Korea, October 7 - 10, 2002.

User’s Guide

Welcome to the OpenMC User’s Guide! This tutorial will guide you through the essential aspects of using OpenMC to perform simulations.

A Beginner’s Guide to OpenMC

What does OpenMC do?

In a nutshell, OpenMC simulates neutral particles (presently neutrons and photons) moving stochastically through an arbitrarily defined model that represents an real-world experimental setup. The experiment could be as simple as a sphere of metal or as complicated as a full-scale nuclear reactor. This is what’s known as Monte Carlo simulation. In the case of a nuclear reactor model, neutrons are especially important because they are the particles that induce fission in isotopes of uranium and other elements. Knowing the behavior of neutrons allows one to determine how often and where fission occurs. The amount of energy released is then directly proportional to the fission reaction rate since most heat is produced by fission. By simulating many neutrons (millions or billions), it is possible to determine the average behavior of these neutrons (or the behavior of the energy produced, or any other quantity one is interested in) very accurately.

Using Monte Carlo methods to determine the average behavior of various physical quantities in a system is quite different from other means of solving the same problem. The other class of methods for determining the behavior of neutrons and reactions rates is so-called deterministic methods. In these methods, the starting point is not randomly simulating particles but rather writing an equation that describes the average behavior of the particles. The equation that describes the average behavior of neutrons is called the neutron transport equation. This equation is a seven-dimensional equation (three for space, three for velocity, and one for time) and is very difficult to solve directly. For all but the simplest problems, it is necessary to make some sort of discretization. As an example, we can divide up all space into small sections which are homogeneous and then solve the equation on those small sections. After these discretizations and various approximations, one can arrive at forms that are suitable for solution on a computer. Among these are discrete ordinates, method of characteristics, finite-difference diffusion, and nodal methods.

So why choose Monte Carlo over deterministic methods? Each method has its pros and cons. Let us first take a look at few of the salient pros and cons of deterministic methods:

  • Pro: Depending on what method is used, solution can be determined very quickly.
  • Pro: The solution is a global solution, i.e. we know the average behavior everywhere.
  • Pro: Once the problem is converged, the solution is known.
  • Con: If the model is complex, it is necessary to do sophisticated mesh generation.
  • Con: It is necessary to generate multi-group cross sections which requires knowing the solution a priori.

Now let’s look at the pros and cons of Monte Carlo methods:

  • Pro: No mesh generation is required to build geometry. By using constructive solid geometry, it’s possible to build complex models with curved surfaces.
  • Pro: Monte Carlo methods can be used with either continuous-energy or multi-group cross sections.
  • Pro: Running simulations in parallel is conceptually very simple.
  • Con: Because they rely on repeated random sampling, they are computationally very expensive.
  • Con: A simulation doesn’t automatically give you the global solution everywhere – you have to specifically ask for those quantities you want.
  • Con: Even after the problem is converged, it is necessary to simulate many particles to reduce stochastic uncertainty.

Because fewer approximations are made in solving a problem by the Monte Carlo method, it is often seen as a “gold standard” which can be used as a benchmark for a solution of the same problem by deterministic means. However, it comes at the expense of a potentially longer simulation.

How does it work?

In order to do anything, the code first needs to have a model of some problem of interest. This could be a nuclear reactor or any other physical system with fissioning material. You, as the code user, will need to describe the model so that the code can do something with it. A basic model consists of a few things:

  • A description of the geometry – the problem must be split up into regions of homogeneous material composition.
  • For each different material in the problem, a description of what nuclides are in the material and at what density.
  • Various parameters telling the code how many particles to simulate and what options to use.
  • A list of different physical quantities that the code should return at the end of the simulation. In a Monte Carlo simulation, if you don’t ask for anything, it will not give you any answers (other than a few default quantities).

What do I need to know?

If you are starting to work with OpenMC, there are a few things you should be familiar with. Whether you plan on working in Linux, macOS, or Windows, you should be comfortable working in a command line environment. There are many resources online for learning command line environments. If you are using Linux or Mac OS X (also Unix-derived), this tutorial will help you get acquainted with commonly-used commands.

To reap the full benefits of OpenMC, you should also have basic proficiency in the use of Python, as OpenMC includes a rich Python API that offers many usability improvements over dealing with raw XML input files.

OpenMC uses a version control software called git to keep track of changes to the code, document bugs and issues, and other development tasks. While you don’t necessarily have to have git installed in order to download and run OpenMC, it makes it much easier to receive updates if you do have it installed and have a basic understanding of how it works. There are a list of good git tutorials at the git documentation website. The OpenMC source code and documentation are hosted at GitHub. In order to receive updates to the code directly, submit bug reports, and perform other development tasks, you may want to sign up for a free account on GitHub. Once you have an account, you can follow these instructions on how to set up your computer for using GitHub.

If you are new to nuclear engineering, you may want to review the NRC’s Reactor Concepts Manual. This manual describes the basics of nuclear power for electricity generation, the fission process, and the overall systems in a pressurized or boiling water reactor. Another resource that is a bit more technical than the Reactor Concepts Manual but still at an elementary level is the DOE Fundamentals Handbook on Nuclear Physics and Reactor Theory Volume I and Volume II. You may also find it helpful to review the following terms:

Installation and Configuration

Installing on Linux/Mac with conda-forge

Conda is an open source package management system and environment management system for installing multiple versions of software packages and their dependencies and switching easily between them. If you have conda installed on your system, OpenMC can be installed via the conda-forge channel. First, add the conda-forge channel with:

conda config --add channels conda-forge

To list the versions of OpenMC that are available on the conda-forge channel, in your terminal window or an Anaconda Prompt run:

conda search openmc

OpenMC can then be installed with:

conda create -n openmc-env openmc

This will install OpenMC in a conda environment called openmc-env. To activate the environment, run:

conda activate openmc-env

Installing on Linux/Mac/Windows with Docker

OpenMC can be easily deployed using Docker on any Windows, Mac, or Linux system. With Docker running, execute the following command in the shell to download and run a Docker image with the most recent release of OpenMC from DockerHub:

docker run openmc/openmc:latest

This will take several minutes to run depending on your internet download speed. The command will place you in an interactive shell running in a Docker container with OpenMC installed.

Note

The docker run command supports many options for spawning containers including mounting volumes from the host filesystem, which many users will find useful.

Installing from Source using Spack

Spack is a package management tool designed to support multiple versions and configurations of software on a wide variety of platforms and environments. Please follow Spack’s setup guide to configure the Spack system.

The OpenMC Spack recipe has been configured with variants that match most options provided in the CMakeLists.txt file. To see a list of these variants and other information use:

spack info openmc

Note

It should be noted that by default OpenMC builds with -O2 -g flags which are equivalent to a CMake build type of RelwithDebInfo. In addition, MPI is OFF while OpenMP is ON.

It is recommended to install OpenMC with the Python API. Information about this Spack recipe can be found with the following command:

spack info py-openmc

Note

The only variant for the Python API is mpi.

The most basic installation of OpenMC can be accomplished by entering the following command:

Caution

When installing any Spack package, dependencies are assumed to be at configured defaults unless otherwise specfied in the specification on the command line. In the above example, assuming the default options weren’t changed in Spack’s package configuration, py-openmc will link against a non-optimized non-MPI openmc. Even if an optimized openmc was built separately, it will rebuild openmc with optimization OFF. Thus, if you are trying to link against dependencies that were configured different than defaults, ^openmc[variants] will have to be present in the command.

For a more performant build of OpenMC with optimization turned ON and MPI provided by OpenMPI, the following command can be used:

spack install py-openmc+mpi ^openmc+optimize ^openmpi

Note

+mpi is automatically forwarded to OpenMC.

Tip

When installing py-openmc, it will use Spack’s preferred Python. For example, assuming Spack’s preferred Python is 3.8.7, to build py-openmc against the latest Python 3.7 instead, ^python@3.7.0:3.7.99 should be added to the specification on the command line. Additionally, a compiler type and version can be specified at the end of the command using %gcc@<version>, %intel@<version>, etc.

A useful tool in Spack is to look at the dependency tree before installation. This can be observed using Spack’s spec tool:

Once installed, environment/lmod modules can be generated or Spack’s load feature can be used to access the installed packages.

Installing from Source

Prerequisites

Required

  • A C/C++ compiler such as gcc

    OpenMC’s core codebase is written in C++. The source files have been tested to work with a wide variety of compilers. If you are using a Debian-based distribution, you can install the g++ compiler using the following command:

    sudo apt install g++
    
  • CMake cross-platform build system

    The compiling and linking of source files is handled by CMake in a platform-independent manner. If you are using Debian or a Debian derivative such as Ubuntu, you can install CMake using the following command:

    sudo apt install cmake
    
  • HDF5 Library for portable binary output format

    OpenMC uses HDF5 for many input/output files. As such, you will need to have HDF5 installed on your computer. The installed version will need to have been compiled with the same compiler you intend to compile OpenMC with. If compiling with gcc from the APT repositories, users of Debian derivatives can install HDF5 and/or parallel HDF5 through the package manager:

    sudo apt install libhdf5-dev
    

    Parallel versions of the HDF5 library called libhdf5-mpich-dev and libhdf5-openmpi-dev exist which are built against MPICH and OpenMPI, respectively. To link against a parallel HDF5 library, make sure to set the HDF5_PREFER_PARALLEL CMake option, e.g.:

    CXX=mpicxx.mpich cmake -DHDF5_PREFER_PARALLEL=on ..
    

    Note that the exact package names may vary depending on your particular distribution and version.

    If you are using building HDF5 from source in conjunction with MPI, we recommend that your HDF5 installation be built with parallel I/O features. An example of configuring HDF5 is listed below:

    CC=mpicc ./configure --enable-parallel
    

    You may omit --enable-parallel if you want to compile HDF5 in serial.

Optional

  • An MPI implementation for distributed-memory parallel runs

    To compile with support for parallel runs on a distributed-memory architecture, you will need to have a valid implementation of MPI installed on your machine. The code has been tested and is known to work with the latest versions of both OpenMPI and MPICH. OpenMPI and/or MPICH can be installed on Debian derivatives with:

    sudo apt install mpich libmpich-dev
    sudo apt install openmpi-bin libopenmpi-dev
    
  • DAGMC toolkit for simulation using CAD-based geometries

    OpenMC supports particle tracking in CAD-based geometries via the Direct Accelerated Geometry Monte Carlo (DAGMC) toolkit (installation instructions). For use in OpenMC, only the MOAB_DIR and BUILD_TALLY variables need to be specified in the CMake configuration step.

  • git version control software for obtaining source code

Obtaining the Source

All OpenMC source code is hosted on GitHub. You can download the source code directly from GitHub or, if you have the git version control software installed on your computer, you can use git to obtain the source code. The latter method has the benefit that it is easy to receive updates directly from the GitHub repository. GitHub has a good set of instructions for how to set up git to work with GitHub since this involves setting up ssh keys. With git installed and setup, the following command will download the full source code from the GitHub repository:

git clone --recurse-submodules https://github.com/openmc-dev/openmc.git

By default, the cloned repository will be set to the development branch. To switch to the source of the latest stable release, run the following commands:

cd openmc
git checkout master
Build Configuration

Compiling OpenMC with CMake is carried out in two steps. First, cmake is run to determine the compiler, whether optional packages (MPI, HDF5) are available, to generate a list of dependencies between source files so that they may be compiled in the correct order, and to generate a normal Makefile. The Makefile is then used by make to actually carry out the compile and linking commands. A typical out-of-source build would thus look something like the following

mkdir build && cd build
cmake ..
make

Note that first a build directory is created as a subdirectory of the source directory. The Makefile in the top-level directory will automatically perform an out-of-source build with default options.

CMakeLists.txt Options

The following options are available in the CMakeLists.txt file:

debug
Enables debugging when compiling. The flags added are dependent on which compiler is used.
profile
Enables profiling using the GNU profiler, gprof.
optimize
Enables high-optimization using compiler-dependent flags. For gcc and Intel C++, this compiles with -O3.
openmp
Enables shared-memory parallelism using the OpenMP API. The C++ compiler being used must support OpenMP. (Default: on)
dagmc
Enables use of CAD-based DAGMC geometries. Please see the note about DAGMC in the optional dependencies list for more information on this feature. The installation directory for DAGMC should also be defined as DAGMC_ROOT in the CMake configuration command. (Default: off)
coverage
Compile and link code instrumented for coverage analysis. This is typically used in conjunction with gcov.

To set any of these options (e.g. turning on debug mode), the following form should be used:

cmake -Ddebug=on /path/to/openmc
Compiling with MPI

To compile with MPI, set the CXX environment variable to the path to the MPI C++ wrapper. For example, in a bash shell:

export CXX=mpicxx
cmake /path/to/openmc

Note that in many shells, environment variables can be set for a single command, i.e.

CXX=mpicxx cmake /path/to/openmc
Selecting HDF5 Installation

CMakeLists.txt searches for the h5cc or h5pcc HDF5 C wrapper on your PATH environment variable and subsequently uses it to determine library locations and compile flags. If you have multiple installations of HDF5 or one that does not appear on your PATH, you can set the HDF5_ROOT environment variable to the root directory of the HDF5 installation, e.g.

export HDF5_ROOT=/opt/hdf5/1.8.15
cmake /path/to/openmc

This will cause CMake to search first in /opt/hdf5/1.8.15/bin for h5cc / h5pcc before it searches elsewhere. As noted above, an environment variable can typically be set for a single command, i.e.

HDF5_ROOT=/opt/hdf5/1.8.15 cmake /path/to/openmc
Compiling on Linux and Mac OS X

To compile OpenMC on Linux or Max OS X, run the following commands from within the root directory of the source code:

mkdir build && cd build
cmake ..
make
make install

This will build an executable named openmc and install it (by default in /usr/local/bin). If you do not have administrative privileges, you can install OpenMC locally by specifying an install prefix when running cmake:

cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local ..

The CMAKE_INSTALL_PREFIX variable can be changed to any path for which you have write-access.

Compiling on Windows 10

Recent versions of Windows 10 include a subsystem for Linux that allows one to run Bash within Ubuntu running in Windows. First, follow the installation guide here to get Bash on Ubuntu on Windows setup. Once you are within bash, obtain the necessary prerequisites via apt. Finally, follow the instructions for compiling on linux.

Testing Build

To run the test suite, you will first need to download a pre-generated cross section library along with windowed multipole data. Please refer to our Test Suite documentation for further details.

Installing Python API

If you installed OpenMC using Conda, no further steps are necessary in order to use OpenMC’s Python API. However, if you are installing from source, the Python API is not installed by default when make install is run because in many situations it doesn’t make sense to install a Python package in the same location as the openmc executable (for example, if you are installing the package into a virtual environment). The easiest way to install the openmc Python package is to use pip, which is included by default in Python 3.4+. From the root directory of the OpenMC distribution/repository, run:

pip install .

pip will first check that all required third-party packages have been installed, and if they are not present, they will be installed by downloading the appropriate packages from the Python Package Index (PyPI). However, do note that since pip runs the setup.py script which requires NumPy, you will have to first install NumPy:

pip install numpy
Installing in “Development” Mode

If you are primarily doing development with OpenMC, it is strongly recommended to install the Python package in “editable” mode.

Prerequisites

The Python API works with Python 3.5+. In addition to Python itself, the API relies on a number of third-party packages. All prerequisites can be installed using Conda (recommended), pip, or through the package manager in most Linux distributions.

Required

NumPy
NumPy is used extensively within the Python API for its powerful N-dimensional array.
SciPy
SciPy’s special functions, sparse matrices, and spatial data structures are used for several optional features in the API.
pandas
Pandas is used to generate tally DataFrames as demonstrated in an example notebook.
h5py
h5py provides Python bindings to the HDF5 library. Since OpenMC outputs various HDF5 files, h5py is needed to provide access to data within these files from Python.
Matplotlib
Matplotlib is used to providing plotting functionality in the API like the Universe.plot() method and the openmc.plot_xs() function.
uncertainties
Uncertainties are used for decay data in the openmc.data module.
lxml
lxml is used for the openmc-validate-xml script and various other parts of the Python API.

Optional

mpi4py
mpi4py provides Python bindings to MPI for running distributed-memory parallel runs. This package is needed if you plan on running depletion simulations in parallel using MPI.
Cython
Cython is used for resonance reconstruction for ENDF data converted to openmc.data.IncidentNeutron.
vtk
The Python VTK bindings are needed to convert voxel and track files to VTK format.
pytest
The pytest framework is used for unit testing the Python API.

If you are running simulations that require OpenMC’s Python bindings to the C API (including depletion and CMFD), it is recommended to build h5py (and mpi4py, if you are using MPI) using the same compilers and HDF5 version as for OpenMC. Thus, the install process would proceed as follows:

mkdir build && cd build
HDF5_ROOT=<path to HDF5> CXX=<path to mpicxx> cmake ..
make
make install

cd ..
MPICC=<path to mpicc> pip install mpi4py
HDF5_DIR=<path to HDF5> pip install --no-binary=h5py h5py

If you are using parallel HDF5, you’ll also need to make sure the right MPI wrapper is used when installing h5py:

CC=<path to mpicc> HDF5_MPI=ON HDF5_DIR=<path to HDF5> pip install --no-binary=h5py h5py

Configuring Input Validation with GNU Emacs nXML mode

The GNU Emacs text editor has a built-in mode that extends functionality for editing XML files. One of the features in nXML mode is the ability to perform real-time validation of XML files against a RELAX NG schema. The OpenMC source contains RELAX NG schemas for each type of user input file. In order for nXML mode to know about these schemas, you need to tell emacs where to find a “locating files” description. Adding the following lines to your ~/.emacs file will enable real-time validation of XML input files:

(require 'rng-loc)
(add-to-list 'rng-schema-locating-files "~/openmc/schemas.xml")

Make sure to replace the last string on the second line with the path to the schemas.xml file in your own OpenMC source directory.

Cross Section Configuration

In order to run a simulation with OpenMC, you will need cross section data for each nuclide or material in your problem. OpenMC can be run in continuous-energy or multi-group mode.

In continuous-energy mode, OpenMC uses a native HDF5 format (see Nuclear Data File Formats) to store all nuclear data. Pregenerated HDF5 libraries can be found at https://openmc.org; unless you have specific data needs, it is highly recommended to use one of the pregenerated libraries. Alternatively, if you have ACE format data that was produced with NJOY, such as that distributed with MCNP or Serpent, it can be converted to the HDF5 format using the using the Python API. Several sources provide openly available ACE data including the ENDF/B, JEFF, and TENDL libraries as well as the LANL Nuclear Data Team. In addition to tabulated cross sections in the HDF5 files, OpenMC relies on windowed multipole data to perform on-the-fly Doppler broadening.

In multi-group mode, OpenMC utilizes an HDF5-based library format which can be used to describe nuclide- or material-specific quantities.

Environment Variables

When openmc is run, it will look for several environment variables that indicate where cross sections can be found. While the location of cross sections can also be indicated through the openmc.Materials.cross_sections attribute (or in the materials.xml file), if you always use the same set of cross section data, it is often easier to just set an environment variable that will be picked up by default every time OpenMC is run. The following environment variables are used:

OPENMC_CROSS_SECTIONS
Indicates the path to the cross_sections.xml summary file that is used to locate HDF5 format cross section libraries if the user has not specified openmc.Materials.cross_sections (equivalently, the <cross_sections> Element in materials.xml).
OPENMC_MG_CROSS_SECTIONS
Indicates the path to an HDF5 file that contains multi-group cross sections if the user has not specified openmc.Materials.cross_sections (equivalently, the <cross_sections> Element in materials.xml).

To set these environment variables persistently, export them from your shell profile (.profile or .bashrc in bash).

Continuous-Energy Cross Sections

Using Pregenerated Libraries

Various evaluated nuclear data libraries have been processed into the HDF5 format required by OpenMC and can be found at https://openmc.org. You can find both libraries generated by the OpenMC development team as well as libraries based on ACE files distributed elsewhere. To use these libraries, download the archive file, unpack it, and then set your OPENMC_CROSS_SECTIONS environment variable to the absolute path of the cross_sections.xml file contained in the unpacked directory.

Manually Creating a Library from ACE files

The openmc.data module in the Python API enables users to directly convert ACE data to OpenMC’s HDF5 format and create a corresponding cross_sections.xml file. For those who prefer to use the API directly, the openmc.data.IncidentNeutron and openmc.data.ThermalScattering classes can be used to read ACE data and convert it to HDF5. For continuous-energy incident neutron data, use the IncidentNeutron.from_ace() class method to read in an existing ACE file and the IncidentNeutron.export_to_hdf5() method to write the data to an HDF5 file.

u235 = openmc.data.IncidentNeutron.from_ace('92235.710nc')
u235.export_to_hdf5('U235.h5')

If you have multiple ACE files for the same nuclide at different temperatures, you can use the IncidentNeutron.add_temperature_from_ace() method to append cross sections to an existing IncidentNeutron instance:

u235 = openmc.data.IncidentNeutron.from_ace('92235.710nc')
for suffix in [711, 712, 713, 714, 715, 716]:
    u235.add_temperature_from_ace('92235.{}nc'.format(suffix))
u235.export_to_hdf5('U235.h5')

Similar methods exist for thermal scattering data:

light_water = openmc.data.ThermalScattering.from_ace('lwtr.20t')
for suffix in range(21, 28):
    light_water.add_temperature_from_ace('lwtr.{}t'.format(suffix))
light_water.export_to_hdf5('lwtr.h5')

Once you have created corresponding HDF5 files for each of your ACE files, you can create a library and export it to XML using the openmc.data.DataLibrary class:

library = openmc.data.DataLibrary()
library.register_file('U235.h5')
library.register_file('lwtr.h5')
...
library.export_to_xml()

At this point, you will have a cross_sections.xml file that you can use in OpenMC.

Hint

The IncidentNeutron class allows you to view/modify cross sections, secondary angle/energy distributions, probability tables, etc. For a more thorough overview of the capabilities of this class, see the example notebook.

Manually Creating a Library from ENDF files

If you need to create a nuclear data library and you do not already have suitable ACE files or you need to further customize the data (for example, adding more temperatures), the IncidentNeutron.from_njoy() and ThermalScattering.from_njoy() methods can be used to create data instances by directly running NJOY. Both methods require that you pass the name of ENDF file(s) that are passed on to NJOY. For example, to generate data for Zr-92:

zr92 = openmc.data.IncidentNeutron.from_njoy('n-040_Zr_092.endf')

By default, data is produced at room temperature, 293.6 K. You can also specify a list of temperatures that you want data at:

zr92 = openmc.data.IncidentNeutron.from_njoy(
    'n-040_Zr_092.endf', temperatures=[300., 600., 1000.])

The IncidentNeutron.from_njoy() method assumes you have an executable named njoy available on your path. If you want to explicitly name the executable, the njoy_exec optional argument can be used. Additionally, the stdout argument can be used to show the progress of the NJOY run.

To generate a thermal scattering file, you need to specify both an ENDF incident neutron sub-library file as well as a thermal neutron scattering sub-library file; for example:

light_water = openmc.data.ThermalScattering.from_njoy(
    'neutrons/n-001_H_001.endf', 'thermal_scatt/tsl-HinH2O.endf')

Once you have instances of IncidentNeutron and ThermalScattering, a library can be created by using the export_to_hdf5() methods and the DataLibrary class as described in Manually Creating a Library from ACE files.

Enabling Resonance Scattering Treatments

In order for OpenMC to correctly treat elastic scattering in heavy nuclides where low-lying resonances might be present (see Energy-Dependent Cross Section Model), the elastic scattering cross section at 0 K must be present. If the data you are using was generated via IncidentNeutron.from_njoy(), you will already have 0 K elastic scattering cross sections available. Otherwise, to add 0 K elastic scattering cross sections to an existing IncidentNeutron instance, you can use the IncidentNeutron.add_elastic_0K_from_endf() method which requires an ENDF file for the nuclide you are modifying:

u238 = openmc.data.IncidentNeutron.from_hdf5('U238.h5')
u238.add_elastic_0K_from_endf('n-092_U_238.endf')
u238.export_to_hdf5('U238_with_0K.h5')

With 0 K elastic scattering data present, you can turn on a resonance scattering method using Settings.resonance_scattering.

Note

The process of reconstructing resonances and generating tabulated 0 K cross sections can be computationally expensive, especially for nuclides like U-238 where thousands of resonances are present. Thus, running the IncidentNeutron.add_elastic_0K_from_endf() method may take several minutes to complete.

Photon Cross Sections

Photon interaction data is needed to run OpenMC with photon transport enabled. Some of this data, namely bremsstrahlung cross sections from Seltzer and Berger, mean excitation energy from the NIST ESTAR database, and Compton profiles calculated by Biggs et al. and available in the Geant4 G4EMLOW data file, is distributed with OpenMC. The rest is available from the NNDC, which provides ENDF data from the photo-atomic and atomic relaxation sublibraries of the ENDF/B-VII.1 library.

Most of the pregenerated HDF5 libraries available at https://openmc.org already have photon interaction data included. If you are building a data library yourself, it is possible to use the Python API directly to convert photon interaction data from an ENDF or ACE file to an HDF5 file. The openmc.data.IncidentPhoton class contains an IncidentPhoton.from_ace() method that will generate photon data from an ACE table and an IncidentPhoton.export_to_hdf5() method that writes the data to an HDF5 file:

u = openmc.data.IncidentPhoton.from_ace('92000.12p')
u.export_to_hdf5('U.h5')

Similarly, the IncidentPhoton.from_endf() method can be used to read photon data from an ENDF file. In this case, both the photo-atomic and atomic relaxation sublibrary files are required:

u = openmc.data.IncidentPhoton.from_endf('photoat-092_U_000.endf',
                                         'atom-092_U_000.endf')

Once the HDF5 files have been generated, a library can be created using the DataLibrary class as described in Manually Creating a Library from ACE files.

Windowed Multipole Data

OpenMC is capable of using windowed multipole data for on-the-fly Doppler broadening. A comprehensive multipole data library containing all nuclides in ENDF/B-VII.1 is available on GitHub. To obtain this library, download and unpack an archive (.zip or .tag.gz) from GitHub. Once unpacked, you can use the openmc.data.DataLibrary class to register the .h5 files as described in Manually Creating a Library from ACE files.

The official ENDF/B-VII.1 HDF5 library includes the windowed multipole library, so if you are using this library, the windowed multipole data will already be available to you.

Multi-Group Cross Sections

Multi-group cross section libraries are generally tailored to the specific calculation to be performed. Therefore, at this point in time, OpenMC is not distributed with any pre-existing multi-group cross section libraries. However, if obtained or generated their own library, the user should set the OPENMC_MG_CROSS_SECTIONS environment variable to the absolute path of the file library expected to used most frequently.

For an example of how to create a multi-group library, see the example notebook.

Basics of Using OpenMC

Running a Model

When you build and install OpenMC, you will have an openmc executable on your system. When you run openmc, the first thing it will do is look for a set of XML files that describe the model you want to simulate. Three of these files are required and another three are optional, as described below.

Required

Materials Specification – materials.xml
This file describes what materials are present in the problem and what they are composed of. Additionally, it indicates where OpenMC should look for a cross section library.
Geometry Specification – geometry.xml
This file describes how the materials defined in materials.xml occupy regions of space. Physical volumes are defined using constructive solid geometry, described in detail in Defining Geometry.
Settings Specification – settings.xml
This file indicates what mode OpenMC should be run in, how many particles to simulate, the source definition, and a whole host of miscellaneous options.

Optional

Tallies Specification – tallies.xml
This file describes what physical quantities should be tallied during the simulation (fluxes, reaction rates, currents, etc.).
Geometry Plotting Specification – plots.xml
This file gives specifications for producing slice or voxel plots of the geometry.

Warning

OpenMC models should be treated as code, and it is important to be careful with code from untrusted sources.

eXtensible Markup Language (XML)

Unlike many other Monte Carlo codes which use an arbitrary-format ASCII file with “cards” to specify a particular geometry, materials, and associated run settings, the input files for OpenMC are structured in a set of XML files. XML, which stands for eXtensible Markup Language, is a simple format that allows data to be exchanged efficiently between different programs and interfaces.

Anyone who has ever seen webpages written in HTML will be familiar with the structure of XML whereby “tags” enclosed in angle brackets denote that a particular piece of data will follow. Let us examine the follow example:

<person>
  <firstname>John</firstname>
  <lastname>Smith</lastname>
  <age>27</age>
  <occupation>Health Physicist</occupation>
</person>

Here we see that the first tag indicates that the following data will describe a person. The nested tags firstname, lastname, age, and occupation indicate characteristics about the person being described.

In much the same way, OpenMC input uses XML tags to describe the geometry, the materials, and settings for a Monte Carlo simulation. Note that because the XML files have a well-defined structure, they can be validated using the openmc-validate-xml script or using Emacs nXML mode.

Creating Input Files

The most rudimentary option for creating input files is to simply write them from scratch using the XML format specifications. This approach will feel familiar to users of other Monte Carlo codes such as MCNP and Serpent, with the added bonus that the XML formats feel much more “readable”. Alternatively, input files can be generated using OpenMC’s Python API, which is introduced in the following section.

Python API

OpenMC’s Python API defines a set of functions and classes that roughly correspond to elements in the XML files. For example, the openmc.Cell Python class directly corresponds to the <cell> Element in XML. Each XML file itself also has a corresponding class: openmc.Geometry for geometry.xml, openmc.Materials for materials.xml, openmc.Settings for settings.xml, and so on. To create a model then, one creates instances of these classes and then uses the export_to_xml() method, e.g., Geometry.export_to_xml(). Most scripts that generate a full model will look something like the following:

# Create materials
materials = openmc.Materials()
...
materials.export_to_xml()

# Create geometry
geometry = openmc.Geometry()
...
geometry.export_to_xml()

# Assign simulation settings
settings = openmc.Settings()
...
settings.export_to_xml()

Once a model has been created and exported to XML, a simulation can be run either by calling openmc directly from a shell or by using the openmc.run() function from Python.

Identifying Objects

In the XML user input files, each object (cell, surface, tally, etc.) has to be uniquely identified by a positive integer (ID) in the same manner as MCNP and Serpent. In the Python API, integer IDs can be assigned but it is not strictly required. When IDs are not explicitly assigned to instances of the OpenMC Python classes, they will be automatically assigned.

Viewing and Analyzing Results

After a simulation has been completed by running openmc, you will have several output files that were created:

tallies.out
An ASCII file showing the mean and standard deviation of the mean for any user-defined tallies.
summary.h5
An HDF5 file with a complete description of the geometry and materials used in the simulation.
statepoint.#.h5
An HDF5 file with the complete results of the simulation, including tallies as well as the final source distribution. This file can be used both to view/analyze results as well as restart a simulation if desired.

For a simple simulation with few tallies, looking at the tallies.out file might be sufficient. For anything more complicated (plotting results, finding a subset of results, etc.), you will likely find it easier to work with the statepoint file directly using the openmc.StatePoint class. For more details on working with statepoints, see Working with State Points.

Physical Units

Unless specified otherwise, all length quantities are assumed to be in units of centimeters, all energy quantities are assumed to be in electronvolts, and all time quantities are assumed to be in seconds.

Measure Default unit Symbol
length centimeter cm
energy electronvolt eV
time second s

ERSN-OpenMC Graphical User Interface

A third-party Java-based user-friendly graphical user interface for creating XML input files called ERSN-OpenMC is developed and maintained by members of the Radiation and Nuclear Systems Group at the Faculty of Sciences Tetouan, Morocco. The GUI also allows one to automatically download prerequisites for installing and running OpenMC.

Material Compositions

Materials in OpenMC are defined as a set of nuclides/elements at specified densities and are created using the openmc.Material class. Once a material has been instantiated, nuclides can be added with Material.add_nuclide() and elements can be added with Material.add_element(). Densities can be specified using atom fractions or weight fractions. For example, to create a material and add Gd152 at 0.5 atom percent, you’d run:

mat = openmc.Material()
mat.add_nuclide('Gd152', 0.5, 'ao')

The third argument to Material.add_nuclide() can also be ‘wo’ for weight percent. The densities specified for each nuclide/element are relative and are renormalized based on the total density of the material. The total density is set using the Material.set_density() method. The density can be specified in gram per cubic centimeter (‘g/cm3’), atom per barn-cm (‘atom/b-cm’), or kilogram per cubic meter (‘kg/m3’), e.g.,

mat.set_density('g/cm3', 4.5)

Natural Elements

The Material.add_element() method works exactly the same as Material.add_nuclide(), except that instead of specifying a single isotope of an element, you specify the element itself. For example,

mat.add_element('C', 1.0)

This method can also accept case-insensitive element names such as

mat.add_element('aluminium', 1.0)

Internally, OpenMC stores data on the atomic masses and natural abundances of all known isotopes and then uses this data to determine what isotopes should be added to the material. When the material is later exported to XML for use by the openmc executable, you’ll see that any natural elements were expanded to the naturally-occurring isotopes.

The Material.add_element() method can also be used to add uranium at a specified enrichment through the enrichment argument. For example, the following would add 3.2% enriched uranium to a material:

mat.add_element('U', 1.0, enrichment=3.2)

In addition to U235 and U238, concentrations of U234 and U236 will be present and are determined through a correlation based on measured data.

It is also possible to perform enrichment of any element that is composed of two naturally-occurring isotopes (e.g., Li or B) in terms of atomic percent. To invoke this, provide the additional argument enrichment_target to Material.add_element(). For example the following would enrich B10 to 30ao%:

mat.add_element('B', 1.0, enrichment=30.0, enrichment_target='B10')

In order to enrich an isotope in terms of mass percent (wo%), provide the extra argument enrichment_type. For example the following would enrich Li6 to 15wo%:

mat.add_element('Li', 1.0, enrichment=15.0, enrichment_target='Li6',
                enrichment_type='wo')

Often, cross section libraries don’t actually have all naturally-occurring isotopes for a given element. For example, in ENDF/B-VII.1, cross section evaluations are given for O16 and O17 but not for O18. If OpenMC is aware of what cross sections you will be using (through the OPENMC_CROSS_SECTIONS environment variable), it will attempt to only put isotopes in your model for which you have cross section data. In the case of oxygen in ENDF/B-VII.1, the abundance of O18 would end up being lumped with O16.

Thermal Scattering Data

If you have a moderating material in your model like water or graphite, you should assign thermal scattering data (so-called \(S(\alpha,\beta)\)) using the Material.add_s_alpha_beta() method. For example, to model light water, you would need to add hydrogen and oxygen to a material and then assign the c_H_in_H2O thermal scattering data:

water = openmc.Material()
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
water.add_s_alpha_beta('c_H_in_H2O')
water.set_density('g/cm3', 1.0)

Naming Conventions

OpenMC uses the GNDS naming convention for nuclides, metastable states, and compounds:

Nuclides:SymA where “A” is the mass number (e.g., Fe56)
Elements:Sym0 (e.g., Fe0 or C0)
Excited states:SymA_eN (e.g., V51_e1 for the first excited state of Vanadium-51.) This is only used in decay data.
Metastable states:
 SymA_mN (e.g., Am242_m1 for the first excited state of Americium-242).
Compounds:c_String_Describing_Material (e.g., c_H_in_H2O). Used for thermal scattering data.

Important

The element syntax, e.g., C0, is only used when the cross section evaluation is an elemental evaluation, like carbon in ENDF/B-VII.1! If you are adding an element via Material.add_element(), just use Sym.

Temperature

Some Monte Carlo codes define temperature implicitly through the cross section data, which is itself given only at a particular temperature. In OpenMC, the material definition is decoupled from the specification of temperature. Instead, temperatures are assigned to cells directly. Alternatively, a default temperature can be assigned to a material that is to be applied to any cell where the material is used. In the absence of any cell or material temperature specification, a global default temperature can be set that is applied to all cells and materials. Anytime a material temperature is specified, it will override the global default temperature. Similarly, anytime a cell temperatures is specified, it will override the material or global default temperature. All temperatures should be given in units of Kelvin.

To assign a default material temperature, one should use the temperature attribute, e.g.,

hot_fuel = openmc.Material()
hot_fuel.temperature = 1200.0  # temperature in Kelvin

Warning

MCNP users should be aware that OpenMC does not use the concept of cross section suffixes like “71c” or “80c”. Temperatures in Kelvin should be assigned directly per material or per cell using the Material.temperature or Cell.temperature attributes, respectively.

Material Mixtures

In OpenMC it is possible to mix any number of materials to create a new material with the correct nuclide composition and density. The Material.mix_materials() method takes a list of materials and a list of their mixing fractions. Mixing fractions can be provided as atomic fractions, weight fractions, or volume fractions. The fraction type can be specified by passing ‘ao’, ‘wo’, or ‘vo’ as the third argument, respectively. For example, assuming the required materials have already been defined, a MOX material with 3% plutonium oxide by weight could be created using the following:

mox = openmc.Material.mix_materials([uo2, puo2], [0.97, 0.03], 'wo')

It should be noted that, if mixing fractions are specifed as atomic or weight fractions, the supplied fractions should sum to one. If the fractions are specified as volume fractions, and the sum of the fractions is less than one, then the remaining fraction is set as void material.

Warning

Materials with \(S(\alpha,\beta)\) thermal scattering data cannot be used in Material.mix_materials(). However, thermal scattering data can be added to a material created by Material.mix_materials().

Material Collections

The openmc executable expects to find a materials.xml file when it is run. To create this file, one needs to instantiate the openmc.Materials class and add materials to it. The Materials class acts like a list (in fact, it is a subclass of Python’s built-in list class), so materials can be added by passing a list to the constructor, using methods like append(), or through the operator +=. Once materials have been added to the collection, it can be exported using the Materials.export_to_xml() method.

materials = openmc.Materials()
materials.append(water)
materials += [uo2, zircaloy]
materials.export_to_xml()

# This is equivalent
materials = openmc.Materials([water, uo2, zircaloy])
materials.export_to_xml()
Cross Sections

OpenMC uses a file called cross_sections.xml to indicate where cross section data can be found on the filesystem. This file serves the same role that xsdir does for MCNP or xsdata does for Serpent. Information on how to generate a cross section listing file can be found in Manually Creating a Library from ACE files. Once you have a cross sections file that has been generated, you can tell OpenMC to use this file either by setting Materials.cross_sections or by setting the OPENMC_CROSS_SECTIONS environment variable to the path of the cross_sections.xml file. The former approach would look like:

materials.cross_sections = '/path/to/cross_sections.xml'

Defining Geometry

Surfaces and Regions

The geometry of a model in OpenMC is defined using constructive solid geometry (CSG), also sometimes referred to as combinatorial geometry. CSG allows a user to create complex regions using Boolean operators (intersection, union, and complement) on simpler regions. In order to define a region that we can assign to a cell, we must first define surfaces which bound the region. A surface is a locus of zeros of a function of Cartesian coordinates \(x,y,z\), e.g.

  • A plane perpendicular to the \(x\) axis: \(x - x_0 = 0\)
  • A cylinder parallel to the \(z\) axis: \((x - x_0)^2 + (y - y_0)^2 - R^2 = 0\)
  • A sphere: \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\)

Defining a surface alone is not sufficient to specify a volume – in order to define an actual volume, one must reference the half-space of a surface. A surface half-space is the region whose points satisfy a positive or negative inequality of the surface equation. For example, for a sphere of radius one centered at the origin, the surface equation is \(f(x,y,z) = x^2 + y^2 + z^2 - 1 = 0\). Thus, we say that the negative half-space of the sphere, is defined as the collection of points satisfying \(f(x,y,z) < 0\), which one can reason is the inside of the sphere. Conversely, the positive half-space of the sphere would correspond to all points outside of the sphere, satisfying \(f(x,y,z) > 0\).

In the Python API, surfaces are created via subclasses of openmc.Surface. The available surface types and their corresponding classes are listed in the following table.

Surface types available in OpenMC.
Surface Equation Class
Plane perpendicular to \(x\)-axis \(x - x_0 = 0\) openmc.XPlane
Plane perpendicular to \(y\)-axis \(y - y_0 = 0\) openmc.YPlane
Plane perpendicular to \(z\)-axis \(z - z_0 = 0\) openmc.ZPlane
Arbitrary plane \(Ax + By + Cz = D\) openmc.Plane
Infinite cylinder parallel to \(x\)-axis \((y-y_0)^2 + (z-z_0)^2 - R^2 = 0\) openmc.XCylinder
Infinite cylinder parallel to \(y\)-axis \((x-x_0)^2 + (z-z_0)^2 - R^2 = 0\) openmc.YCylinder
Infinite cylinder parallel to \(z\)-axis \((x-x_0)^2 + (y-y_0)^2 - R^2 = 0\) openmc.ZCylinder
Sphere \((x-x_0)^2 + (y-y_0)^2 + (z-z_0)^2 - R^2 = 0\) openmc.Sphere
Cone parallel to the \(x\)-axis \((y-y_0)^2 + (z-z_0)^2 - R^2(x-x_0)^2 = 0\) openmc.XCone
Cone parallel to the \(y\)-axis \((x-x_0)^2 + (z-z_0)^2 - R^2(y-y_0)^2 = 0\) openmc.YCone
Cone parallel to the \(z\)-axis \((x-x_0)^2 + (y-y_0)^2 - R^2(z-z_0)^2 = 0\) openmc.ZCone
General quadric surface \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\) openmc.Quadric

Each surface is characterized by several parameters. As one example, the parameters for a sphere are the \(x,y,z\) coordinates of the center of the sphere and the radius of the sphere. All of these parameters can be set either as optional keyword arguments to the class constructor or via attributes:

sphere = openmc.Sphere(r=10.0)

# This is equivalent
sphere = openmc.Sphere()
sphere.r = 10.0

Once a surface has been created, half-spaces can be obtained by applying the unary - or + operators, corresponding to the negative and positive half-spaces, respectively. For example:

>>> sphere = openmc.Sphere(r=10.0)
>>> inside_sphere = -sphere
>>> outside_sphere = +sphere
>>> type(inside_sphere)
<class 'openmc.surface.Halfspace'>

Instances of openmc.Halfspace can be combined together using the Boolean operators & (intersection), | (union), and ~ (complement):

>>> inside_sphere = -openmc.Sphere()
>>> above_plane = +openmc.ZPlane()
>>> northern_hemisphere = inside_sphere & above_plane
>>> type(northern_hemisphere)
<class 'openmc.region.Intersection'>

The & operator can be thought of as a logical AND, the | operator as a logical OR, and the ~ operator as a logical NOT. Thus, if you wanted to create a region that consists of the space for which \(-4 < z < -3\) or \(3 < z < 4\), a union could be used:

>>> region_bottom = +openmc.ZPlane(-4) & -openmc.ZPlane(-3)
>>> region_top = +openmc.ZPlane(3) & -openmc.ZPlane(4)
>>> combined_region = region_bottom | region_top

Half-spaces and the objects resulting from taking the intersection, union, and/or complement or half-spaces are all considered regions that can be assigned to cells.

For many regions, a bounding-box can be determined automatically:

>>> northern_hemisphere.bounding_box
(array([-1., -1., 0.]), array([1., 1., 1.]))

While a bounding box can be determined for regions involving half-spaces of spheres, cylinders, and axis-aligned planes, it generally cannot be determined if the region involves cones, non-axis-aligned planes, or other exotic second-order surfaces. For example, the openmc.model.hexagonal_prism() function returns the interior region of a hexagonal prism; because it is bounded by a openmc.Plane, trying to get its bounding box won’t work:

>>> hex = openmc.model.hexagonal_prism()
>>> hex.bounding_box
(array([-0.8660254,       -inf,       -inf]),
 array([ 0.8660254,        inf,        inf]))
Boundary Conditions

When a surface is created, by default particles that pass through the surface will consider it to be transmissive, i.e., they pass through the surface freely. If your model does not extend to infinity in all spatial dimensions, you may want to specify different behavior for particles passing through a surface. To specify a vacuum boundary condition, simply change the Surface.boundary_type attribute to ‘vacuum’:

outer_surface = openmc.Sphere(r=100.0, boundary_type='vacuum')

# This is equivalent
outer_surface = openmc.Sphere(r=100.0)
outer_surface.boundary_type = 'vacuum'

Reflective and periodic boundary conditions can be set with the strings ‘reflective’ and ‘periodic’. Vacuum and reflective boundary conditions can be applied to any type of surface. Periodic boundary conditions can be applied to pairs of planar surfaces. If there are only two periodic surfaces they will be matched automatically. Otherwise it is necessary to specify pairs explicitly using the Surface.periodic_surface attribute as in the following example:

p1 = openmc.Plane(a=0.3, b=5.0, d=1.0, boundary_type='periodic')
p2 = openmc.Plane(a=0.3, b=5.0, d=-1.0, boundary_type='periodic')
p1.periodic_surface = p2

Both rotational and translational periodic boundary conditions are specified in the same fashion. If both planes have the same normal vector, a translational periodicity is assumed; rotational periodicity is assumed otherwise. Currently, only rotations about the \(z\)-axis are supported.

For a rotational periodic BC, the normal vectors of each surface must point inwards—towards the valid geometry. For example, a XPlane and YPlane would be valid for a 90-degree periodic rotation if the geometry lies in the first quadrant of the Cartesian grid. If the geometry instead lies in the fourth quadrant, the YPlane must be replaced by a Plane with the normal vector pointing in the \(-y\) direction.

Cells

Once you have a material created and a region of space defined, you need to define a cell that assigns the material to the region. Cells are created using the openmc.Cell class:

fuel = openmc.Cell(fill=uo2, region=pellet)

# This is equivalent
fuel = openmc.Cell()
fuel.fill = uo2
fuel.region = pellet

In this example, an instance of openmc.Material is assigned to the Cell.fill attribute. One can also fill a cell with a universe or lattice. If you provide no fill to a cell or assign a value of None, it will be treated as a “void” cell with no material within. Particles are allowed to stream through the cell but will undergo no collisions:

# This cell will be filled with void on export to XML
gap = openmc.Cell(region=pellet_gap)

The classes Halfspace, Intersection, Union, and Complement and all instances of openmc.Region and can be assigned to the Cell.region attribute.

Universes

Similar to MCNP and Serpent, OpenMC is capable of using universes, collections of cells that can be used as repeatable units of geometry. At a minimum, there must be one “root” universe present in the model. To define a universe, an instance of openmc.Universe is created and then cells can be added using the Universe.add_cells() or Universe.add_cell() methods. Alternatively, a list of cells can be specified in the constructor:

universe = openmc.Universe(cells=[cell1, cell2, cell3])

# This is equivalent
universe = openmc.Universe()
universe.add_cells([cell1, cell2])
universe.add_cell(cell3)

Universes are generally used in three ways:

  1. To be assigned to a Geometry object (see Exporting a Geometry Model),
  2. To be assigned as the fill for a cell via the Cell.fill attribute, and
  3. To be used in a regular arrangement of universes in a lattice.

Once a universe is constructed, it can actually be used to determine what cell or material is found at a given location by using the Universe.find() method, which returns a list of universes, cells, and lattices which are traversed to find a given point. The last element of that list would contain the lowest-level cell at that location:

>>> universe.find((0., 0., 0.))[-1]
Cell
        ID             =    10000
        Name           =    cell 1
        Fill           =    Material 10000
        Region         =    -10000
        Rotation       =    None
        Temperature    =    None
        Translation    =    None

As you are building a geometry, it is also possible to display a plot of single universe using the Universe.plot() method. This method requires that you have matplotlib installed.

Lattices

Many particle transport models involve repeated structures that occur in a regular pattern such as a rectangular or hexagonal lattice. In such a case, it would be cumbersome to have to define the boundaries of each of the cells to be filled with a universe. OpenMC provides a means to define lattice structures through the openmc.RectLattice and openmc.HexLattice classes.

Rectangular Lattices

A rectangular lattice defines a two-dimensional or three-dimensional array of universes that are filled into rectangular prisms (lattice elements) each of which has the same width, length, and height. To completely define a rectangular lattice, one needs to specify

  • The coordinates of the lower-left corner of the lattice (RectLattice.lower_left),
  • The pitch of the lattice, i.e., the distance between the center of adjacent lattice elements (RectLattice.pitch),
  • What universes should fill each lattice element (RectLattice.universes), and
  • A universe that is used to fill any lattice position outside the well-defined portion of the lattice (RectLattice.outer).

For example, to create a 3x3 lattice centered at the origin in which each lattice element is 5cm by 5cm and is filled by a universe u, one could run:

lattice = openmc.RectLattice()
lattice.lower_left = (-7.5, -7.5)
lattice.pitch = (5.0, 5.0)
lattice.universes = [[u, u, u],
                     [u, u, u],
                     [u, u, u]]

Note that because this is a two-dimensional lattice, the lower-left coordinates and pitch only need to specify the \(x,y\) values. The order that the universes appear is such that the first row corresponds to lattice elements with the highest \(y\) -value. Note that the RectLattice.universes attribute expects a doubly-nested iterable of type openmc.Universe — this can be normal Python lists, as shown above, or a NumPy array can be used as well:

lattice.universes = np.tile(u, (3, 3))

For a three-dimensional lattice, the \(x,y,z\) coordinates of the lower-left coordinate need to be given and the pitch should also give dimensions for all three axes. For example, to make a 3x3x3 lattice where the bottom layer is universe u, the middle layer is universe q and the top layer is universe z would look like:

lat3d = openmc.RectLattice()
lat3d.lower_left = (-7.5, -7.5, -7.5)
lat3d.pitch = (5.0, 5.0, 5.0)
lat3d.universes = [
    [[u, u, u],
     [u, u, u],
     [u, u, u]],
    [[q, q, q],
     [q, q, q],
     [q, q, q]],
    [[z, z, z],
     [z, z, z]
     [z, z, z]]]

Again, using NumPy can make things easier:

lat3d.universes = np.empty((3, 3, 3), dtype=openmc.Universe)
lat3d.universes[0, ...] = u
lat3d.universes[1, ...] = q
lat3d.universes[2, ...] = z

Finally, it’s possible to specify that lattice positions that aren’t normally without the bounds of the lattice be filled with an “outer” universe. This allows one to create a truly infinite lattice if desired. An outer universe is set with the RectLattice.outer attribute.

Hexagonal Lattices

OpenMC also allows creation of 2D and 3D hexagonal lattices. Creating a hexagonal lattice is similar to creating a rectangular lattice with a few differences:

  • The center of the lattice must be specified (HexLattice.center).
  • For a 2D hexagonal lattice, a single value for the pitch should be specified, although it still needs to appear in a list. For a 3D hexagonal lattice, the pitch in the radial and axial directions should be given.
  • For a hexagonal lattice, the HexLattice.universes attribute cannot be given as a NumPy array for reasons explained below.
  • As with rectangular lattices, the HexLattice.outer attribute will specify an outer universe.

For a 2D hexagonal lattice, the HexLattice.universes attribute should be set to a two-dimensional list of universes filling each lattice element. Each sub-list corresponds to one ring of universes and is ordered from the outermost ring to the innermost ring. The universes within each sub-list are ordered from the “top” (position with greatest y value) and proceed in a clockwise fashion around the ring. The HexLattice.show_indices() static method can be used to help figure out how to place universes:

>>> print(openmc.HexLattice.show_indices(3))
            (0, 0)
      (0,11)      (0, 1)
(0,10)      (1, 0)      (0, 2)
      (1, 5)      (1, 1)
(0, 9)      (2, 0)      (0, 3)
      (1, 4)      (1, 2)
(0, 8)      (1, 3)      (0, 4)
      (0, 7)      (0, 5)
            (0, 6)

Note that by default, hexagonal lattices are positioned such that each lattice element has two faces that are parallel to the \(y\) axis. As one example, to create a three-ring lattice centered at the origin with a pitch of 10 cm where all the lattice elements centered along the \(y\) axis are filled with universe u and the remainder are filled with universe q, the following code would work:

hexlat = openmc.HexLattice()
hexlat.center = (0, 0)
hexlat.pitch = [10]

outer_ring = [u, q, q, q, q, q, u, q, q, q, q, q]
middle_ring = [u, q, q, u, q, q]
inner_ring = [u]
hexlat.universes = [outer_ring, middle_ring, inner_ring]

If you need to create a hexagonal boundary (composed of six planar surfaces) for a hexagonal lattice, openmc.model.hexagonal_prism() can be used.

Exporting a Geometry Model

Once you have finished building your geometry by creating surfaces, cell, and, if needed, lattices, the last step is to create an instance of openmc.Geometry and export it to an XML file that the openmc executable can read using the Geometry.export_to_xml() method. This can be done as follows:

geom = openmc.Geometry(root_univ)
geom.export_to_xml()

# This is equivalent
geom = openmc.Geometry()
geom.root_universe = root_univ
geom.export_to_xml()

Note that it’s not strictly required to manually create a root universe. You can also pass a list of cells to the openmc.Geometry constructor and it will handle creating the unverse:

geom = openmc.Geometry([cell1, cell2, cell3])
geom.export_to_xml()

Using CAD-based Geometry

OpenMC relies on the Direct Accelerated Geometry Monte Carlo toolkit (DAGMC) to represent CAD-based geometry in a surface mesh format. A DAGMC run can be enabled in OpenMC by setting the dagmc property to True in the model Settings either via the Python openmc.settings Python class:

settings = openmc.Settings()
settings.dagmc = True

or in the settings.xml file:

<dagmc>true</dagmc>

With dagmc set to true, OpenMC will load the DAGMC model (from a local file named dagmc.h5m) when initializing a simulation. If a geometry.xml is present as well, it will be ignored.

Note: DAGMC geometries used in OpenMC are currently required to be clean, meaning that all surfaces have been imprinted and merged successfully and that the model is watertight. Future implementations of DAGMC geometry will support small volume overlaps and un-merged surfaces.

Calculating Atoms Content

If the total volume occupied by all instances of a cell in the geometry is known by the user, it is possible to assign this volume to a cell without performing a stochastic volume calculation:

from uncertainties import ufloat

# Set known total volume in [cc]
cell = openmc.Cell()
cell.volume = 17.0

# Set volume if it is known with some uncertainty
cell.volume = ufloat(17.0, 0.1)

Once a volume is set, and a cell is filled with a material or distributed materials, it is possible to use the atoms() method to obtain a dictionary of nuclides and their total number of atoms in all instances of a cell (e.g. {'H1': 1.0e22, 'O16': 0.5e22, ...}):

cell = openmc.Cell(fill = u02)
cell.volume = 17.0

O16_atoms = cell.atoms['O16']

Execution Settings

Once you have created the materials and geometry for your simulation, the last step to have a complete model is to specify execution settings through the openmc.Settings class. At a minimum, you need to specify a source distribution and how many particles to run. Many other execution settings can be set using the openmc.Settings object, but they are generally optional.

Run Modes

The Settings.run_mode attribute controls what run mode is used when openmc is executed. There are five different run modes that can be specified:

‘eigenvalue’
Runs a \(k\) eigenvalue simulation. See Eigenvalue Calculations for a full description of eigenvalue calculations. In this mode, the Settings.source specifies a starting source that is only used for the first fission generation.
‘fixed source’
Runs a fixed-source calculation with a specified external source, specified in the Settings.source attribute.
‘volume’
Runs a stochastic volume calculation.
‘plot’
Generates slice or voxel plots (see Geometry Visualization).
‘particle restart’
Simulate a single source particle using a particle restart file.

So, for example, to specify that OpenMC should be run in fixed source mode, you would need to instantiate a openmc.Settings object and assign the Settings.run_mode attribute:

settings = openmc.Settings()
settings.run_mode = 'fixed source'

If you don’t specify a run mode, the default run mode is ‘eigenvalue’.

Run Strategy

For a fixed source simulation, the total number of source particle histories simulated is broken up into a number of batches, each corresponding to a realization of the tally random variables. Thus, you need to specify both the number of batches (Settings.batches) as well as the number of particles per batch (Settings.particles).

For a \(k\) eigenvalue simulation, particles are grouped into fission generations, as described in Eigenvalue Calculations. Successive fission generations can be combined into a batch for statistical purposes. By default, a batch will consist of only a single fission generation, but this can be changed with the Settings.generations_per_batch attribute. For problems with a high dominance ratio, using multiple generations per batch can help reduce underprediction of variance, thereby leading to more accurate confidence intervals. Tallies should not be scored to until the source distribution converges, as described in Method of Successive Generations, which may take many generations. To specify the number of batches that should be discarded before tallies begin to accumulate, use the Settings.inactive attribute.

The following example shows how one would simulate 10000 particles per generation, using 10 generations per batch, 150 total batches, and discarding 5 batches. Thus, a total of 145 active batches (or 1450 generations) will be used for accumulating tallies.

settings.particles = 10000
settings.generations_per_batch = 10
settings.batches = 150
settings.inactive = 5
Number of Batches

In general, the stochastic uncertainty in your simulation results is directly related to how many total active particles are simulated (the product of the number of active batches, number of generations per batch, and number of particles). At a minimum, you should use enough active batches so that the central limit theorem is satisfied (about 30). Otherwise, reducing the overall uncertainty in your simulation by a factor of 2 will require using 4 times as many batches (since the standard deviation decreases as \(1/\sqrt{N}\)).

Number of Inactive Batches

For \(k\) eigenvalue simulations, the source distribution is not known a priori. Thus, a “guess” of the source distribution is made and then iterated on, with the source evolving closer to the true distribution at each iteration. Once the source distribution has converged, it is then safe to start accumulating tallies. Consequently, a preset number of inactive batches are run before the active batches (where tallies are turned on) begin. The number of inactive batches necessary to reach a converged source depends on the spatial extent of the problem, its dominance ratio, what boundary conditions are used, and many other factors. For small problems, using 50–100 inactive batches is likely sufficient. For larger models, many hundreds of inactive batches may be necessary. Users are recommended to use the Shannon entropy diagnostic as a way of determining how many inactive batches are necessary.

Specifying the initial source used for the very first batch is described in below. Although the initial source is arbitrary in the sense that any source will eventually converge to the correct distribution, using a source guess that is closer to the actual converged source distribution will translate into needing fewer inactive batches (and hence less simulation time).

For fixed source simulations, the source distribution is known exactly, so no inactive batches are needed. In this case the Settings.inactive attribute can be omitted since it defaults to zero.

Number of Generations per Batch

The standard deviation of tally results is calculated assuming that all realizations (batches) are independent. However, in a \(k\) eigenvalue calculation, the source sites for each batch are produced from fissions in the preceding batch, resulting in a correlation between successive batches. This correlation can result in an underprediction of the variance. That is, the variance reported is actually less than the true variance. To mitigate this effect, OpenMC allows you to group together multiple fission generations into a single batch for statistical purposes, rather than having each fission generation be a separate batch, which is the default behavior.

Number of Particles per Generation

There are several considerations for choosing the number of particles per generation. As discussed in Number of Batches, the total number of active particles will determine the level of stochastic uncertainty in simulation results, so using a higher number of particles will result in less uncertainty. For parallel simulations that use OpenMP and/or MPI, the number of particles per generation should be large enough to ensure good load balancing between threads. For example, if you are running on a single processor with 32 cores, each core should have at least 100 particles or so (i.e., at least 3,200 particles per generation should be used). Using a larger number of particles per generation can also help reduce the cost of synchronization and communication between batches. For \(k\) eigenvalue calculations, experts recommend at least 10,000 particles per generation to avoid any bias in the estimate of \(k\) eigenvalue or tallies.

External Source Distributions

External source distributions can be specified through the Settings.source attribute. If you have a single external source, you can create an instance of openmc.Source and use it to set the Settings.source attribute. If you have multiple external sources with varying source strengths, Settings.source should be set to a list of openmc.Source objects.

The openmc.Source class has three main attributes that one can set: Source.space, which defines the spatial distribution, Source.angle, which defines the angular distribution, and Source.energy, which defines the energy distribution.

The spatial distribution can be set equal to a sub-class of openmc.stats.Spatial; common choices are openmc.stats.Point or openmc.stats.Box. To independently specify distributions in the \(x\), \(y\), and \(z\) coordinates, you can use openmc.stats.CartesianIndependent. To independently specify distributions using spherical or cylindrical coordinates, you can use openmc.stats.SphericalIndependent or openmc.stats.CylindricalIndependent, respectively.

The angular distribution can be set equal to a sub-class of openmc.stats.UnitSphere such as openmc.stats.Isotropic, openmc.stats.Monodirectional, or openmc.stats.PolarAzimuthal. By default, if no angular distribution is specified, an isotropic angular distribution is used. As an example of a non-trivial angular distribution, the following code would create a conical distribution with an aperture of 30 degrees pointed in the positive x direction:

from math import pi, cos
aperture = 30.0
mu = openmc.stats.Uniform(cos(aperture/2), 1.0)
phi = openmc.stats.Uniform(0.0, 2*pi)
angle = openmc.stats.PolarAzimuthal(mu, phi, reference_uvw=(1., 0., 0.))

The energy distribution can be set equal to any univariate probability distribution. This could be a probability mass function (openmc.stats.Discrete), a Watt fission spectrum (openmc.stats.Watt), or a tabular distribution (openmc.stats.Tabular). By default, if no energy distribution is specified, a Watt fission spectrum with \(a\) = 0.988 MeV and \(b\) = 2.249 MeV -1 is used.

As an example, to create an isotropic, 10 MeV monoenergetic source uniformly distributed over a cube centered at the origin with an edge length of 10 cm, one would run:

source = openmc.Source()
source.space = openmc.stats.Box((-5, -5, -5), (5, 5, 5))
source.angle = openmc.stats.Isotropic()
source.energy = openmc.stats.Discrete([10.0e6], [1.0])
settings.source = source

The openmc.Source class also has a Source.strength attribute that indicates the relative strength of a source distribution if multiple are used. For example, to create two sources, one that should be sampled 70% of the time and another that should be sampled 30% of the time:

src1 = openmc.Source()
src1.strength = 0.7
...

src2 = openmc.Source()
src2.strength = 0.3
...

settings.source = [src1, src2]

Finally, the Source.particle attribute can be used to indicate the source should be composed of particles other than neutrons. For example, the following would generate a photon source:

source = openmc.Source()
source.particle = 'photon'
...

settings.source = source

For a full list of all classes related to statistical distributions, see openmc.stats – Statistics.

File-based Sources

OpenMC can use a pregenerated HDF5 source file by specifying the filename argument to openmc.Source:

settings.source = openmc.Source(filename='source.h5')

Statepoint and source files are generated automatically when a simulation is run and can be used as the starting source in a new simulation. Alternatively, a source file can be manually generated with the openmc.write_source_file() function. This is particularly useful for coupling OpenMC with another program that generates a source to be used in OpenMC.

A source file based on particles that cross one or more surfaces can be generated during a simulation using the Settings.surf_source_write attribute:

settings.surf_source_write = {
    'surfaces_ids': [1, 2, 3],
    'max_particles': 10000
}

In this example, at most 10,000 source particles are stored when particles cross surfaces with IDs of 1, 2, or 3.

Custom Sources

It is often the case that one may wish to simulate a complex source distribution that is not possible to represent with the classes described above. For these situations, it is possible to define a complex source class containing an externally defined source function that is loaded at runtime. A simple example source is shown below.

#include <memory> // for unique_ptr

#include "openmc/random_lcg.h"
#include "openmc/source.h"
#include "openmc/particle.h"

class CustomSource : public openmc::Source
{
  openmc::Particle::Bank sample(uint64_t* seed) const
  {
    openmc::Particle::Bank particle;
    // weight
    particle.particle = openmc::Particle::Type::neutron;
    particle.wgt = 1.0;
    // position
    double angle = 2.0 * M_PI * openmc::prn(seed);
    double radius = 3.0;
    particle.r.x = radius * std::cos(angle);
    particle.r.y = radius * std::sin(angle);
    particle.r.z = 0.0;
    // angle
    particle.u = {1.0, 0.0, 0.0};
    particle.E = 14.08e6;
    particle.delayed_group = 0;
    return particle;
  }
};

extern "C" std::unique_ptr<CustomSource> openmc_create_source(std::string parameters)
{
  return std::make_unique<CustomSource>();
}

The above source creates monodirectional 14.08 MeV neutrons that are distributed in a ring with a 3 cm radius. This routine is not particularly complex, but should serve as an example upon which to build more complicated sources.

Note

The source class must inherit from openmc::Source and implement a sample() function.

Note

The openmc_create_source() function signature must be declared extern "C".

Note

You should only use the openmc::prn() random number generator.

In order to build your external source, you will need to link it against the OpenMC shared library. This can be done by writing a CMakeLists.txt file:

cmake_minimum_required(VERSION 3.3 FATAL_ERROR)
project(openmc_sources CXX)
add_library(source SHARED source_ring.cpp)
find_package(OpenMC REQUIRED HINTS <path to openmc>)
target_link_libraries(source OpenMC::libopenmc)

After running cmake and make, you will have a libsource.so (or .dylib) file in your build directory. Setting the openmc.Source.library attribute to the path of this shared library will indicate that it should be used for sampling source particles at runtime.

Custom Parameterized Sources

Some custom sources may have values (parameters) that can be changed between runs. This is supported by using the openmc_create_source() function to pass parameters defined in the openmc.Source.parameters attribute to the source class when it is created:

#include <memory> // for unique_ptr

#include "openmc/source.h"
#include "openmc/particle.h"

class CustomSource : public openmc::Source {
public:
  CustomSource(double energy) : energy_{energy} { }

  // Samples from an instance of this class.
  openmc::Particle::Bank sample(uint64_t* seed) const
  {
    openmc::Particle::Bank particle;
    // weight
    particle.particle = openmc::Particle::Type::neutron;
    particle.wgt = 1.0;
    // position
    particle.r.x = 0.0;
    particle.r.y = 0.0;
    particle.r.z = 0.0;
    // angle
    particle.u = {1.0, 0.0, 0.0};
    particle.E = this->energy_;
    particle.delayed_group = 0;

    return particle;
  }

private:
  double energy_;
};

extern "C" std::unique_ptr<CustomSource> openmc_create_source(std::string parameter) {
  double energy = std::stod(parameter);
  return std::make_unique<CustomSource>(energy);
}

As with the basic custom source functionality, the custom source library location must be provided in the openmc.Source.library attribute.

Shannon Entropy

To assess convergence of the source distribution, the scalar Shannon entropy metric is often used in Monte Carlo codes. OpenMC also allows you to calculate Shannon entropy at each generation over a specified mesh, created using the openmc.RegularMesh class. After instantiating a RegularMesh, you need to specify the lower-left coordinates of the mesh (RegularMesh.lower_left), the number of mesh cells in each direction (RegularMesh.dimension) and either the upper-right coordinates of the mesh (RegularMesh.upper_right) or the width of each mesh cell (RegularMesh.width). Once you have a mesh, simply assign it to the Settings.entropy_mesh attribute.

entropy_mesh = openmc.RegularMesh()
entropy_mesh.lower_left = (-50, -50, -25)
entropy_mesh.upper_right = (50, 50, 25)
entropy_mesh.dimension = (8, 8, 8)

settings.entropy_mesh = entropy_mesh

If you’re unsure of what bounds to use for the entropy mesh, you can try getting a bounding box for the entire geometry using the Geometry.bounding_box property:

geom = openmc.Geometry()
...
m = openmc.RegularMesh()
m.lower_left, m.upper_right = geom.bounding_box
m.dimension = (8, 8, 8)

settings.entropy_mesh = m

Photon Transport

In addition to neutrons, OpenMC is also capable of simulating the passage of photons through matter. This allows the modeling of photon production from neutrons as well as pure photon calculations. The Settings.photon_transport attribute can be used to enable photon transport:

settings.photon_transport = True

The way in which OpenMC handles secondary charged particles can be specified with the Settings.electron_treatment attribute. By default, the thick-target bremsstrahlung (TTB) approximation is used to generate bremsstrahlung radiation emitted by electrons and positrons created in photon interactions. To neglect secondary bremsstrahlung photons and instead deposit all energy from electrons locally, the local energy deposition option can be selected:

settings.electron_treatment = 'led'

Note

Some features related to photon transport are not currently implemented, including:

  • Tallying photon energy deposition.
  • Generating a photon source from a neutron calculation that can be used for a later fixed source photon calculation.
  • Photoneutron reactions.

Generation of Output Files

A number of attributes of the openmc.Settings class can be used to control what files are output and how often. First, there is the Settings.output attribute which takes a dictionary having keys ‘summary’, ‘tallies’, and ‘path’. The first two keys controls whether a summary.h5 and tallies.out file are written, respectively (see Viewing and Analyzing Results for a description of those files). By default, output files are written to the current working directory; this can be changed by setting the ‘path’ key. For example, if you want to disable the tallies.out file and write the summary.h5 to a directory called ‘results’, you’d specify the Settings.output dictionary as:

settings.output = {
    'tallies': False,
    'path': 'results'
}

Generation of statepoint and source files is handled separately through the Settings.statepoint and Settings.sourcepoint attributes. Both of those attributes expect dictionaries and have a ‘batches’ key which indicates at which batches statepoints and source files should be written. Note that by default, the source is written as part of the statepoint file; this behavior can be changed by the ‘separate’ and ‘write’ keys of the Settings.sourcepoint dictionary, the first of which indicates whether the source should be written to a separate file and the second of which indicates whether the source should be written at all.

As an example, to write a statepoint file every five batches:

settings.batches = n
settings.statepoint = {'batches': range(5, n + 5, 5)}

Specifying Tallies

In order to obtain estimates of physical quantities in your simulation, you need to create one or more tallies using the openmc.Tally class. As explained in detail in the theory manual, tallies provide estimates of a scoring function times the flux integrated over some region of phase space, as in:

\[X = \underbrace{\int d\mathbf{r} \int d\mathbf{\Omega} \int dE}_{\text{filters}} \underbrace{f(\mathbf{r}, \mathbf{\Omega}, E)}_{\text{scores}} \psi (\mathbf{r}, \mathbf{\Omega}, E)\]

Thus, to specify a tally, we need to specify what regions of phase space should be included when deciding whether to score an event as well as what the scoring function (\(f\) in the above equation) should be used. The regions of phase space are generally called filters and the scoring functions are simply called scores.

The only cases when filters do not correspond directly with the regions of phase space are when expansion functions are applied in the integrand, such as for Legendre expansions of the scattering kernel.

Filters

To specify the regions of phase space, one must create a openmc.Filter. Since openmc.Filter is an abstract class, you actually need to instantiate one of its sub-classes (for a full listing, see Constructing Tallies). For example, to indicate that events that occur in a given cell should score to the tally, we would create a openmc.CellFilter:

cell_filter = openmc.CellFilter([fuel.id, moderator.id, reflector.id])

Another commonly used filter is openmc.EnergyFilter, which specifies multiple energy bins over which events should be scored. Thus, if we wanted to tally events where the incident particle has an energy in the ranges [0 eV, 4 eV] and [4 eV, 1 MeV], we would do the following:

energy_filter = openmc.EnergyFilter([0.0, 4.0, 1.0e6])

Energies are specified in eV and need to be monotonically increasing.

Caution

An energy bin between zero and the lowest energy specified is not included by default as it is in MCNP.

Once you have created a filter, it should be assigned to a openmc.Tally instance through the Tally.filters attribute:

tally.filters.append(cell_filter)
tally.filters.append(energy_filter)

# This is equivalent
tally.filters = [cell_filter, energy_filter]

Note

You are actually not required to assign any filters to a tally. If you create a tally with no filters, all events will score to the tally. This can be useful if you want to know, for example, a reaction rate over your entire model.

Scores

To specify the scoring functions, a list of strings needs to be given to the Tally.scores attribute. You can score the flux (‘flux’), or a reaction rate (‘total’, ‘fission’, etc.). For example, to tally the elastic scattering rate and the fission neutron production, you’d assign:

tally.scores = ['elastic', 'nu-fission']

With no further specification, you will get the total elastic scattering rate and the total fission neutron production. If you want reaction rates for a particular nuclide or set of nuclides, you can set the Tally.nuclides attribute to a list of strings indicating which nuclides. The nuclide names should follow the same naming convention as that used for material specification. If we wanted the reaction rates only for U235 and U238 (separately), we’d set:

tally.nuclides = ['U235', 'U238']

You can also list ‘all’ as a nuclide which will give you a separate reaction rate for every nuclide in the model.

The following tables show all valid scores:

Flux scores: units are particle-cm per source particle.
Score Description
flux Total flux.
Reaction scores: units are reactions per source particle.
Score Description
absorption Total absorption rate. For incident neutrons, this accounts for all reactions that do not produce secondary neutrons as well as fission. For incident photons, this includes photoelectric and pair production.
elastic Elastic scattering reaction rate.
fission Total fission reaction rate.
scatter Total scattering rate.
total Total reaction rate.
(n,2nd) (n,2nd) reaction rate.
(n,2n) (n,2n) reaction rate.
(n,3n) (n,3n) reaction rate.
(n,na) (n,n\(\alpha\)) reaction rate.
(n,n3a) (n,n3\(\alpha\)) reaction rate.
(n,2na) (n,2n\(\alpha\)) reaction rate.
(n,3na) (n,3n\(\alpha\)) reaction rate.
(n,np) (n,np) reaction rate.
(n,n2a) (n,n2\(\alpha\)) reaction rate.
(n,2n2a) (n,2n2\(\alpha\)) reaction rate.
(n,nd) (n,nd) reaction rate.
(n,nt) (n,nt) reaction rate.
(n,n3He) (n,n3He) reaction rate.
(n,nd2a) (n,nd2\(\alpha\)) reaction rate.
(n,nt2a) (n,nt2\(\alpha\)) reaction rate.
(n,4n) (n,4n) reaction rate.
(n,2np) (n,2np) reaction rate.
(n,3np) (n,3np) reaction rate.
(n,n2p) (n,n2p) reaction rate.
(n,n*X*) Level inelastic scattering reaction rate. The X indicates what which inelastic level, e.g., (n,n3) is third-level inelastic scattering.
(n,nc) Continuum level inelastic scattering reaction rate.
(n,gamma) Radiative capture reaction rate.
(n,p) (n,p) reaction rate.
(n,d) (n,d) reaction rate.
(n,t) (n,t) reaction rate.
(n,3He) (n,3He) reaction rate.
(n,a) (n,\(\alpha\)) reaction rate.
(n,2a) (n,2\(\alpha\)) reaction rate.
(n,3a) (n,3\(\alpha\)) reaction rate.
(n,2p) (n,2p) reaction rate.
(n,pa) (n,p\(\alpha\)) reaction rate.
(n,t2a) (n,t2\(\alpha\)) reaction rate.
(n,d2a) (n,d2\(\alpha\)) reaction rate.
(n,pd) (n,pd) reaction rate.
(n,pt) (n,pt) reaction rate.
(n,da) (n,d\(\alpha\)) reaction rate.
coherent-scatter Coherent (Rayleigh) scattering reaction rate.
incoherent-scatter Incoherent (Compton) scattering reaction rate.
photoelectric Photoelectric absorption reaction rate.
pair-production Pair production reaction rate.
Arbitrary integer An arbitrary integer is interpreted to mean the reaction rate for a reaction with a given ENDF MT number.
Particle production scores: units are particles produced per source particles.
Score Description
delayed-nu-fission Total production of delayed neutrons due to fission.
prompt-nu-fission Total production of prompt neutrons due to fission.
nu-fission Total production of neutrons due to fission.
nu-scatter This score is similar in functionality to the scatter score except the total production of neutrons due to scattering is scored vice simply the scattering rate. This accounts for multiplicity from (n,2n), (n,3n), and (n,4n) reactions.
H1-production Total production of H1.
H2-production Total production of H2 (deuterium).
H3-production Total production of H3 (tritium).
He3-production Total production of He3.
He4-production Total production of He4 (alpha particles).
Miscellaneous scores: units are indicated for each.
Score Description
current Used in combination with a meshsurface filter: Partial currents on the boundaries of each cell in a mesh. It may not be used in conjunction with any other score. Only energy and mesh filters may be used. Used in combination with a surface filter: Net currents on any surface previously defined in the geometry. It may be used along with any other filter, except meshsurface filters. Surfaces can alternatively be defined with cell from and cell filters thereby resulting in tallying partial currents. Units are particles per source particle.
events Number of scoring events. Units are events per source particle.
inverse-velocity The flux-weighted inverse velocity where the velocity is in units of centimeters per second.
heating Total nuclear heating in units of eV per source particle. For neutrons, this corresponds to MT=301 produced by NJOY’s HEATR module while for photons, this is tallied from either direct photon energy deposition (analog estimator) or pre-generated photon heating number. See Heating and Energy Deposition
heating-local Total nuclear heating in units of eV per source particle assuming energy from secondary photons is deposited locally. Note that this score should only be used for incident neutrons. See Heating and Energy Deposition.
kappa-fission The recoverable energy production rate due to fission. The recoverable energy is defined as the fission product kinetic energy, prompt and delayed neutron kinetic energies, prompt and delayed \(\gamma\)-ray total energies, and the total energy released by the delayed \(\beta\) particles. The neutrino energy does not contribute to this response. The prompt and delayed \(\gamma\)-rays are assumed to deposit their energy locally. Units are eV per source particle.
fission-q-prompt The prompt fission energy production rate. This energy comes in the form of fission fragment nuclei, prompt neutrons, and prompt \(\gamma\)-rays. This value depends on the incident energy and it requires that the nuclear data library contains the optional fission energy release data. Energy is assumed to be deposited locally. Units are eV per source particle.
fission-q-recoverable The recoverable fission energy production rate. This energy comes in the form of fission fragment nuclei, prompt and delayed neutrons, prompt and delayed \(\gamma\)-rays, and delayed \(\beta\)-rays. This tally differs from the kappa-fission tally in that it is dependent on incident neutron energy and it requires that the nuclear data library contains the optional fission energy release data. Energy is assumed to be deposited locally. Units are eV per source paticle.
decay-rate The delayed-nu-fission-weighted decay rate where the decay rate is in units of inverse seconds.
damage-energy Damage energy production in units of eV per source particle. This corresponds to MT=444 produced by NJOY’s HEATR module.

Normalization of Tally Results

As described in Scores, all tally scores are normalized per source particle simulated. However, for analysis of a given system, we usually want tally scores in a more natural unit. For example, neutron flux is often reported in units of particles/cm2-s. For a fixed source simulation, it is usually straightforward to convert units if the source rate is known. For example, if the system being modeled includes a source that is emitting 104 neutrons per second, the tally results just need to be multipled by 104. This can either be done manually or using the openmc.Source.strength attribute.

For a \(k\)-eigenvalue calculation, normalizing tally results is not as simple because the source rate is not actually known. Instead, we typically know the system power, \(P\), which represents how much energy is deposited per unit time. Most of this energy originates from fission, but a small percentage also results from other reactions (e.g., photons emitted from \((n,\gamma)\) reactions). The most rigorous method to normalize tally results is to run a coupled neutron-photon calculation and tally the heating score over the entire system. This score provides the heating rate in units of [eV/source], which we’ll denote \(H\). Then, calculate the heating rate in J/source as

\[H' = 1.602\times10^{-19} \left [ \frac{\text{J}}{\text{eV}} \right ] \cdot H \left [\frac{\text{eV}}{\text{source}} \right ].\]

Dividing the power by the observed heating rate then gives us a normalization factor that can be applied to other tallies:

\[f = \frac{P}{H'} = \frac{[\text{J}/\text{s}]}{[\text{J}/\text{source}]} = \left [ \frac{\text{source}}{\text{s}} \right ].\]

Multiplying by the normalization factor and dividing by volume, we can then get the flux in typical units:

\[\phi' = \frac{f\phi}{V} = \frac{[\text{source}/\text{s}][\text{particle-cm}/\text{source}]} {[\text{cm}^3]} = \left [\frac{\text{particle}}{\text{cm}^2\cdot\text{s}} \right ]\]

There are several slight variations on this procedure:

  • Run a neutron-only calculation and estimate the total heating using the heating-local score (this requires that your nuclear data has local heating data available, such as in the official data library at https://openmc.org. See Heating and Energy Deposition for more information.)
  • Run a neutron-only calculation and use the kappa-fission or fission-q-recoverable scores along with an estimate of the extra heating due to neutron capture reactions.
  • Calculate the overall fission rate and then used a fixed Q value to estimate the heating rate.

Note that the only difference between these and the above procedures is in how \(H'\) is estimated.

Geometry Visualization

OpenMC is capable of producing two-dimensional slice plots of a geometry as well as three-dimensional voxel plots using the geometry plotting run mode. The geometry plotting mode relies on the presence of a plots.xml file that indicates what plots should be created. To create this file, one needs to create one or more openmc.Plot instances, add them to a openmc.Plots collection, and then use the Plots.export_to_xml method to write the plots.xml file.

Slice Plots

_images/atr.png

By default, when an instance of openmc.Plot is created, it indicates that a 2D slice plot should be made. You can specify the origin of the plot (Plot.origin), the width of the plot in each direction (Plot.width), the number of pixels to use in each direction (Plot.pixels), and the basis directions for the plot. For example, to create a \(x\) - \(z\) plot centered at (5.0, 2.0, 3.0) with a width of (50., 50.) and 400x400 pixels:

plot = openmc.Plot()
plot.basis = 'xz'
plot.origin = (5.0, 2.0, 3.0)
plot.width = (50., 50.)
plot.pixels = (400, 400)

The color of each pixel is determined by placing a particle at the center of that pixel and using OpenMC’s internal find_cell routine (the same one used for particle tracking during simulation) to determine the cell and material at that location.

Note

In this example, pixels are 50/400=0.125 cm wide. Thus, this plot may miss any features smaller than 0.125 cm, since they could exist between pixel centers. More pixels can be used to resolve finer features but will result in larger files.

By default, a unique color will be assigned to each cell in the geometry. If you want your plot to be colored by material instead, change the Plot.color_by attribute:

plot.color_by = 'material'

If you don’t like the random colors assigned, you can also indicate that particular cells/materials should be given colors of your choosing:

plot.colors = {
    water: 'blue',
    clad: 'black'
}

# This is equivalent
plot.colors = {
    water: (0, 0, 255),
    clad: (0, 0, 0)
}

Note that colors can be given as RGB tuples or by a string indicating a valid SVG color.

When you’re done creating your openmc.Plot instances, you need to then assign them to a openmc.Plots collection and export it to XML:

plots = openmc.Plots([plot1, plot2, plot3])
plots.export_to_xml()

# This is equivalent
plots = openmc.Plots()
plots.append(plot1)
plots += [plot2, plot3]
plots.export_to_xml()

To actually generate the plots, run the openmc.plot_geometry() function. Alternatively, run the openmc executable with the --plot command-line flag. When that has finished, you will have one or more .ppm files, i.e., portable pixmap files. On some Linux distributions, these .ppm files are natively viewable. If you find that you’re unable to open them on your system (or you don’t like the fact that they are not compressed), you may want to consider converting them to another format. This is easily accomplished with the convert command available on most Linux distributions as part of the ImageMagick package. (On Debian derivatives: sudo apt install imagemagick). Images are then converted like:

convert myplot.ppm myplot.png

Alternatively, if you’re working within a Jupyter Notebook or QtConsole, you can use the openmc.plot_inline() to run OpenMC in plotting mode and display the resulting plot within the notebook.

Voxel Plots

_images/3dba.png

The openmc.Plot class can also be told to generate a 3D voxel plot instead of a 2D slice plot. Simply change the Plot.type attribute to ‘voxel’. In this case, the Plot.width and Plot.pixels attributes should be three items long, e.g.:

vox_plot = openmc.Plot()
vox_plot.type = 'voxel'
vox_plot.width = (100., 100., 50.)
vox_plot.pixels = (400, 400, 200)

The voxel plot data is written to an HDF5 file. The voxel file can subsequently be converted into a standard mesh format that can be viewed in ParaView, VisIt, etc. This typically will compress the size of the file significantly. The provided openmc-voxel-to-vtk script can convert the HDF5 voxel file to VTK formats. Once processed into a standard 3D file format, colors and masks can be defined using the stored ID numbers to better explore the geometry. The process for doing this will depend on the 3D viewer, but should be straightforward.

Note

3D voxel plotting can be very computer intensive for the viewing program (Visit, ParaView, etc.) if the number of voxels is large (>10 million or so). Thus if you want an accurate picture that renders smoothly, consider using only one voxel in a certain direction.

Depletion and Transmutation

OpenMC supports coupled depletion, or burnup, calculations through the openmc.deplete Python module. OpenMC solves the transport equation to obtain transmutation reaction rates, and then the reaction rates are used to solve a set of transmutation equations that determine the evolution of nuclide densities within a material. The nuclide densities predicted as some future time are then used to determine updated reaction rates, and the process is repeated for as many timesteps as are requested.

The depletion module is designed such that the flux/reaction rate solution (the transport “operator”) is completely isolated from the solution of the transmutation equations and the method used for advancing time. At present, the openmc.deplete module offers a single transport operator, openmc.deplete.Operator (which uses the OpenMC transport solver), but in principle additional operator classes based on other transport codes could be implemented and no changes to the depletion solver itself would be needed. The operator class requires a openmc.Geometry instance and a openmc.Settings instance:

geom = openmc.Geometry()
settings = openmc.Settings()
...

op = openmc.deplete.Operator(geom, settings)

Any material that contains a fissionable nuclide is depleted by default, but this can behavior can be changed with the Material.depletable attribute.

Important

The volume must be specified for each material that is depleted by setting the Material.volume attribute. This is necessary in order to calculate the proper normalization of tally results based on the source rate.

openmc.deplete supports multiple time-integration methods for determining material compositions over time. Each method appears as a different class. For example, openmc.deplete.CECMIntegrator runs a depletion calculation using the CE/CM algorithm (deplete over a timestep using the middle-of-step reaction rates). An instance of openmc.deplete.Operator is passed to one of these functions along with the timesteps and power level:

power = 1200.0e6  # watts
timesteps = [10.0, 10.0, 10.0]  # days
openmc.deplete.CECMIntegrator(op, timesteps, power, timestep_units='d').integrate()

The coupled transport-depletion problem is executed, and once it is done a depletion_results.h5 file is written. The results can be analyzed using the openmc.deplete.ResultsList class. This class has methods that allow for easy retrieval of k-effective, nuclide concentrations, and reaction rates over time:

results = openmc.deplete.ResultsList.from_hdf5("depletion_results.h5")
time, keff = results.get_eigenvalue()

Note that the coupling between the transport solver and the transmutation solver happens in-memory rather than by reading/writing files on disk.

Fixed-Source Transmutation

When the power or power_density argument is used for one of the Integrator classes, it is assumed that OpenMC is running in k-eigenvalue mode, and normalization of tally results is performed based on energy deposition. It is also possible to run a fixed-source simulation and perform normalization based on a known source rate. First, as with all fixed-source calculations, we need to set the run mode:

settings.run_mode = 'fixed source'

Additionally, all materials that you wish to deplete need to be marked as such using the Material.depletable attribute:

mat = openmc.Material()
mat.depletable = True

When constructing the Operator, you should indicate that normalization of tally results will be done based on the source rate rather than a power or power density:

op = openmc.deplete.Operator(geometry, settings, normalization_mode='source-rate')

Finally, when creating a depletion integrator, use the source_rates argument:

integrator = openmc.deplete.PredictorIntegrator(op, timesteps, sources_rates=...)

As with the power argument, you can provide a different source rate for each timestep in the calculation. A zero source rate for a given timestep will result in a decay-only step, where all reaction rates are zero.

Caveats

Energy Deposition

The default energy deposition mode, "fission-q", instructs the openmc.deplete.Operator to normalize reaction rates using the product of fission reaction rates and fission Q values taken from the depletion chain. This approach does not consider indirect contributions to energy deposition, such as neutron heating and energy from secondary photons. In doing this, the energy deposited during a transport calculation will be lower than expected. This causes the reaction rates to be over-adjusted to hit the user-specific power, or power density, leading to an over-depletion of burnable materials.

There are some remedies. First, the fission Q values can be directly set in a variety of ways. This requires knowing what the total fission energy release should be, including indirect components. Some examples are provided below:

# use a dictionary of fission_q values
fission_q = {"U235": 202e+6}  # energy in eV

# create a modified chain and write it to a new file
chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q)
chain.export_to_xml("chain_mod_q.xml")
op = openmc.deplete.Operator(geometry, setting, "chain_mod_q.xml")

# alternatively, pass the modified fission Q directly to the operator
op = openmc.deplete.Operator(geometry, setting, "chain.xml",
    fission_q=fission_q)

A more complete way to model the energy deposition is to use the modified heating reactions described in Heating and Energy Deposition. These values can be used to normalize reaction rates instead of using the fission reaction rates with:

op = openmc.deplete.Operator(geometry, settings, "chain.xml",
    normalization_mode="energy-deposition")

These modified heating libraries can be generated by running the latest version of openmc.data.IncidentNeutron.from_njoy(), and will eventually be bundled into the distributed libraries.

Local Spectra and Repeated Materials

It is not uncommon to explicitly create a single burnable material across many locations. From a pure transport perspective, there is nothing wrong with creating a single 3.5 wt.% enriched fuel fuel_3, and placing that fuel in every fuel pin in an assembly or even full core problem. This certainly expedites the model making process, but can pose issues with depletion. Under this setup, openmc.deplete will deplete a single fuel_3 material using a single set of reaction rates, and produce a single new composition for the next time step. This can be problematic if the same fuel_3 is used in very different regions of the problem.

As an example, consider a full-scale power reactor core with vacuum boundary conditions, and with fuel pins solely composed of the same fuel_3 material. The fuel pins towards the center of the problem will surely experience a more intense neutron flux and greater reaction rates than those towards the edge of the domain. This indicates that the fuel in the center should be at a more depleted state than periphery pins, at least for the fist depletion step. However, without any other instructions, OpenMC will deplete fuel_3 as a single material, and all of the fuel pins will have an identical composition at the next transport step.

This can be countered by instructing the operator to treat repeated instances of the same material as a unique material definition with:

op = openmc.deplete.Operator(geometry, settings, chain_file,
    diff_burnable_mats=True)

For our example problem, this would deplete fuel on the outer region of the problem with different reaction rates than those in the center. Materials will be depleted corresponding to their local neutron spectra, and have unique compositions at each transport step. The volume of the original fuel_3 material must represent the volume of all the fuel_3 in the problem. When creating the unique materials, this volume will be equally distributed across all material instances.

Note

This will increase the total memory usage and run time due to an increased number of tallies and material definitions.

Executables and Scripts

openmc

Once you have a model built (see Basics of Using OpenMC), you can either run the openmc executable directly from the directory containing your XML input files, or you can specify as a command-line argument the directory containing the XML input files.

Warning

OpenMC models should be treated as code, and it is important to be careful with code from untrusted sources.

For example, if your XML input files are in the directory /home/username/somemodel/, one way to run the simulation would be:

cd /home/username/somemodel
openmc

Alternatively, you could run from any directory:

openmc /home/username/somemodel

Note that in the latter case, any output files will be placed in the present working directory which may be different from /home/username/somemodel. openmc accepts the following command line flags:

-c, --volume Run in stochastic volume calculation mode
-e, --event Run using event-based parallelism
-g, --geometry-debug
 Run in geometry debugging mode, where cell overlaps are checked for after each move of a particle
-n, --particles N
 Use N particles per generation or batch
-p, --plot Run in plotting mode
-r, --restart file
 Restart a previous run from a state point or a particle restart file
-s, --threads N
 Run with N OpenMP threads
-t, --track Write tracks for all particles
-v, --version Show version information
-h, --help Show help message

Note

If you’re using the Python API, openmc.run() is equivalent to running openmc from the command line.

openmc-ace-to-hdf5

This script can be used to create HDF5 nuclear data libraries used by OpenMC if you have existing ACE files. There are four different ways you can specify ACE libraries that are to be converted:

  1. List each ACE library as a positional argument. This is very useful in conjunction with the usual shell utilities (ls, find, etc.).
  2. Use the --xml option to specify a pre-v0.9 cross_sections.xml file.
  3. Use the --xsdir option to specify a MCNP xsdir file.
  4. Use the --xsdata option to specify a Serpent xsdata file.

The script does not use any extra information from cross_sections.xml/ xsdir/ xsdata files to determine whether the nuclide is metastable. Instead, the --metastable argument can be used to specify whether the ZAID naming convention follows the NNDC data convention (1000*Z + A + 300 + 100*m), or the MCNP data convention (essentially the same as NNDC, except that the first metastable state of Am242 is 95242 and the ground state is 95642).

The optional --fission_energy_release argument will accept an HDF5 file containing a library of fission energy release (ENDF MF=1 MT=458) data. A library built from ENDF/B-VII.1 data is released with OpenMC and can be found at openmc/data/fission_Q_data_endb71.h5. This data is necessary for ‘fission-q-prompt’ and ‘fission-q-recoverable’ tallies, but is not needed otherwise.

-h, --help show help message and exit
-d DESTINATION, --destination DESTINATION
 Directory to create new library in
-m META, --metastable META
 How to interpret ZAIDs for metastable nuclides. META can be either ‘nndc’ or ‘mcnp’. (default: nndc)
--xml XML Old-style cross_sections.xml that lists ACE libraries
--xsdir XSDIR MCNP xsdir file that lists ACE libraries
--xsdata XSDATA
 Serpent xsdata file that lists ACE libraries
--fission_energy_release FISSION_ENERGY_RELEASE
 HDF5 file containing fission energy release data

openmc-plot-mesh-tally

openmc-plot-mesh-tally provides a graphical user interface for plotting mesh tallies. The path to the statepoint file can be provided as an optional arugment (if omitted, a file dialog will be presented).

openmc-track-to-vtk

This script converts HDF5 particle track files to VTK poly data that can be viewed with ParaView or VisIt. The filenames of the particle track files should be given as posititional arguments. The output filename can also be changed with the -o flag:

-o OUT, --out OUT
 Output VTK poly filename

openmc-update-inputs

If you have existing XML files that worked in a previous version of OpenMC that no longer work with the current version, you can try to update these files using openmc-update-inputs. If any of the given files do not match the most up-to-date formatting, then they will be automatically rewritten. The old out-of-date files will not be deleted; they will be moved to a new file with ‘.original’ appended to their name.

Formatting changes that will be made:

geometry.xml
Lattices containing ‘outside’ attributes/tags will be replaced with lattices containing ‘outer’ attributes, and the appropriate cells/universes will be added. Any ‘surfaces’ attributes/elements on a cell will be renamed ‘region’.
materials.xml
Nuclide names will be changed from ACE aliases (e.g., Am-242m) to HDF5/GND names (e.g., Am242_m1). Thermal scattering table names will be changed from ACE aliases (e.g., HH2O) to HDF5/GND names (e.g., c_H_in_H2O).

openmc-update-mgxs

This script updates OpenMC’s deprecated multi-group cross section XML files to the latest HDF5-based format.

-i IN, --input IN
 Input XML file
-o OUT, --output OUT
 Output file in HDF5 format

openmc-validate-xml

Input files can be checked before executing OpenMC using the openmc-validate-xml script which is installed alongside the Python API. Two command line arguments can be set when running openmc-validate-xml:

-i, --input-path
 Location of OpenMC input files.
-r, --relaxng-path
 Location of OpenMC RelaxNG files

If the RelaxNG path is not set, the script will search for these files because it expects that the user is either running the script located in the install directory bin folder or in src/utils. Once executed, it will match OpenMC XML files with their RelaxNG schema and check if they are valid. Below is a table of the messages that will be printed after each file is checked.

Message Description
[XML ERROR] Cannot parse XML file.
[NO RELAXNG FOUND] No RelaxNG file found for XML file.
[NOT VALID] XML file does not match RelaxNG.
[VALID] XML file matches RelaxNG.

openmc-voxel-to-vtk

When OpenMC generates voxel plots, they are in an HDF5 format that is not terribly useful by itself. The openmc-voxel-to-vtk script converts a voxel HDF5 file to a VTK file. To run this script, you will need to have the VTK Python bindings installed. To convert a voxel file, simply provide the path to the file:

openmc-voxel-to-vtk voxel_1.h5

The openmc-voxel-to-vtk script also takes the following optional command-line arguments:

-o, --output Path to output VTK file

Data Processing and Visualization

This section is intended to explain procedures for carrying out common post-processing tasks with OpenMC. While several utilities of varying complexity are provided to help automate the process, the most powerful capabilities for post-processing derive from use of the Python API.

Working with State Points

Tally results are saved in both a text file (tallies.out) as well as an HDF5 statepoint file. While the tallies.out file may be fine for simple tallies, in many cases the user requires more information about the tally or the run, or has to deal with a large number of result values (e.g. for mesh tallies). In these cases, extracting data from the statepoint file via the Python API is the preferred method of data analysis and visualization.

Data Extraction

A great deal of information is available in statepoint files (See State Point File Format), all of which is accessible through the Python API. The openmc.StatePoint class can load statepoints and access data as requested; it is used in many of the provided plotting utilities, OpenMC’s regression test suite, and can be used in user-created scripts to carry out manipulations of the data.

An example notebook demonstrates how to extract data from a statepoint using the Python API.

Plotting in 2D

The notebook example also demonstrates how to plot a structured mesh tally in two dimensions using the Python API. One can also use the openmc-plot-mesh-tally script which provides an interactive GUI to explore and plot structured mesh tallies for any scores and filter bins.

_images/plotmeshtally.png
Getting Data into MATLAB

There is currently no front-end utility to dump tally data to MATLAB files, but the process is straightforward. First extract the data using the Python API via openmc.statepoint and then use the Scipy MATLAB IO routines to save to a MAT file. Note that all arrays that are accessible in a statepoint are already in NumPy arrays that can be reshaped and dumped to MATLAB in one step.

Particle Track Visualization

_images/Tracks.png

OpenMC can dump particle tracks—the position of particles as they are transported through the geometry. There are two ways to make OpenMC output tracks: all particle tracks through a command line argument or specific particle tracks through settings.xml.

Running openmc with the argument -t or --track will cause a track file to be created for every particle transported in the code. Be careful as this will produce as many files as there are source particles in your simulation. To identify a specific particle for which a track should be created, set the Settings.track attribute to a tuple containing the batch, generation, and particle number of the desired particle. For example, to create a track file for particle 4 of batch 1 and generation 2:

settings = openmc.Settings()
settings.track = (1, 2, 4)

To specify multiple particles, the length of the iterable should be a multiple of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2:

settings.track = (1, 2, 3, 1, 2, 4)

After running OpenMC, the working directory will contain a file of the form “track_(batch #)_(generation #)_(particle #).h5” for each particle tracked. These track files can be converted into VTK poly data files with the openmc-track-to-vtk script.

Source Site Processing

For eigenvalue problems, OpenMC will store information on the fission source sites in the statepoint file by default. For each source site, the weight, position, sampled direction, and sampled energy are stored. To extract this data from a statepoint file, the openmc.statepoint module can be used. An example notebook demontrates how to analyze and plot source information.

Running in Parallel

If you are running a simulation on a computer with multiple cores, multiple sockets, or multiple nodes (i.e., a cluster), you can benefit from the fact that OpenMC is able to use all available hardware resources if configured correctly. OpenMC is capable of using both distributed-memory (MPI) and shared-memory (OpenMP) parallelism. If you are on a single-socket workstation or a laptop, using shared-memory parallelism is likely sufficient. On a multi-socket node, cluster, or supercomputer, chances are you will need to use both distributed-memory (across nodes) and shared-memory (within a single node) parallelism.

Shared-Memory Parallelism (OpenMP)

When using OpenMP, multiple threads will be launched and each is capable of simulating a particle independently of all other threads. The primary benefit of using OpenMP within a node is that it requires very little extra memory per thread. OpenMP can be turned on or off at configure-time; by default it is turned on. The only requirement is that the C++ compiler you use must support the OpenMP 3.1 or higher standard. Most recent compilers do support the use of OpenMP.

To specify the number of threads at run-time, you can use the threads argument to openmc.run():

openmc.run(threads=8)

If you’re running openmc directly from the command line, you can use the -s or --threads command-line argument. Alternatively, you can use the OMP_NUM_THREADS environment variable. If you do not specify the number of threads, the OpenMP library will try to determine how many hardware threads are available on your system and use that many threads.

In general, it is recommended to use as many OpenMP threads as you have hardware threads on your system. Notably, on a system with Intel hyperthreading, the hyperthreads should be used and can be expected to provide a 10–30% performance improvement over not using hyperthreads.

Distributed-Memory Parallelism (MPI)

MPI defines a library specification for message-passing between processes. There are two major implementations of MPI, OpenMPI and MPICH. Both implementations are known to work with OpenMC; there is no obvious reason to prefer one over the other. Building OpenMC with support for MPI requires that you have one of these implementations installed on your system. For instructions on obtaining MPI, see Prerequisites. Once you have an MPI implementation installed, compile OpenMC following Compiling with MPI.

To run a simulation using MPI, openmc needs to be called using the mpiexec wrapper. For example, to run OpenMC using 32 processes:

mpiexec -n 32 openmc

The same thing can be achieved from the Python API by supplying the mpi_args argument to openmc.run():

openmc.run(mpi_args=['mpiexec', '-n', '32'])

Maximizing Performance

There are a number of things you can do to ensure that you obtain optimal performance on a machine when running in parallel:

  • Use OpenMP within each NUMA node. Some large server processors have so many cores that the last level cache is split to reduce memory latency. For example, the Intel Xeon Haswell-EP architecture uses a snoop mode called cluster on die where the L3 cache is split in half. Thus, in general, you should use one MPI process per socket (and OpenMP within each socket), but for these large processors, you will want to go one step further and use one process per NUMA node. The Xeon Phi Knights Landing architecture uses a similar concept called sub NUMA clustering.

  • Use a sufficiently large number of particles per generation. Between fission generations, a number of synchronization tasks take place. If the number of particles per generation is too low and you are using many processes/threads, the synchronization time may become non-negligible.

  • Use hardware threading if available.

  • Use process binding. When running with MPI, you should ensure that processes are bound to a specific hardware region. This can be set using the -bind-to (MPICH) or --bind-to (OpenMPI) option to mpiexec.

  • Turn off generation of tallies.out. For large simulations with millions of tally bins or more, generating this ASCII file might consume considerable time. You can turn off generation of tallies.out via the Settings.output attribute:

    settings = openmc.Settings()
    settings.output = {'tallies': False}
    

Stochastic Volume Calculations

OpenMC has a capability to stochastically determine volumes of cells, materials, and universes. The method works by overlaying a bounding box, sampling points from within the box, and seeing what fraction of points were found in a desired domain. The benefit of doing this stochastically (as opposed to equally-spaced points), is that it is possible to give reliable error estimates on each stochastic quantity.

To specify that a volume calculation be run, you first need to create an instance of openmc.VolumeCalculation. The constructor takes a list of cells, materials, or universes; the number of samples to be used; and the lower-left and upper-right Cartesian coordinates of a bounding box that encloses the specified domains:

lower_left = (-0.62, -0.62, -50.)
upper_right = (0.62, 0.62, 50.)
vol_calc = openmc.VolumeCalculation([fuel, clad, moderator], 1000000,
                                    lower_left, upper_right)

For domains contained within regions that have simple definitions, OpenMC can sometimes automatically determine a bounding box. In this case, the last two arguments are not necessary. For example,

sphere = openmc.Sphere(r=10.0)
cell = openm.Cell(region=-sphere)
vol_calc = openmc.VolumeCalculation([cell], 1000000)

Of course, the volumes that you need this capability for are often the ones with complex definitions.

A threshold can be applied for the calculation’s variance, standard deviation, or relative error of volume estimates using openmc.VolumeCalculation.set_trigger():

vol_calc.set_trigger(1e-05, 'std_dev')

If a threshold is provided, calculations will be performed iteratively using the number of samples specified on the calculation until all volume uncertainties are below the threshold value. If no threshold is provided, the calculation will run the number of samples specified once and return the result.

Once you have one or more openmc.VolumeCalculation objects created, you can then assign then to Settings.volume_calculations:

settings = openmc.Settings()
settings.volume_calculations = [cell_vol_calc, mat_vol_calc]

To execute the volume calculations, one can either set Settings.run_mode to ‘volume’ and run openmc.run(), or alternatively run openmc.calculate_volumes() which doesn’t require that Settings.run_mode be set.

When your volume calculations have finished, you can load the results using the VolumeCalculation.load_results() method on an existing object. If you don’t have an existing VolumeCalculation object, you can create one and load results simultaneously using the VolumeCalculation.from_hdf5() class method:

vol_calc = openmc.VolumeCalculation(...)
...
openmc.calculate_volumes()
vol_calc.load_results('volume_1.h5')

# ..or we can create a new object
vol_calc = openmc.VolumeCalculation.from_hdf5('volume_1.h5')

After the results are loaded, volume estimates will be stored in VolumeCalculation.volumes. There is also a VolumeCalculation.atoms_dataframe attribute that shows stochastic estimates of the number of atoms of each type of nuclide within the specified domains along with their uncertainties.

Troubleshooting

Problems with Compilation

If you are experiencing problems trying to compile OpenMC, first check if the error you are receiving is among the following options.

Problems with Simulations

Segmentation Fault

A segmentation fault occurs when the program tries to access a variable in memory that was outside the memory allocated for the program. The best way to debug a segmentation fault is to re-compile OpenMC with debug options turned on. Create a new build directory and type the following commands:

mkdir build-debug && cd build-debug
cmake -Ddebug=on /path/to/openmc
make

Now when you re-run your problem, it should report exactly where the program failed. If after reading the debug output, you are still unsure why the program failed, post a message on the OpenMC Discourse Forum.

ERROR: No cross_sections.xml file was specified in settings.xml or in the OPENMC_CROSS_SECTIONS environment variable.

OpenMC needs to know where to find cross section data for each nuclide. Information on what data is available and in what files is summarized in a cross_sections.xml file. You need to tell OpenMC where to find the cross_sections.xml file either with the <cross_sections> Element in settings.xml or with the OPENMC_CROSS_SECTIONS environment variable. It is recommended to add a line in your .profile or .bash_profile setting the OPENMC_CROSS_SECTIONS environment variable.

Geometry Debugging
Overlapping Cells

For fast run times, normal simulations do not check if the geometry is incorrectly defined to have overlapping cells. This can lead to incorrect results that may or may not be obvious when there are errors in the geometry input file. The built-in 2D and 3D plotters will check for cell overlaps at the center of every pixel or voxel position they process, however this might not be a sufficient check to ensure correctly defined geometry. For instance, if an overlap is of small aspect ratio, the plotting resolution might not be high enough to produce any pixels in the overlapping area.

To reliably validate a geometry input, it is best to run the problem in geometry debugging mode with the -g, -geometry-debug, or --geometry-debug command-line options. This will enable checks for overlapping cells at every move of esch simulated particle. Depending on the complexity of the geometry input file, this could add considerable overhead to the run (these runs can still be done in parallel). As a result, for this run mode the user will probably want to run fewer particles than a normal simulation run. In this case it is important to be aware of how much coverage each area of the geometry is getting. For instance, if certain regions do not have many particles travelling through them there will not be many locations where overlaps are checked for in that region. The user should refer to the output after a geometry debug run to see how many checks were performed in each cell, and then adjust the number of starting particles or starting source distributions accordingly to achieve good coverage.

ERROR: After particle __ crossed surface __ it could not be located in any cell and it did not leak.

This error can arise either if a problem is specified with no boundary conditions or if there is an error in the geometry itself. First check to ensure that all of the outer surfaces of your geometry have been given vacuum or reflective boundary conditions. If proper boundary conditions have been applied and you still receive this error, it means that a surface/cell/lattice in your geometry has been specified incorrectly or is missing.

The best way to debug this error is to turn on a trace for the particle getting lost. After the error message, the code will display what batch, generation, and particle number caused the error. In your settings.xml, add a <trace> Element followed by the batch, generation, and particle number. This will give you detailed output every time that particle enters a cell, crosses a boundary, or has a collision. For example, if you received this error at cycle 5, generation 1, particle 4032, you would enter:

<trace>5 1 4032</trace>

For large runs it is often advantageous to run only the offending particle by using particle restart mode with the -r command-line option in conjunction with the particle restart files that are created when particles are lost with this error.

Developer’s Guide

Welcome to the OpenMC Developer’s Guide! This guide documents how contributions are made to OpenMC, what style rules exist for the code, how to run tests, and other related topics.

Contributing to OpenMC

Thank you for considering contributing to OpenMC! We look forward to welcoming new members to the community and will do our best to help you get up to speed. The purpose of this section is to document how the project is managed: how contributions (bug fixes, enhancements, new features) are made, how they are evaluated, who is permitted to merge pull requests, and what happens in the event of disagreements. Once you have read through this section, the Development Workflow section outlines the actual mechanics of making a contribution (forking, submitting a pull request, etc.).

The goal of our governance model is to:

  • Encourage new contributions.
  • Encourage contributors to remain involved.
  • Avoid unnecessary processes and bureaucracy whenever possible.
  • Create a transparent decision making process which makes it clear how contributors can be involved in decision making.

Overview

OpenMC uses a liberal contribution model for project governance. Anyone involved in development in a non-trivial capacity is given an opportunity to influence the direction of the project. Project decisions are made through a consensus-seeking process rather than by voting.

Terminology

  • A Contributor is any individual creating or commenting on an issue or pull request.
  • A Committer is a subset of contributors who are authorized to review and merge pull requests.
  • The TC (Technical Committee) is a group of committers who have the authority to make decisions on behalf of the project team in order to resolve disputes.
  • The Project Lead is a single individual who has the authority to make a final decision when the TC is unable to reach consensus.

Contribution Process

Any change to the OpenMC repository must be made through a pull request (PR). This applies to all changes to documentation, code, binary files, etc. Even long term committers and TC members must use pull requests.

No pull request may be merged without being independently reviewed.

For non-trivial contributions, pull requests should not be merged for at least 36 hours to ensure that contributors in other timezones have time to review. Consideration should be given to weekends and other holiday periods to ensure active committers have reasonable time to become involved in the discussion and review process if they wish. Any committer may request that the review period be extended if they are unable to review the change within 36 hours.

During review, a committer may request that a specific contributor who is most versed in a particular area review the PR before it can be merged.

A pull request can be merged by any committer, but only if no objections are raised by any other committer. In the case of an objection being raised, all involved committers should seek consensus through discussion and compromise.

In the case of an objection being raised in a pull request by another committer, all involved committers should seek to arrive at a consensus by way of addressing concerns being expressed through discussion, compromise on the proposed change, or withdrawal of the proposed change.

If objections to a PR are made and committers cannot reach a consensus on how to proceed, the decision is escalated to the TC. TC members should regularly discuss pending contributions in order to find a resolution. It is expected that only a small minority of issues be brought to the TC for resolution and that discussion and compromise among committers be the default resolution mechanism.

Becoming a Committer

All contributors who make a non-trivial contribution will be added as a committer in a timely manner. Committers are expected to follow this policy.

TC Process

Any issues brought to the TC will be addressed among the committee with a consensus-seeking process. The group tries to find a resolution that has no objections among TC members. If a consensus cannot be reached, the Project Lead has the ultimate authority to make a final decision. It is expected that the majority of decisions made by the TC are via a consensus seeking process and that the Project Lead intercedes only as a last resort.

Resolution may involve returning the issue to committers with suggestions on how to move forward towards a consensus.

Members can be added to the TC at any time. Any committer can nominate another committer to the TC and the TC uses its standard consensus seeking process to evaluate whether or not to add this new member. Members who do not participate consistently at the level of a majority of the other members are expected to resign.

In the event that the Project Lead resigns or otherwise steps down, the TC uses a consensus seeking process to choose a new Project Lead.

Leadership Team

The TC consists of the following individuals:

The Project Lead is Paul Romano.

Next Steps

If you are interested in working on a specific feature or helping to address outstanding issues, consider joining the developer’s mailing list and/or Slack community. Note that some issues have specifically been labeled as good for first-time contributors. Once you’re at the point of writing code, make sure your read through the Development Workflow section to understand the mechanics of making pull requests and what is expected during code reviews.

Development Workflow

Anyone wishing to make contributions to OpenMC should be fully acquianted and comfortable working with git and GitHub. We assume here that you have git installed on your system, have a GitHub account, and have setup SSH keys to be able to create/push to repositories on GitHub.

Overview

Development of OpenMC relies heavily on branching; specifically, we use a branching model sometimes referred to as git flow. If you plan to contribute to OpenMC development, we highly recommend that you read the linked blog post to get a sense of how the branching model works. There are two main branches that always exist: master and develop. The master branch is a stable branch that contains the latest release of the code. The develop branch is where any ongoing development takes place prior to a release and is not guaranteed to be stable. When the development team decides that a release should occur, the develop branch is merged into master.

All new features, enhancements, and bug fixes should be developed on a branch that branches off of develop. When the feature is completed, a pull request is initiated on GitHub that is then reviewed by a committer. If the pull request is satisfactory, it is then merged into develop. Note that a committer may not review their own pull request (i.e., an independent code review is required).

Code Review Criteria

In order to be considered suitable for inclusion in the develop branch, the following criteria must be satisfied for all proposed changes:

  • Changes have a clear purpose and are useful.
  • Compiles and passes all tests under multiple build configurations (This is checked by Travis CI).
  • If appropriate, test cases are added to regression or unit test suites.
  • No memory leaks (checked with valgrind).
  • Conforms to the OpenMC style guide.
  • No degradation of performance or greatly increased memory usage. This is not a hard rule – in certain circumstances, a performance loss might be acceptable if there are compelling reasons.
  • New features/input are documented.
  • No unnecessary external software dependencies are introduced.

Contributing

Now that you understand the basic development workflow, let’s discuss how an individual can contribute to development. Note that this would apply to both new features and bug fixes. The general steps for contributing are as follows:

  1. Fork the main openmc repository from openmc-dev/openmc. This will create a repository with the same name under your personal account. As such, you can commit to it as you please without disrupting other developers.

    _images/fork.png
  2. Clone your fork of OpenMC and create a branch that branches off of develop:

    git clone --recurse-submodules git@github.com:yourusername/openmc.git
    cd openmc
    git checkout -b newbranch develop
    
  3. Make your changes on the new branch that you intend to have included in develop. If you have made other changes that should not be merged back, ensure that those changes are made on a different branch.

  4. Issue a pull request from GitHub and select the develop branch of openmc-dev/openmc as the target.

    At a minimum, you should describe what the changes you’ve made are and why you are making them. If the changes are related to an oustanding issue, make sure it is cross-referenced.

  5. A committer will review your pull request based on the criteria above. Any issues with the pull request can be discussed directly on the pull request page itself.

  6. After the pull request has been thoroughly vetted, it is merged back into the develop branch of openmc-dev/openmc.

Private Development

While the process above depends on the fork of the OpenMC repository being publicly available on GitHub, you may also wish to do development on a private repository for research or commercial purposes. The proper way to do this is to create a complete copy of the OpenMC repository (not a fork from GitHub). The private repository can then either be stored just locally or in conjunction with a private repository on Github (this requires a paid plan). Alternatively, Bitbucket offers private repositories for free. If you want to merge some changes you’ve made in your private repository back to openmc-dev/openmc repository, simply follow the steps above with an extra step of pulling a branch from your private repository into a public fork.

Working in “Development” Mode

If you are making changes to the Python API during development, it is highly suggested to install the Python API in development/editable mode using pip. From the root directory of the OpenMC repository, run:

pip install -e .[test]

This installs the OpenMC Python package in “editable” mode so that 1) it can be imported from a Python interpreter and 2) any changes made are immediately reflected in the installed version (that is, you don’t need to keep reinstalling it). While the same effect can be achieved using the PYTHONPATH environment variable, this is generally discouraged as it can interfere with virtual environments.

Style Guide for OpenMC

In order to keep the OpenMC code base consistent in style, this guide specifies a number of rules which should be adhered to when modified existing code or adding new code in OpenMC.

C++

Important

To ensure consistent styling with little effort, this project uses clang-format. The repository contains a .clang-format file that can be used to automatically apply the style rules that are described below. The easiest way to use clang-format is through a plugin/extension for your editor/IDE that automatically runs clang-format using the .clang-format file whenever a file is saved.

Indentation

Use two spaces per indentation level.

Miscellaneous

Follow the C++ Core Guidelines except when they conflict with another guideline listed here. For convenience, many important guidelines from that list are repeated here.

Conform to the C++14 standard.

Always use C++-style comments (//) as opposed to C-style (/**/). (It is more difficult to comment out a large section of code that uses C-style comments.)

Do not use C-style casting. Always use the C++-style casts static_cast, const_cast, or reinterpret_cast. (See ES.49)

Source Files

Use a .cpp suffix for code files and .h for header files.

Header files should always use include guards with the following style (See SF.8):

#ifndef OPENMC_MODULE_NAME_H
#define OPENMC_MODULE_NAME_H

namespace openmc {
...
content
...
}

#endif // OPENMC_MODULE_NAME_H

Avoid hidden dependencies by always including a related header file first, followed by C/C++ library includes, other library includes, and then local includes. For example:

// foo.cpp
#include "foo.h"

#include <cstddef>
#include <iostream>
#include <vector>

#include "hdf5.h"
#include "pugixml.hpp"

#include "error.h"
#include "random_lcg.h"
Naming

Struct and class names should be CamelCase, e.g. HexLattice.

Functions (including member functions) should be lower-case with underscores, e.g. get_indices.

Local variables, global variables, and struct/class member variables should be lower-case with underscores (e.g., n_cells) except for physics symbols that are written differently by convention (e.g., E for energy). Data members of classes (but not structs) additionally have trailing underscores (e.g., a_class_member_).

The following conventions are used for variables with short names:

  • d stands for “distance”
  • E stands for “energy”
  • p stands for “particle”
  • r stands for “position”
  • rx stands for “reaction”
  • u stands for “direction”
  • xs stands for “cross section”

All classes and non-member functions should be declared within the openmc namespace. Global variables must be declared in a namespace nested within the openmc namespace. The following sub-namespaces are in use:

  • openmc::data: Fundamental nuclear data (cross sections, multigroup data, decay constants, etc.)
  • openmc::model: Variables related to geometry, materials, and tallies
  • openmc::settings: Global settings / options
  • openmc::simulation: Variables used only during a simulation

Accessors and mutators (get and set functions) may be named like variables. These often correspond to actual member variables, but this is not required. For example, int count() and void set_count(int count).

Variables declared constexpr or const that have static storage duration (exist for the duration of the program) should be upper-case with underscores, e.g., SQRT_PI.

Use C++-style declarator layout (see NL.18): pointer and reference operators in declarations should be placed adject to the base type rather than the variable name. Avoid declaring multiple names in a single declaration to avoid confusion:

T* p; // good
T& p; // good
T *p; // bad
T* p, q; // misleading
Curly braces

For a class declaration, the opening brace should be on the same line that lists the name of the class.

class Matrix {
  ...
};

For a function definition, the opening and closing braces should each be on their own lines. This helps distinguish function code from the argument list. If the entire function fits on one or two lines, then the braces can be on the same line. e.g.:

return_type function(type1 arg1, type2 arg2)
{
  content();
}

return_type
function_with_many_args(type1 arg1, type2 arg2, type3 arg3,
                        type4 arg4)
{
  content();
}

int return_one() {return 1;}

int return_one()
{return 1;}

For a conditional, the opening brace should be on the same line as the end of the conditional statement. If there is a following else if or else statement, the closing brace should be on the same line as that following statement. Otherwise, the closing brace should be on its own line. A one-line conditional can have the closing brace on the same line or it can omit the braces entirely e.g.:

if (condition) {
  content();
}

if (condition1) {
  content();
} else if (condition 2) {
  more_content();
} else {
  further_content();
}

if (condition) {content()};

if (condition) content();

For loops similarly have an opening brace on the same line as the statement and a closing brace on its own line. One-line loops may have the closing brace on the same line or omit the braces entirely.

for (int i = 0; i < 5; i++) {
  content();
}

for (int i = 0; i < 5; i++) {content();}

for (int i = 0; i < 5; i++) content();
Documentation

Classes, structs, and functions are to be annotated for the Doxygen documentation generation tool. Use the \ form of Doxygen commands, e.g., \brief instead of @brief.

Python

Style for Python code should follow PEP8.

Docstrings for functions and methods should follow numpydoc style.

Python code should work with Python 3.4+.

Use of third-party Python packages should be limited to numpy, scipy, matplotlib, pandas, and h5py. Use of other third-party packages must be implemented as optional dependencies rather than required dependencies.

Prefer pathlib when working with filesystem paths over functions in the os module or other standard-library modules. Functions that accept arguments that represent a filesystem path should work with both strings and Path objects.

Test Suite

The OpenMC test suite consists of two parts, a regression test suite and a unit test suite. The regression test suite is based on regression or integrated testing where different types of input files are configured and the full OpenMC code is executed. Results from simulations are compared with expected results. The unit tests are primarily intended to test individual functions/classes in the OpenMC Python API.

Prerequisites

  • The test suite relies on the third-party pytest package. To run either or both the regression and unit test suites, it is assumed that you have OpenMC fully installed, i.e., the openmc executable is available on your PATH and the openmc Python module is importable. In development where it would be onerous to continually install OpenMC every time a small change is made, it is recommended to install OpenMC in development/editable mode. With setuptools, this is accomplished by running:

    python setup.py develop
    

    or using pip (recommended):

    pip install -e .[test]
    
  • The test suite requires a specific set of cross section data in order for tests to pass. A download URL for the data that OpenMC expects can be found within tools/ci/download-xs.sh.

  • In addition to the HDF5 data, some tests rely on ENDF files. A download URL for those can also be found in tools/ci/download-xs.sh.

  • Some tests require NJOY to preprocess cross section data. The test suite assumes that you have an njoy executable available on your PATH.

Running Tests

To execute the test suite, go to the tests/ directory and run:

pytest

If you want to collect information about source line coverage in the Python API, you must have the pytest-cov plugin installed and run:

pytest --cov=../openmc --cov-report=html

Generating XML Inputs

Many of the regression tests rely on the Python API to build an appropriate model. However, it can sometimes be desirable to work directly with the XML input files rather than having to run a script in order to run the problem/test. To build the input files for a test without actually running the test, you can run:

pytest --build-inputs <name-of-test>

Adding Tests to the Regression Suite

To add a new test to the regression test suite, create a sub-directory in the tests/regression_tests/ directory. To configure a test you need to add the following files to your new test directory:

  • OpenMC input XML files, if they are not generated through the Python API
  • test.py - Python test driver script; please refer to other tests to see how to construct. Any output files that are generated during testing must be removed at the end of this script.
  • inputs_true.dat - ASCII file that contains Python API-generated XML files concatenated together. When the test is run, inputs that are generated are compared to this file.
  • results_true.dat - ASCII file that contains the expected results from the test. The file results_test.dat is compared to this file during the execution of the python test driver script. When the above files have been created, generate a results_test.dat file and copy it to this name and commit. It should be noted that this file should be generated with basic compiler options during openmc configuration and build (e.g., no MPI, no debug/optimization).

In addition to this description, please see the various types of tests that are already included in the test suite to see how to create them. If all is implemented correctly, the new test will automatically be discovered by pytest.

Making User Input Changes

Users are encouraged to use OpenMC’s Python API to build XML files that the OpenMC solver then reads during the initialization phase. Thus, to modify, add, or remove user input options, changes must be made both within the Python API and the C++ source that reads XML files produced by the Python API. The following steps should be followed to make changes to user input:

  1. Determine the Python class you need to change. For example, if you are adding a new setting, you probably want to change the openmc.Settings class. If you are adding a new surface type, you would need to create a subclass of openmc.Surface.

  2. To add a new option, the class will need a property attribute. For example, if you wanted to add a “fast_mode” setting, you would need two methods that look like:

    @property
    def fast_mode(self):
        ...
    
    @fast_mode.setter
    def fast_mode(self, fast_mode):
        ...
    
  3. Make sure that when an instance of the class is exported to XML (usually through a export_to_xml() or to_xml_element() method), a new element is written to the appropriate file. OpenMC uses the xml.etree.ElementTree API, so refer to the documentation of that module for guidance on creating elements/attributes.

  4. Make sure that your input can be categorized as one of the datatypes from XML Schema Part 2 and that parsing of the data appropriately reflects this. For example, for a boolean value, true can be represented either by “true” or by “1”.

  5. Now that you’re done with the Python side, you need to make modifications to the C++ codebase. Make appropriate changes in source files (e.g., settings.cpp). You should use convenience functions defined by xml_interface.cpp.

  6. If you’ve made changes in the geometry or materials, make sure they are written out to the statepoint or summary files and that the openmc.StatePoint and openmc.Summary classes read them in.

  7. Finally, a set of RELAX NG schemas exists that enables validation of input files. You should modify the RELAX NG schema for the file you changed. The easiest way to do this is to change the compact syntax file (e.g. src/relaxng/geometry.rnc) and then convert it to regular XML syntax using trang:

    trang geometry.rnc geometry.rng
    

For most user input additions and changes, it is simple enough to follow a “monkey see, monkey do” approach. When in doubt, contact your nearest OpenMC developer or send a message to the developers mailing list.

Building Sphinx Documentation

In order to build the documentation in the docs directory, you will need to have the Sphinx third-party Python package. The easiest way to install Sphinx is via pip:

pip install sphinx

Additionally, you will need several Sphinx extensions that can be installed directly with pip:

pip install sphinx-numfig
pip install sphinxcontrib-katex
pip install sphinxcontrib-svg2pdfconverter

Building Documentation as a Webpage

To build the documentation as a webpage (what appears at https://docs.openmc.org), simply go to the docs directory and run:

make html

Building Documentation as a PDF

To build PDF documentation, you will need to have a LaTeX distribution installed on your computer. Once you have a LaTeX distribution installed, simply go to the docs directory and run:

make latexpdf

Deployment with Docker

OpenMC can be easily deployed using Docker on any Windows, Mac or Linux system. With Docker running, execute the following command in the shell to build a Docker image called debian/openmc:latest:

docker build -t debian/openmc:latest https://github.com/openmc-dev/openmc.git#develop

Note

This may take 5 – 10 minutes to run to completion.

This command will execute the instructions in OpenMC’s Dockerfile to build a Docker image with OpenMC installed. The image includes OpenMC with MPICH and parallel HDF5 in the /opt/openmc directory, and Miniconda3 with all of the Python pre-requisites (NumPy, SciPy, Pandas, etc.) installed. The NJOY2016 codebase is installed in /opt/NJOY2016 to support full functionality and testing of the openmc.data Python module. The publicly available nuclear data libraries necessary to run OpenMC’s test suite – including NNDC and WMP cross sections and ENDF data – are in the /opt/openmc/data directory, and the corresponding OPENMC_CROSS_SECTIONS, OPENMC_MULTIPOLE_LIBRARY, and OPENMC_ENDF_DATA environment variables are initialized.

After building the Docker image, you can run the following to see the names of all images on your machine, including debian/openmc:latest:

docker image ls

Now you can run the following to create a Docker container called my_openmc based on the debian/openmc:latest image:

docker run -it --name=my_openmc debian/openmc:latest

This command will open an interactive shell running from within the Docker container where you have access to use OpenMC.

Note

The docker run command supports many options for spawning containers – including mounting volumes from the host filesystem – which many users will find useful.

Python API

OpenMC includes a rich Python API that enables programmatic pre- and post-processing. The easiest way to begin using the API is to take a look at the Examples. This assumes that you are already familiar with Python and common third-party packages such as NumPy. If you have never used Python before, the prospect of learning a new code and a programming language might sound daunting. However, you should keep in mind that there are many substantial benefits to using the Python API, including:

  • The ability to define dimensions using variables.
  • Availability of standard-library modules for working with files.
  • An entire ecosystem of third-party packages for scientific computing.
  • Automated multi-group cross section generation (openmc.mgxs)
  • A fully-featured nuclear data interface (openmc.data)
  • Depletion capability (openmc.deplete)
  • Convenience functions (e.g., a function returning a hexagonal region)
  • Ability to plot individual universes as geometry is being created
  • A \(k_\text{eff}\) search function (openmc.search_for_keff())
  • Random sphere packing for generating TRISO particle locations (openmc.model.pack_spheres())
  • Ability to create materials based on natural elements or uranium enrichment

For those new to Python, there are many good tutorials available online. We recommend going through the modules from Codecademy and/or the Scipy lectures.

The full API documentation serves to provide more information on a given module or class.

Tip

Users are strongly encouraged to use the Python API to generate input files and analyze results.

Modules

openmc – Basic Functionality

Handling nuclear data

openmc.XSdata A multi-group cross section data set providing all the multi-group data necessary for a multi-group OpenMC calculation.
openmc.MGXSLibrary Multi-Group Cross Sections file used for an OpenMC simulation.

Simulation Settings

openmc.Source Distribution of phase space coordinates for source sites.
openmc.SourceParticle Source particle
openmc.VolumeCalculation Stochastic volume calculation specifications and results.
openmc.Settings Settings used for an OpenMC simulation.

The following function can be used for generating a source file:

openmc.write_source_file Write a source file using a collection of source particles

Material Specification

openmc.Nuclide A nuclide that can be used in a material.
openmc.Element A natural element that auto-expands to add the isotopes of an element to a material in their natural abundance.
openmc.Macroscopic A Macroscopic object that can be used in a material.
openmc.Material A material composed of a collection of nuclides/elements.
openmc.Materials Collection of Materials used for an OpenMC simulation.

Cross sections for nuclides, elements, and materials can be plotted using the following function:

openmc.plot_xs Creates a figure of continuous-energy cross sections for this item.

Building geometry

openmc.Plane An arbitrary plane of the form \(Ax + By + Cz = D\).
openmc.XPlane A plane perpendicular to the x axis of the form \(x - x_0 = 0\)
openmc.YPlane A plane perpendicular to the y axis of the form \(y - y_0 = 0\)
openmc.ZPlane A plane perpendicular to the z axis of the form \(z - z_0 = 0\)
openmc.XCylinder An infinite cylinder whose length is parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = r^2\).
openmc.YCylinder An infinite cylinder whose length is parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = r^2\).
openmc.ZCylinder An infinite cylinder whose length is parallel to the z-axis of the form \((x - x_0)^2 + (y - y_0)^2 = r^2\).
openmc.Sphere A sphere of the form \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2\).
openmc.Cone A conical surface parallel to the x-, y-, or z-axis.
openmc.XCone A cone parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2\).
openmc.YCone A cone parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2\).
openmc.ZCone A cone parallel to the x-axis of the form \((x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2\).
openmc.Quadric A surface of the form \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\).
openmc.Halfspace A positive or negative half-space region.
openmc.Intersection Intersection of two or more regions.
openmc.Union Union of two or more regions.
openmc.Complement Complement of a region.
openmc.Cell A region of space defined as the intersection of half-space created by quadric surfaces.
openmc.Universe A collection of cells that can be repeated.
openmc.RectLattice A lattice consisting of rectangular prisms.
openmc.HexLattice A lattice consisting of hexagonal prisms.
openmc.Geometry Geometry representing a collection of surfaces, cells, and universes.

Many of the above classes are derived from several abstract classes:

openmc.Surface An implicit surface with an associated boundary condition.
openmc.Region Region of space that can be assigned to a cell.
openmc.Lattice A repeating structure wherein each element is a universe.

Constructing Tallies

openmc.Filter Tally modifier that describes phase-space and other characteristics.
openmc.UniverseFilter Bins tally event locations based on the Universe they occured in.
openmc.MaterialFilter Bins tally event locations based on the Material they occured in.
openmc.CellFilter Bins tally event locations based on the Cell they occured in.
openmc.CellFromFilter Bins tally on which Cell the neutron came from.
openmc.CellbornFilter Bins tally events based on which Cell the neutron was born in.
openmc.CellInstanceFilter Bins tally events based on which cell instance a particle is in.
openmc.SurfaceFilter Filters particles by surface crossing
openmc.MeshFilter Bins tally event locations onto a regular, rectangular mesh.
openmc.MeshSurfaceFilter Filter events by surface crossings on a regular, rectangular mesh.
openmc.EnergyFilter Bins tally events based on incident particle energy.
openmc.EnergyoutFilter Bins tally events based on outgoing particle energy.
openmc.MuFilter Bins tally events based on particle scattering angle.
openmc.PolarFilter Bins tally events based on the incident particle’s direction.
openmc.AzimuthalFilter Bins tally events based on the incident particle’s direction.
openmc.DistribcellFilter Bins tally event locations on instances of repeated cells.
openmc.DelayedGroupFilter Bins fission events based on the produced neutron precursor groups.
openmc.EnergyFunctionFilter Multiplies tally scores by an arbitrary function of incident energy.
openmc.LegendreFilter Score Legendre expansion moments up to specified order.
openmc.SpatialLegendreFilter Score Legendre expansion moments in space up to specified order.
openmc.SphericalHarmonicsFilter Score spherical harmonic expansion moments up to specified order.
openmc.ZernikeFilter Score Zernike expansion moments in space up to specified order.
openmc.ZernikeRadialFilter Score the \(m = 0\) (radial variation only) Zernike moments up to specified order.
openmc.ParticleFilter Bins tally events based on the Particle type.
openmc.RegularMesh A regular Cartesian mesh in one, two, or three dimensions
openmc.RectilinearMesh A 3D rectilinear Cartesian mesh
openmc.UnstructuredMesh A 3D unstructured mesh
openmc.Trigger A criterion for when to finish a simulation based on tally uncertainties.
openmc.TallyDerivative A material perturbation derivative to apply to a tally.
openmc.Tally A tally defined by a set of scores that are accumulated for a list of nuclides given a set of filters.
openmc.Tallies Collection of Tallies used for an OpenMC simulation.

Geometry Plotting

openmc.Plot Definition of a finite region of space to be plotted.
openmc.Plots Collection of Plots used for an OpenMC simulation.

Running OpenMC

openmc.run Run an OpenMC simulation.
openmc.calculate_volumes Run stochastic volume calculations in OpenMC.
openmc.plot_geometry Run OpenMC in plotting mode
openmc.plot_inline Display plots inline in a Jupyter notebook.
openmc.search_for_keff Function to perform a keff search by modifying a model parametrized by a single independent variable.

Post-processing

openmc.Particle Information used to restart a specific particle that caused a simulation to fail.
openmc.StatePoint State information on a simulation at a certain point in time (at the end of a given batch).
openmc.Summary Summary of geometry, materials, and tallies used in a simulation.

The following classes and functions are used for functional expansion reconstruction.

openmc.ZernikeRadial Create radial only Zernike polynomials given coefficients and domain.
openmc.legendre_from_expcoef Return a Legendre series object based on expansion coefficients.

Various classes may be created when performing tally slicing and/or arithmetic:

openmc.arithmetic.CrossScore A special-purpose tally score used to encapsulate all combinations of two tally’s scores as an outer product for tally arithmetic.
openmc.arithmetic.CrossNuclide A special-purpose nuclide used to encapsulate all combinations of two tally’s nuclides as an outer product for tally arithmetic.
openmc.arithmetic.CrossFilter A special-purpose filter used to encapsulate all combinations of two tally’s filter bins as an outer product for tally arithmetic.
openmc.arithmetic.AggregateScore A special-purpose tally score used to encapsulate an aggregate of a subset or all of tally’s scores for tally aggregation.
openmc.arithmetic.AggregateNuclide A special-purpose tally nuclide used to encapsulate an aggregate of a subset or all of tally’s nuclides for tally aggregation.
openmc.arithmetic.AggregateFilter A special-purpose tally filter used to encapsulate an aggregate of a subset or all of a tally filter’s bins for tally aggregation.

Coarse Mesh Finite Difference Acceleration

CMFD is implemented in OpenMC and allows users to accelerate fission source convergence during inactive neutron batches. To use CMFD, the openmc.cmfd.CMFDRun class executes OpenMC through the C API, solving the CMFD system between fission generations and modifying the source weights. Note that the openmc.cmfd module is not imported by default with the openmc namespace and needs to be imported explicitly.

openmc.cmfd.CMFDMesh A structured Cartesian mesh used for CMFD acceleration.
openmc.cmfd.CMFDRun Class for running CMFD acceleration through the C API.

At the minimum, a CMFD mesh needs to be specified in order to run CMFD. Once the mesh and other optional properties are set, a simulation can be run with CMFD turned on using openmc.cmfd.CMFDRun.run().

openmc.model – Model Building

Convenience Functions

openmc.model.borated_water Return a Material with the composition of boron dissolved in water.
openmc.model.cylinder_from_points Return a cylinder given points that define the axis and a radius.
openmc.model.hexagonal_prism Create a hexagon region from six surface planes.
openmc.model.rectangular_prism Get an infinite rectangular prism from four planar surfaces.
openmc.model.subdivide Create regions separated by a series of surfaces.
openmc.model.pin Convenience function for building a fuel pin

Composite Surfaces

openmc.model.RectangularParallelepiped Rectangular parallelpiped composite surface
openmc.model.RightCircularCylinder Right circular cylinder composite surface
openmc.model.XConeOneSided One-sided cone parallel the x-axis
openmc.model.YConeOneSided One-sided cone parallel the y-axis
openmc.model.ZConeOneSided One-sided cone parallel the z-axis

TRISO Fuel Modeling

Classes
openmc.model.TRISO Tristructural-isotopic (TRISO) micro fuel particle
Functions
openmc.model.create_triso_lattice Create a lattice containing TRISO particles for optimized tracking.
openmc.model.pack_spheres Generate a random, non-overlapping configuration of spheres within a container.

Model Container

Classes
openmc.model.Model Model container.

openmc.examples – Example Models

Simple Models

openmc.examples.slab_mg Create a 1D slab model.

Reactor Models

openmc.examples.pwr_pin_cell Create a PWR pin-cell model.
openmc.examples.pwr_assembly Create a PWR assembly model.
openmc.examples.pwr_core Create a PWR full-core model.

openmc.deplete – Depletion

Primary API

The two primary requirements to perform depletion with openmc.deplete are:

  1. A transport operator
  2. A time-integration scheme

The former is responsible for executing a transport code, like OpenMC, and retaining important information required for depletion. The most common examples are reaction rates and power normalization data. The latter is responsible for projecting reaction rates and compositions forward in calendar time across some step size \(\Delta t\), and obtaining new compositions given a power or power density. The Operator is provided to handle communicating with OpenMC. Several classes are provided that implement different time-integration algorithms for depletion calculations, which are described in detail in Colin Josey’s thesis, Development and analysis of high order neutron transport-depletion coupling algorithms.

PredictorIntegrator Deplete using a first-order predictor algorithm.
CECMIntegrator Deplete using the CE/CM algorithm.
CELIIntegrator Deplete using the CE/LI CFQ4 algorithm.
CF4Integrator Deplete using the CF4 algorithm.
EPCRK4Integrator Deplete using the EPC-RK4 algorithm.
LEQIIntegrator Deplete using the LE/QI CFQ4 algorithm.
SICELIIntegrator Deplete using the SI-CE/LI CFQ4 algorithm.
SILEQIIntegrator Deplete using the SI-LE/QI CFQ4 algorithm.

Each of these classes expects a “transport operator” to be passed. An operator specific to OpenMC is available using the following class:

Operator OpenMC transport operator for depletion.

The Operator must also have some knowledge of how nuclides transmute and decay. This is handled by the Chain.

Minimal Example

A minimal example for performing depletion would be:

Internal Classes and Functions

When running in parallel using mpi4py, the MPI intercommunicator used can be changed by modifying the following module variable. If it is not explicitly modified, it defaults to mpi4py.MPI.COMM_WORLD.

openmc.deplete.comm

MPI intercommunicator used to call OpenMC library

Type:mpi4py.MPI.Comm

During a depletion calculation, the depletion chain, reaction rates, and number densities are managed through a series of internal classes that are not normally visible to a user. However, should you find yourself wondering about these classes (e.g., if you want to know what decay modes or reactions are present in a depletion chain), they are documented here. The following classes store data for a depletion chain:

Chain Full representation of a depletion chain.
DecayTuple Decay mode information
Nuclide Decay modes, reactions, and fission yields for a single nuclide.
ReactionTuple Transmutation reaction information
FissionYieldDistribution Energy-dependent fission product yields for a single nuclide
FissionYield Mapping for fission yields of a parent at a specific energy

The Chain class uses information from the following module variable:

chain.REACTIONS

Dictionary that maps transmutation reaction names to information needed when a chain is being generated: MT values, the change in atomic/mass numbers resulting from the reaction, and what secondaries are produced.

Type:dict

The following classes are used during a depletion simulation and store auxiliary data, such as number densities and reaction rates for each material.

AtomNumber Stores local material compositions (atoms of each nuclide).
OperatorResult Result of applying transport operator
ReactionRates Reaction rates resulting from a transport operator call
Results Output of a depletion run
ResultsList A list of openmc.deplete.Results objects

The following class and functions are used to solve the depletion equations, with cram.CRAM48() being the default.

cram.IPFCramSolver CRAM depletion solver that uses incomplete partial factorization
cram.CRAM16 Solve depletion equations using IPF CRAM
cram.CRAM48 Solve depletion equations using IPF CRAM
pool.deplete Deplete materials using given reaction rates for a specified time
pool.USE_MULTIPROCESSING

Boolean switch to enable or disable the use of multiprocessing when solving the Bateman equations. The default is to use multiprocessing, but can cause the simulation to hang in some computing environments, namely due to MPI and networking restrictions. Disabling this option will result in only a single CPU core being used for depletion.

Type:bool

The following classes are used to help the openmc.deplete.Operator compute quantities like effective fission yields, reaction rates, and total system energy.

helpers.AveragedFissionYieldHelper Class that computes fission yields based on average fission energy
helpers.ChainFissionHelper Computes normalization using fission Q values from depletion chain
helpers.ConstantFissionYieldHelper Class that uses a single set of fission yields on each isotope
helpers.DirectReactionRateHelper Class for generating one-group reaction rates with direct tallies
helpers.EnergyScoreHelper Class responsible for obtaining system energy via a tally score
helpers.FissionYieldCutoffHelper Helper that computes fission yields based on a cutoff energy
helpers.FluxCollapseHelper Class that generates one-group reaction rates using multigroup flux

Abstract Base Classes

A good starting point for extending capabilities in openmc.deplete is to examine the following abstract base classes. Custom classes can inherit from abc.TransportOperator to implement alternative schemes for collecting reaction rates and other data from a transport code prior to depleting materials

abc.TransportOperator Abstract class defining a transport operator

The following classes are abstract classes used to pass information from OpenMC simulations back on to the abc.TransportOperator

abc.NormalizationHelper Abstract class for obtaining normalization factor on tallies
abc.FissionYieldHelper Abstract class for processing energy dependent fission yields
abc.ReactionRateHelper Abstract class for generating reaction rates for operators
abc.TalliedFissionYieldHelper Abstract class for computing fission yields with tallies

Custom integrators or depletion solvers can be developed by subclassing from the following abstract base classes:

abc.Integrator Abstract class for solving the time-integration for depletion
abc.SIIntegrator Abstract class for the Stochastic Implicit Euler integrators
abc.DepSystemSolver Abstract class for solving depletion equations

openmc.mgxs – Multi-Group Cross Section Generation

Energy Groups

Module Variables
openmc.mgxs.GROUP_STRUCTURES
[SAR1990]Sartori, E., OECD/NEA Data Bank: Standard Energy Group Structures of Cross Section Libraries for Reactor Shielding, Reactor Cell and Fusion Neutronics Applications: VITAMIN-J, ECCO-33, ECCO-2000 and XMAS JEF/DOC-315 Revision 3 - DRAFT (December 11, 1990).
[SAN2004]Santamarina, A., Collignon, C., & Garat, C. (2004). French calculation schemes for light water reactor analysis. United States: American Nuclear Society - ANS.
[HFA2005]Hfaiedh, N. & Santamarina, A., “Determination of the Optimized SHEM Mesh for Neutron Transport Calculations,” Proc. Top. Mtg. in Mathematics & Computations, Supercomputing, Reactor Physics and Nuclear and Biological Applications, September 12-15, Avignon, France, 2005.
[SAN2007]Santamarina, A. & Hfaiedh, N. (2007). The SHEM energy mesh for accurate fuel depletion and BUC calculations. Proceedings of the International Conference on Safety Criticality ICNC 2007, St Peterburg (Russia), Vol. I pp. 446-452.
[HEB2008]Hébert, Alain & Santamarina, Alain. (2008). Refinement of the Santamarina-Hfaiedh energy mesh between 22.5 eV and 11.4 keV. International Conference on the Physics of Reactors 2008, PHYSOR 08. 2. 929-938.
Type:Dictionary of commonly used energy group structures
Classes
openmc.mgxs.EnergyGroups An energy groups structure used for multi-group cross-sections.

Multi-group Cross Sections

openmc.mgxs.MGXS An abstract multi-group cross section for some energy group structure within some spatial domain.
openmc.mgxs.MatrixMGXS An abstract multi-group cross section for some energy group structure within some spatial domain.
openmc.mgxs.AbsorptionXS An absorption multi-group cross section.
openmc.mgxs.CaptureXS A capture multi-group cross section.
openmc.mgxs.Chi The fission spectrum.
openmc.mgxs.Current A current multi-group cross section.
openmc.mgxs.DiffusionCoefficient A diffusion coefficient multi-group cross section.
openmc.mgxs.FissionXS A fission multi-group cross section.
openmc.mgxs.InverseVelocity An inverse velocity multi-group cross section.
openmc.mgxs.KappaFissionXS A recoverable fission energy production rate multi-group cross section.
openmc.mgxs.MultiplicityMatrixXS The scattering multiplicity matrix.
openmc.mgxs.NuFissionMatrixXS A fission production matrix multi-group cross section.
openmc.mgxs.ScatterXS A scattering multi-group cross section.
openmc.mgxs.ScatterMatrixXS A scattering matrix multi-group cross section with the cosine of the change-in-angle represented as one or more Legendre moments or a histogram.
openmc.mgxs.ScatterProbabilityMatrix The group-to-group scattering probability matrix.
openmc.mgxs.TotalXS A total multi-group cross section.
openmc.mgxs.TransportXS A transport-corrected total multi-group cross section.
openmc.mgxs.ArbitraryXS A multi-group cross section for an arbitrary reaction type.
openmc.mgxs.ArbitraryMatrixXS A multi-group matrix cross section for an arbitrary reaction type.
openmc.mgxs.MeshSurfaceMGXS An abstract multi-group cross section for some energy group structure on the surfaces of a mesh domain.

Multi-delayed-group Cross Sections

openmc.mgxs.MDGXS An abstract multi-delayed-group cross section for some energy and delayed group structures within some spatial domain.
openmc.mgxs.MatrixMDGXS An abstract multi-delayed-group cross section for some energy group and delayed group structure within some spatial domain.
openmc.mgxs.ChiDelayed The delayed fission spectrum.
openmc.mgxs.DelayedNuFissionXS A fission delayed neutron production multi-group cross section.
openmc.mgxs.DelayedNuFissionMatrixXS A fission delayed neutron production matrix multi-group cross section.
openmc.mgxs.Beta The delayed neutron fraction.
openmc.mgxs.DecayRate The decay rate for delayed neutron precursors.

Multi-group Cross Section Libraries

openmc.mgxs.Library A multi-energy-group and multi-delayed-group cross section library for some energy group structure.

openmc.stats – Statistics

Univariate Probability Distributions

openmc.stats.Univariate Probability distribution of a single random variable.
openmc.stats.Discrete Distribution characterized by a probability mass function.
openmc.stats.Uniform Distribution with constant probability over a finite interval [a,b]
openmc.stats.Maxwell Maxwellian distribution in energy.
openmc.stats.Watt Watt fission energy spectrum.
openmc.stats.Tabular Piecewise continuous probability distribution.
openmc.stats.Legendre Probability density given by a Legendre polynomial expansion \(\sum\limits_{\ell=0}^N \frac{2\ell + 1}{2} a_\ell P_\ell(\mu)\).
openmc.stats.Mixture Probability distribution characterized by a mixture of random variables.
openmc.stats.Normal Normally distributed sampling.
openmc.stats.Muir Muir energy spectrum.

Angular Distributions

openmc.stats.UnitSphere Distribution of points on the unit sphere.
openmc.stats.PolarAzimuthal Angular distribution represented by polar and azimuthal angles
openmc.stats.Isotropic Isotropic angular distribution.
openmc.stats.Monodirectional Monodirectional angular distribution.

Spatial Distributions

openmc.stats.Spatial Distribution of locations in three-dimensional Euclidean space.
openmc.stats.CartesianIndependent Spatial distribution with independent x, y, and z distributions.
openmc.stats.CylindricalIndependent Spatial distribution represented in cylindrical coordinates.
openmc.stats.SphericalIndependent Spatial distribution represented in spherical coordinates.
openmc.stats.Box Uniform distribution of coordinates in a rectangular cuboid.
openmc.stats.Point Delta function in three dimensions.

openmc.data – Nuclear Data Interface

Core Classes

The following classes are used for incident neutron data, decay data, fission and product yields.

IncidentNeutron Continuous-energy neutron interaction data.
Reaction A nuclear reaction
Product Secondary particle emitted in a nuclear reaction
FissionEnergyRelease Energy relased by fission reactions.
DataLibrary Collection of cross section data libraries.
Decay Radioactive decay data.
FissionProductYields Independent and cumulative fission product yields.
WindowedMultipole Resonant cross sections represented in the windowed multipole format.
ProbabilityTables Unresolved resonance region probability tables.

The following classes are used for storing atomic data (incident photon cross sections, atomic relaxation):

IncidentPhoton Photon interaction data.
PhotonReaction Photon-induced reaction
AtomicRelaxation Atomic relaxation data.

The following classes are used for storing thermal neutron scattering data:

ThermalScattering A ThermalScattering object contains thermal scattering data as represented by an S(alpha, beta) table.
ThermalScatteringReaction Thermal scattering reaction
CoherentElastic Coherent elastic scattering data from a crystalline material
IncoherentElastic Incoherent elastic scattering cross section

Core Functions

atomic_mass Return atomic mass of isotope in atomic mass units.
atomic_weight Return atomic weight of an element in atomic mass units.
dose_coefficients Return effective dose conversion coefficients from ICRP-116
gnd_name Return nuclide name using GND convention
isotopes Return naturally occurring isotopes and their abundances
linearize Return a tabulated representation of a one-variable function
thin Check for (x,y) points that can be removed.
water_density Return the density of liquid water at a given temperature and pressure.
zam Return tuple of (atomic number, mass number, metastable state)

One-dimensional Functions

Function1D A function of one independent variable with HDF5 support.
Tabulated1D A one-dimensional tabulated function.
Polynomial A power series class.
Combination Combination of multiple functions with a user-defined operator
Sum Sum of multiple functions.
Regions1D Piecewise composition of multiple functions.
ResonancesWithBackground Cross section in resolved resonance region.

Angle-Energy Distributions

AngleEnergy Distribution in angle and energy of a secondary particle.
KalbachMann Kalbach-Mann distribution
CorrelatedAngleEnergy Correlated angle-energy distribution
UncorrelatedAngleEnergy Uncorrelated angle-energy distribution
NBodyPhaseSpace N-body phase space distribution
LaboratoryAngleEnergy Laboratory angle-energy distribution
AngleDistribution Angle distribution as a function of incoming energy
EnergyDistribution Abstract superclass for all energy distributions.
ArbitraryTabulated Arbitrary tabulated function given in ENDF MF=5, LF=1 represented as
GeneralEvaporation General evaporation spectrum given in ENDF MF=5, LF=5 represented as
MaxwellEnergy Simple Maxwellian fission spectrum represented as
Evaporation Evaporation spectrum represented as
WattEnergy Energy-dependent Watt spectrum represented as
MadlandNix Energy-dependent fission neutron spectrum (Madland and Nix) given in ENDF MF=5, LF=12 represented as
DiscretePhoton Discrete photon energy distribution
LevelInelastic Level inelastic scattering
ContinuousTabular Continuous tabular distribution
CoherentElasticAE Differential cross section for coherent elastic scattering
IncoherentElasticAE Differential cross section for incoherent elastic scattering
IncoherentElasticAEDiscrete Discrete angle representation of incoherent elastic scattering
IncoherentInelasticAEDiscrete Discrete angle representation of incoherent inelastic scattering

Resonance Data

Resonances Resolved and unresolved resonance data
ResonanceRange Resolved resonance range
SingleLevelBreitWigner Single-level Breit-Wigner resolved resonance formalism data.
MultiLevelBreitWigner Multi-level Breit-Wigner resolved resonance formalism data.
ReichMoore Reich-Moore resolved resonance formalism data.
RMatrixLimited R-matrix limited resolved resonance formalism data.
ResonanceCovariances Resolved resonance covariance data
ResonanceCovarianceRange Resonace covariance range.
SingleLevelBreitWignerCovariance Single-level Breit-Wigner resolved resonance formalism covariance data.
MultiLevelBreitWignerCovariance Multi-level Breit-Wigner resolved resonance formalism covariance data.
ReichMooreCovariance Reich-Moore resolved resonance formalism covariance data.
ParticlePair
SpinGroup Resonance spin group
Unresolved Unresolved resonance parameters as identified by LRU=2 in MF=2.

ACE Format

Classes
ace.Library A Library objects represents an ACE-formatted file which may contain multiple tables with data.
ace.Table ACE cross section table
ace.TableType Type of ACE data table.
Functions
ace.ascii_to_binary Convert an ACE file in ASCII format (type 1) to binary format (type 2).
ace.get_libraries_from_xsdir Determine paths to ACE files from an MCNP xsdir file.
ace.get_libraries_from_xsdata Determine paths to ACE files from a Serpent xsdata file.

ENDF Format

Classes
endf.Evaluation ENDF material evaluation with multiple files/sections
Functions
endf.float_endf Convert string of floating point number in ENDF to float.
endf.get_cont_record Return data from a CONT record in an ENDF-6 file.
endf.get_evaluations Return a list of all evaluations within an ENDF file.
endf.get_head_record Return data from a HEAD record in an ENDF-6 file.
endf.get_tab1_record Return data from a TAB1 record in an ENDF-6 file.
endf.get_tab2_record
endf.get_text_record Return data from a TEXT record in an ENDF-6 file.

NJOY Interface

njoy.run Run NJOY with given commands
njoy.make_pendf Generate pointwise ENDF file from an ENDF file
njoy.make_ace Generate incident neutron ACE file from an ENDF file
njoy.make_ace_thermal Generate thermal scattering ACE file from ENDF files

openmc.lib – Python bindings to the C/C++ API

This module provides bindings to C/C++ functions defined by OpenMC shared library. When the openmc.lib package is imported, the OpenMC shared library is automatically loaded. Calls to the OpenMC library can then be via functions or objects in openmc.lib, for example:

openmc.lib.init()
openmc.lib.run()
openmc.lib.finalize()

Functions

calculate_volumes Run stochastic volume calculation
finalize Finalize simulation and free memory
find_cell Find the cell at a given point
find_material Find the material at a given point
hard_reset Reset tallies, timers, and pseudo-random number generator state.
init Initialize OpenMC
iter_batches Iterator over batches.
keff Return the calculated k-eigenvalue and its standard deviation.
load_nuclide Load cross section data for a nuclide.
next_batch Run next batch.
num_realizations Number of realizations of global tallies.
plot_geometry Plot geometry
reset Reset tally results
run Run simulation
run_in_memory Provides context manager for calling OpenMC shared library functions.
simulation_init Initialize simulation
simulation_finalize Finalize simulation
source_bank Return source bank as NumPy array
statepoint_write Write a statepoint file.

Classes

Cell Cell stored internally.
EnergyFilter
MaterialFilter
Material Material stored internally.
MeshFilter
MeshSurfaceFilter
Nuclide Nuclide stored internally.
RegularMesh RegularMesh stored internally.
Tally Tally stored internally.

openmc.openmoc_compatible – OpenMOC Compatibility

Core Classes

openmc.openmoc_compatible.get_openmoc_material Return an OpenMOC material corresponding to an OpenMC material.
openmc.openmoc_compatible.get_openmc_material Return an OpenMC material corresponding to an OpenMOC material.
openmc.openmoc_compatible.get_openmoc_surface Return an OpenMOC surface corresponding to an OpenMC surface.
openmc.openmoc_compatible.get_openmc_surface Return an OpenMC surface corresponding to an OpenMOC surface.
openmc.openmoc_compatible.get_openmoc_cell Return an OpenMOC cell corresponding to an OpenMC cell.
openmc.openmoc_compatible.get_openmc_cell Return an OpenMC cell corresponding to an OpenMOC cell.
openmc.openmoc_compatible.get_openmoc_universe Return an OpenMOC universe corresponding to an OpenMC universe.
openmc.openmoc_compatible.get_openmc_universe Return an OpenMC universe corresponding to an OpenMOC universe.
openmc.openmoc_compatible.get_openmoc_lattice Return an OpenMOC lattice corresponding to an OpenMOC lattice.
openmc.openmoc_compatible.get_openmc_lattice Return an OpenMC lattice corresponding to an OpenMOC lattice.
openmc.openmoc_compatible.get_openmoc_geometry Return an OpenMC geometry corresponding to an OpenMOC geometry.
openmc.openmoc_compatible.get_openmc_geometry Return an OpenMC geometry corresponding to an OpenMOC geometry.

C/C++ API

The libopenmc shared library that is built when installing OpenMC exports a number of C interoperable functions and global variables that can be used for in-memory coupling. While it is possible to directly use the C/C++ API as documented here for coupling, most advanced users will find it easier to work with the Python bindings in the openmc.lib module.

Warning

The C/C++ API is still experimental and may undergo substantial changes in future releases.

Type Definitions

Bank

Attributes of a source particle.

double wgt

Weight of the particle

double xyz[3]

Position of the particle (units of cm)

double uvw[3]

Unit vector indicating direction of the particle

double E

Energy of the particle in eV

int delayed_group

If the particle is a delayed neutron, indicates which delayed precursor group it was born from. If not a delayed neutron, this member is zero.

Functions

int openmc_calculate_volumes()

Run a stochastic volume calculation

Returns:Return status (negative if an error occurred)
Return type:int
int openmc_cell_get_fill(int32_t index, int* type, int32_t** indices, int32_t* n)

Get the fill for a cell

Parameters:
  • index (int32_t) – Index in the cells array
  • type (int*) – Type of the fill
  • indices (int32_t**) – Array of material indices for cell
  • n (int32_t*) – Length of indices array
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_cell_get_id(int32_t index, int32_t* id)

Get the ID of a cell

Parameters:
  • index (int32_t) – Index in the cells array
  • id (int32_t*) – ID of the cell
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_cell_get_temperature(int32_t index, const int32_t* instance, double* T)

Get the temperature of a cell

Parameters:
  • index (int32_t) – Index in the cells array
  • instance (int32_t*) – Which instance of the cell. If a null pointer is passed, the temperature of the first instance is returned.
  • T (double*) – temperature of the cell
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_cell_set_fill(int32_t index, int type, int32_t n, const int32_t* indices)

Set the fill for a cell

Parameters:
  • index (int32_t) – Index in the cells array
  • type (int) – Type of the fill
  • n (int32_t) – Length of indices array
  • indices (const int32_t*) – Array of material indices for cell
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_cell_set_id(int32_t index, int32_t id)

Set the ID of a cell

Parameters:
  • index (int32_t) – Index in the cells array
  • id (int32_t) – ID of the cell
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_cell_set_temperature(index index, double T, const int32_t* instance, bool set_contained)

Set the temperature of a cell.

Parameters:
  • index (int32_t) – Index in the cells array
  • T (double) – Temperature in Kelvin
  • instance (const int32_t*) – Which instance of the cell. To set the temperature for all instances, pass a null pointer.
  • set_contained – If the cell is not filled by a material, whether to set the temperatures of all filled cells
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_energy_filter_get_bins(int32_t index, double** energies, int32_t* n)

Return the bounding energies for an energy filter

Parameters:
  • index (int32_t) – Index in the filters array
  • energies (double**) – Bounding energies of the bins for the energy filter
  • n (int32_t*) – Number of energies specified
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_energy_filter_set_bins(int32_t index, int32_t n, const double* energies)

Set the bounding energies for an energy filter

Parameters:
  • index (int32_t) – Index in the filters array
  • n (int32_t) – Number of energies specified
  • energies (const double*) – Bounding energies of the bins for the energy filter
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_extend_cells(int32_t n, int32_t* index_start, int32_t* index_end)

Extend the cells array by n elements

Parameters:
  • n (int32_t) – Number of cells to create
  • index_start (int32_t*) – Index of first new cell
  • index_end (int32_t*) – Index of last new cell
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_extend_filters(int32_t n, int32_t* index_start, int32_t* index_end)

Extend the filters array by n elements

Parameters:
  • n (int32_t) – Number of filters to create
  • index_start (int32_t*) – Index of first new filter
  • index_end (int32_t*) – Index of last new filter
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_extend_materials(int32_t n, int32_t* index_start, int32_t* index_end)

Extend the materials array by n elements

Parameters:
  • n (int32_t) – Number of materials to create
  • index_start (int32_t*) – Index of first new material
  • index_end (int32_t*) – Index of last new material
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_extend_sources(int32_t n, int32_t* index_start, int32_t* index_end)

Extend the external sources array by n elements

Parameters:
  • n (int32_t) – Number of sources to create
  • index_start (int32_t*) – Index of first new source
  • index_end (int32_t*) – Index of last new source
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_extend_tallies(int32_t n, int32_t* index_start, int32_t* index_end)

Extend the tallies array by n elements

Parameters:
  • n (int32_t) – Number of tallies to create
  • index_start (int32_t*) – Index of first new tally
  • index_end (int32_t*) – Index of last new tally
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_filter_get_id(int32_t index, int32_t* id)

Get the ID of a filter

Parameters:
  • index (int32_t) – Index in the filters array
  • id (int32_t*) – ID of the filter
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_filter_set_id(int32_t index, int32_t id)

Set the ID of a filter

Parameters:
  • index (int32_t) – Index in the filters array
  • id (int32_t) – ID of the filter
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_finalize()

Finalize a simulation

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_find(double* xyz, int rtype, int32_t* id, int32_t* instance)

Determine the ID of the cell/material containing a given point

Parameters:
  • xyz (double[3]) – Cartesian coordinates
  • rtype (int) – Which ID to return (1=cell, 2=material)
  • id (int32_t*) – ID of the cell/material found. If a material is requested and the point is in a void, the ID is 0. If an error occurs, the ID is -1.
  • instance (int32_t*) – If a cell is repeated in the geometry, the instance of the cell that was found and zero otherwise.
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_get_cell_index(int32_t id, int32_t* index)

Get the index in the cells array for a cell with a given ID

Parameters:
  • id (int32_t) – ID of the cell
  • index (int32_t*) – Index in the cells array
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_get_filter_index(int32_t id, int32_t* index)

Get the index in the filters array for a filter with a given ID

Parameters:
  • id (int32_t) – ID of the filter
  • index (int32_t*) – Index in the filters array
Returns:

Return status (negative if an error occurs)

Return type:

int

void openmc_get_filter_next_id(int32_t* id)

Get an integer ID that has not been used by any filters.

Parameters:
  • id (int32_t*) – Unused integer ID
int openmc_get_keff(double k_combined[2])
Parameters:
  • k_combined (double[2]) – Combined estimate of k-effective
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_get_material_index(int32_t id, int32_t* index)

Get the index in the materials array for a material with a given ID

Parameters:
  • id (int32_t) – ID of the material
  • index (int32_t*) – Index in the materials array
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_get_n_batches(int* n_batches, bool get_max_batches)

Get number of batches to simulate

Parameters:
  • n_batches (int*) – Number of batches to simulate
  • get_max_batches (bool) – Whether to return n_batches or n_max_batches (only relevant when triggers are used)
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_get_nuclide_index(const char name[], int* index)

Get the index in the nuclides array for a nuclide with a given name

Parameters:
  • name (const char[]) – Name of the nuclide
  • index (int*) – Index in the nuclides array
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_get_tally_index(int32_t id, int32_t* index)

Get the index in the tallies array for a tally with a given ID

Parameters:
  • id (int32_t) – ID of the tally
  • index (int32_t*) – Index in the tallies array
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_hard_reset()

Reset tallies, timers, and pseudo-random number generator state

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_init(int argc, char** argv, const void* intracomm)

Initialize OpenMC

Parameters:
  • argc (int) – Number of command-line arguments (including command)
  • argv (char**) – Command-line arguments
  • intracomm (const void*) – MPI intracommunicator. If MPI is not being used, a null pointer should be passed.
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_load_nuclide(const char* name, const double* temps, int n)

Load data for a nuclide from the HDF5 data library.

Parameters:
  • name (const char*) – Name of the nuclide.
  • temps (const double*) – Temperatures in [K] to load data at
  • n (int) – Number of temperatures
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_add_nuclide(int32_t index, const char name[], double density)

Add a nuclide to an existing material. If the nuclide already exists, the density is overwritten.

Parameters:
  • index (int32_t) – Index in the materials array
  • name (const char[]) – Name of the nuclide
  • density (double) – Density in atom/b-cm
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_get_densities(int32_t index, int** nuclides, double** densities, int* n)

Get density for each nuclide in a material.

Parameters:
  • index (int32_t) – Index in the materials array
  • nuclides (int**) – Pointer to array of nuclide indices
  • densities (double**) – Pointer to the array of densities
  • n (int*) – Length of the array
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_get_density(int32_t index, double* density)

Get density of a material.

Parameters:
  • index (int32_t) – Index in the materials array
  • denity (double*) – Pointer to a density
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_get_id(int32_t index, int32_t* id)

Get the ID of a material

Parameters:
  • index (int32_t) – Index in the materials array
  • id (int32_t*) – ID of the material
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_material_set_density(int32_t index, double density, const char* units)

Set the density of a material.

Parameters:
  • index (int32_t) – Index in the materials array
  • density (double) – Density of the material
  • units (const char*) – Units for density
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_set_densities(int32_t index, int n, const char** name, const double* density)
Parameters:
  • index (int32_t) – Index in the materials array
  • n (int) – Length of name/density
  • name (const char**) – Array of nuclide names
  • density (const double*) – Array of densities
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_material_set_id(int32_t index, int32_t id)

Set the ID of a material

Parameters:
  • index (int32_t) – Index in the materials array
  • id (int32_t) – ID of the material
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_material_filter_get_bins(int32_t index, int32_t** bins, int32_t* n)

Get the bins for a material filter

Parameters:
  • index (int32_t) – Index in the filters array
  • bins (int32_t**) – Index in the materials array for each bin
  • n (int32_t*) – Number of bins
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_material_filter_set_bins(int32_t index, int32_t n, const int32_t* bins)

Set the bins for a material filter

Parameters:
  • index (int32_t) – Index in the filters array
  • n (int32_t) – Number of bins
  • bins (const int32_t*) – Index in the materials array for each bin
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_mesh_filter_set_mesh(int32_t index, int32_t index_mesh)

Set the mesh for a mesh filter

Parameters:
  • index (int32_t) – Index in the filters array
  • index_mesh (int32_t) – Index in the meshes array
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_next_batch()

Simulate next batch of particles. Must be called after openmc_simulation_init().

Returns:Integer indicating whether simulation has finished (negative) or not finished (zero).
Return type:int
int openmc_nuclide_name(int index, char** name)

Get name of a nuclide

Parameters:
  • index (int) – Index in the nuclides array
  • name (char**) – Name of the nuclide
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_plot_geometry()

Run plotting mode.

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_reset()

Resets all tally scores

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_run()

Run a simulation

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_set_n_batches(int32_t n_batches, bool set_max_batches, bool add_statepoint_batch)

Set number of batches and number of max batches

Parameters:
  • n_batches (int32_t) – Number of batches to simulate
  • set_max_batches (bool) – Whether to set settings::n_max_batches or settings::n_batches (only relevant when triggers are used)
  • add_statepoint_batch (bool) – Whether to add n_batches to settings::statepoint_batch
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_simulation_finalize()

Finalize a simulation.

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_simulation_init()

Initialize a simulation. Must be called after openmc_init().

Returns:Return status (negative if an error occurs)
Return type:int
int openmc_source_bank(struct Bank** ptr, int64_t* n)

Return a pointer to the source bank array.

Parameters:
  • ptr (struct Bank**) – Pointer to the source bank array
  • n (int64_t*) – Length of the source bank array
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_source_set_strength(int32_t index, double strength)

Set the strength of an external source

Parameters:
  • index (int32_t) – Index in the external source array
  • strength (double) – Source strength
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_statepoint_write(const char filename[], const bool* write_source)

Write a statepoint file

Parameters:
  • filename (const char[]) – Name of file to create. If a null pointer is passed, a filename is assigned automatically.
  • write_source (const bool*) – Whether to include the source bank
Returns:

Return status (negative if an error occurs)

Return type:

int

int openmc_tally_get_id(int32_t index, int32_t* id)

Get the ID of a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • id (int32_t*) – ID of the tally
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_get_filters(int32_t index, int32_t** indices, int* n)

Get filters specified in a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • indices (int32_t**) – Array of filter indices
  • n (int*) – Number of filters
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
Parameters:
  • index (int32_t) – Index in the tallies array
  • n (int32_t*) – Number of realizations
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)

Get nuclides specified in a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • nuclides (int**) – Array of nuclide indices
  • n (int*) – Number of nuclides
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_get_scores(int32_t index, int** scores, int* n)

Get scores specified for a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • scores (int**) – Array of scores
  • n (int*) – Number of scores
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_results(int32_t index, double** ptr, int shape_[3])

Get a pointer to tally results array.

Parameters:
  • index (int32_t) – Index in the tallies array
  • ptr (double**) – Pointer to the results array
  • shape (int[3]) – Shape of the results array
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_set_filters(int32_t index, int n, const int32_t* indices)

Set filters for a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • n (int) – Number of filters
  • indices (const int32_t*) – Array of filter indices
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_set_id(int32_t index, int32_t id)

Set the ID of a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • id (int32_t) – ID of the tally
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_set_nuclides(int32_t index, int n, const char** nuclides)

Set the nuclides for a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • n (int) – Number of nuclides
  • nuclides (const char**) – Array of nuclide names
Returns:

Return status (negative if an error occurred)

Return type:

int

int openmc_tally_set_scores(int32_t index, int n, const int* scores)

Set scores for a tally

Parameters:
  • index (int32_t) – Index in the tallies array
  • n (int) – Number of scores
  • scores (const int*) – Array of scores
Returns:

Return status (negative if an error occurred)

Return type:

int

File Format Specifications

Input Files

Geometry Specification – geometry.xml

<surface> Element

Each <surface> element can have the following attributes or sub-elements:

id:

A unique integer that can be used to identify the surface.

Default: None

name:

An optional string name to identify the surface in summary output files. This string is limited to 52 characters for formatting purposes.

Default: “”

type:

The type of the surfaces. This can be “x-plane”, “y-plane”, “z-plane”, “plane”, “x-cylinder”, “y-cylinder”, “z-cylinder”, “sphere”, “x-cone”, “y-cone”, “z-cone”, or “quadric”.

Default: None

coeffs:

The corresponding coefficients for the given type of surface. See below for a list a what coefficients to specify for a given surface

Default: None

boundary:

The boundary condition for the surface. This can be “transmission”, “vacuum”, “reflective”, or “periodic”. Periodic boundary conditions can only be applied to x-, y-, and z-planes. Only axis-aligned periodicity is supported, i.e., x-planes can only be paired with x-planes. Specify which planes are periodic and the code will automatically identify which planes are paired together.

Default: “transmission”

periodic_surface_id:
 

If a periodic boundary condition is applied, this attribute identifies the id of the corresponding periodic sufrace.

The following quadratic surfaces can be modeled:

x-plane:A plane perpendicular to the x axis, i.e. a surface of the form \(x - x_0 = 0\). The coefficients specified are “\(x_0\)”.
y-plane:A plane perpendicular to the y axis, i.e. a surface of the form \(y - y_0 = 0\). The coefficients specified are “\(y_0\)”.
z-plane:A plane perpendicular to the z axis, i.e. a surface of the form \(z - z_0 = 0\). The coefficients specified are “\(z_0\)”.
plane:An arbitrary plane of the form \(Ax + By + Cz = D\). The coefficients specified are “\(A \: B \: C \: D\)”.
x-cylinder:An infinite cylinder whose length is parallel to the x-axis. This is a quadratic surface of the form \((y - y_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(y_0 \: z_0 \: R\)”.
y-cylinder:An infinite cylinder whose length is parallel to the y-axis. This is a quadratic surface of the form \((x - x_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(x_0 \: z_0 \: R\)”.
z-cylinder:An infinite cylinder whose length is parallel to the z-axis. This is a quadratic surface of the form \((x - x_0)^2 + (y - y_0)^2 = R^2\). The coefficients specified are “\(x_0 \: y_0 \: R\)”.
sphere:A sphere of the form \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R\)”.
x-cone:A cone parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = R^2 (x - x_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”.
y-cone:A cone parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = R^2 (y - y_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”.
z-cone:A cone parallel to the x-axis of the form \((x - x_0)^2 + (y - y_0)^2 = R^2 (z - z_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”.
quadric:A general quadric surface of the form \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\) The coefficients specified are “\(A \: B \: C \: D \: E \: F \: G \: H \: J \: K\)”.
<cell> Element

Each <cell> element can have the following attributes or sub-elements:

id:

A unique integer that can be used to identify the cell.

Default: None

name:

An optional string name to identify the cell in summary output files. This string is limmited to 52 characters for formatting purposes.

Default: “”

universe:

The id of the universe that this cell is contained in.

Default: 0

fill:

The id of the universe that fills this cell.

Note

If a fill is specified, no material should be given.

Default: None

material:

The id of the material that this cell contains. If the cell should contain no material, this can also be set to “void”. A list of materials can be specified for the “distributed material” feature. This will give each unique instance of the cell its own material.

Note

If a material is specified, no fill should be given.

Default: None

region:

A Boolean expression of half-spaces that defines the spatial region which the cell occupies. Each half-space is identified by the unique ID of the surface prefixed by - or + to indicate that it is the negative or positive half-space, respectively. The + sign for a positive half-space can be omitted. Valid Boolean operators are parentheses, union |, complement ~, and intersection. Intersection is implicit and indicated by the presence of whitespace. The order of operator precedence is parentheses, complement, intersection, and then union.

As an example, the following code gives a cell that is the union of the negative half-space of surface 3 and the complement of the intersection of the positive half-space of surface 5 and the negative half-space of surface 2:

<cell id="1" material="1" region="-3 | ~(5 -2)" />

Note

The region attribute/element can be omitted to make a cell fill its entire universe.

Default: A region filling all space.

temperature:

The temperature of the cell in Kelvin. The temperature may be used in windowed multipole Doppler broadening or interpolation of pointwise cross sections versus temperature. A list of temperatures can be specified for the “distributed temperature” feature. This will give each unique instance of the cell its own temperature.

Default: If a material default temperature is supplied, it is used. In the absence of a material default temperature, the global default temperature is used.

rotation:

If the cell is filled with a universe, this element specifies the angles in degrees about the x, y, and z axes that the filled universe should be rotated. Should be given as three real numbers. For example, if you wanted to rotate the filled universe by 90 degrees about the z-axis, the cell element would look something like:

<cell fill="..." rotation="0 0 90" />

The rotation applied is an intrinsic rotation whose Tait-Bryan angles are given as those specified about the x, y, and z axes respectively. That is to say, if the angles are \((\phi, \theta, \psi)\), then the rotation matrix applied is \(R_z(\psi) R_y(\theta) R_x(\phi)\) or

\[\left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi + \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi \cos\theta \end{array} \right ]\]

Default: None

translation:

If the cell is filled with a universe, this element specifies a vector that is used to translate (shift) the universe. Should be given as three real numbers.

Note

Any translation operation is applied after a rotation, if also specified.

Default: None

<lattice> Element

The <lattice> can be used to represent repeating structures (e.g. fuel pins in an assembly) or other geometry which fits onto a rectilinear grid. Each cell within the lattice is filled with a specified universe. A <lattice> accepts the following attributes or sub-elements:

id:

A unique integer that can be used to identify the lattice.

name:

An optional string name to identify the lattice in summary output files. This string is limited to 52 characters for formatting purposes.

Default: “”

dimension:

Two or three integers representing the number of lattice cells in the x- and y- (and z-) directions, respectively.

Default: None

lower_left:

The coordinates of the lower-left corner of the lattice. If the lattice is two-dimensional, only the x- and y-coordinates are specified.

Default: None

pitch:

If the lattice is 3D, then three real numbers that express the distance between the centers of lattice cells in the x-, y-, and z- directions. If the lattice is 2D, then omit the third value.

Default: None

outer:

The unique integer identifier of a universe that will be used to fill all space outside of the lattice. The universe will be tiled repeatedly as if it were placed in a lattice of infinite size. This element is optional.

Default: An error will be raised if a particle leaves a lattice with no outer universe.

universes:

A list of the universe numbers that fill each cell of the lattice.

Default: None

Here is an example of a properly defined 2d rectangular lattice:

<lattice id="10" dimension="3 3" outer="1">
    <lower_left> -1.5 -1.5 </lower_left>
    <pitch> 1.0 1.0 </pitch>
    <universes>
      2 2 2
      2 1 2
      2 2 2
    </universes>
</lattice>
<hex_lattice> Element

The <hex_lattice> can be used to represent repeating structures (e.g. fuel pins in an assembly) or other geometry which naturally fits onto a hexagonal grid or hexagonal prism grid. Each cell within the lattice is filled with a specified universe. This lattice uses the “flat-topped hexagon” scheme where two of the six edges are perpendicular to the y-axis. A <hex_lattice> accepts the following attributes or sub-elements:

id:

A unique integer that can be used to identify the lattice.

name:

An optional string name to identify the hex_lattice in summary output files. This string is limited to 52 characters for formatting purposes.

Default: “”

n_rings:

An integer representing the number of radial ring positions in the xy-plane. Note that this number includes the degenerate center ring which only has one element.

Default: None

n_axial:

An integer representing the number of positions along the z-axis. This element is optional.

Default: None

orientation:

The orientation of the hexagonal lattice. The string “x” indicates that two sides of the lattice are parallel to the x-axis, whereas the string “y” indicates that two sides are parallel to the y-axis.

Default: “y”

center:

The coordinates of the center of the lattice. If the lattice does not have axial sections then only the x- and y-coordinates are specified.

Default: None

pitch:

If the lattice is 3D, then two real numbers that express the distance between the centers of lattice cells in the xy-plane and along the z-axis, respectively. If the lattice is 2D, then omit the second value.

Default: None

outer:

The unique integer identifier of a universe that will be used to fill all space outside of the lattice. The universe will be tiled repeatedly as if it were placed in a lattice of infinite size. This element is optional.

Default: An error will be raised if a particle leaves a lattice with no outer universe.

universes:

A list of the universe numbers that fill each cell of the lattice.

Default: None

Here is an example of a properly defined 2d hexagonal lattice:

<hex_lattice id="10" n_rings="3" outer="1">
    <center> 0.0 0.0 </center>
    <pitch> 1.0 </pitch>
    <universes>
              202
           202   202
        202   202   202
           202   202
        202   101   202
           202   202
        202   202   202
           202   202
              202
    </universes>
</hex_lattice>

Materials Specification – materials.xml

<cross_sections> Element

The <cross_sections> element has no attributes and simply indicates the path to an XML cross section listing file (usually named cross_sections.xml). If this element is absent from the settings.xml file, the OPENMC_CROSS_SECTIONS environment variable will be used to find the path to the XML cross section listing when in continuous-energy mode, and the OPENMC_MG_CROSS_SECTIONS environment variable will be used in multi-group mode.

<material> Element

Each material element can have the following attributes or sub-elements:

id:

A unique integer that can be used to identify the material.

name:

An optional string name to identify the material in summary output files. This string is limited to 52 characters for formatting purposes.

Default: “”

depletable:

Boolean value indicating whether the material is depletable.

volume:

Volume of the material in cm^3.

temperature:

Temperature of the material in Kelvin.

Default: If a material default temperature is not given and a cell temperature is not specified, the global default temperature is used.

density:

An element with attributes/sub-elements called value and units. The value attribute is the numeric value of the density while the units can be “g/cm3”, “kg/m3”, “atom/b-cm”, “atom/cm3”, or “sum”. The “sum” unit indicates that values appearing in ao or wo attributes for <nuclide> and <element> sub-elements are to be interpreted as absolute nuclide/element densities in atom/b-cm or g/cm3, and the total density of the material is taken as the sum of all nuclides/elements. The “macro” unit is used with a macroscopic quantity to indicate that the density is already included in the library and thus not needed here. However, if a value is provided for the value, then this is treated as a number density multiplier on the macroscopic cross sections in the multi-group data. This can be used, for example, when perturbing the density slightly.

Default: None

Note

A macroscopic quantity can not be used in conjunction with a nuclide, element, or sab quantity.

nuclide:

An element with attributes/sub-elements called name, and ao or wo. The name attribute is the name of the cross-section for a desired nuclide. Finally, the ao and wo attributes specify the atom or weight percent of that nuclide within the material, respectively. One example would be as follows:

<nuclide name="H1" ao="2.0" />
<nuclide name="O16" ao="1.0" />

Note

If one nuclide is specified in atom percent, all others must also be given in atom percent. The same applies for weight percentages.

Default: None

sab:

Associates an S(a,b) table with the material. This element has an attribute/sub-element called name. The name attribute is the name of the S(a,b) table that should be associated with the material. There is also an optional fraction element which indicates what fraction of the relevant nuclides will be affected by the S(a,b) table (e.g. which fraction of a material is crystalline versus amorphous). fraction defaults to unity.

Default: None

Note

This element is not used in the multi-group <energy_mode> Element.

isotropic:

The isotropic element indicates a list of nuclides for which elastic scattering should be treated as though it were isotropic in the laboratory system. This element may be most useful when using OpenMC to compute multi-group cross-sections for deterministic transport codes and to quantify the effects of anisotropic scattering.

Default: No nuclides are treated as have isotropic elastic scattering.

Note

This element is not used in the multi-group <energy_mode> Element.

macroscopic:

The macroscopic element is similar to the nuclide element, but, recognizes that some multi-group libraries may be providing material specific macroscopic cross sections instead of always providing nuclide specific data like in the continuous-energy case. To that end, the macroscopic element has one attribute/sub-element called name. The name attribute is the name of the cross-section for a desired nuclide. One example would be as follows:

<macroscopic name="UO2" />

Note

This element is only used in the multi-group <energy_mode> Element.

Default: None

Settings Specification – settings.xml

All simulation parameters and miscellaneous options are specified in the settings.xml file.

<batches> Element

The <batches> element indicates the total number of batches to execute, where each batch corresponds to a tally realization. In a fixed source calculation, each batch consists of a number of source particles. In an eigenvalue calculation, each batch consists of one or many fission source iterations (generations), where each generation itself consists of a number of source neutrons.

Default: None
<confidence_intervals> Element

The <confidence_intervals> element has no attributes and has an accepted value of “true” or “false”. If set to “true”, uncertainties on tally results will be reported as the half-width of the 95% two-sided confidence interval. If set to “false”, uncertainties on tally results will be reported as the sample standard deviation.

Default: false
<create_fission_neutrons> Element

The <create_fission_neutrons> element indicates whether fission neutrons should be created or not. If this element is set to “true”, fission neutrons will be created; otherwise the fission is treated as capture and no fission neutron will be created. Note that this option is only applied to fixed source calculation. For eigenvalue calculation, fission will always be treated as real fission.

Default: true
<cutoff> Element

The <cutoff> element indicates two kinds of cutoffs. The first is the weight cutoff used below which particles undergo Russian roulette. Surviving particles are assigned a user-determined weight. Note that weight cutoffs and Russian rouletting are not turned on by default. The second is the energy cutoff which is used to kill particles under certain energy. The energy cutoff should not be used unless you know particles under the energy are of no importance to results you care. This element has the following attributes/sub-elements:

weight:

The weight below which particles undergo Russian roulette.

Default: 0.25

weight_avg:

The weight that is assigned to particles that are not killed after Russian roulette.

Default: 1.0

energy_neutron:

The energy under which neutrons will be killed.

Default: 0.0

energy_photon:

The energy under which photons will be killed.

Default: 1000.0

energy_electron:
 

The energy under which electrons will be killed.

Default: 0.0

energy_positron:
 

The energy under which positrons will be killed.

Default: 0.0

<dagmc> Element

When the DAGMC mode is enabled, the OpenMC geometry will be read from the file dagmc.h5m. If a geometry.xml file is present with dagmc set to true, it will be ignored.

<delayed_photon_scaling>

Determines whether to scale the fission photon yield to account for delayed photon energy. The photon yields are scaled as (EGP + EGD)/EGP where EGP and EGD are the prompt and delayed photon components of energy release, respectively, from MF=1, MT=458 on an ENDF evaluation.

Default: true
<electron_treatment> Element

When photon transport is enabled, the <electron_treatment> element tells OpenMC whether to deposit all energy from electrons locally (led) or create secondary bremsstrahlung photons (ttb).

Default: ttb
<energy_mode> Element

The <energy_mode> element tells OpenMC if the run-mode should be continuous-energy or multi-group. Options for entry are: continuous-energy or multi-group.

Default: continuous-energy
<entropy_mesh> Element

The <entropy_mesh> element indicates the ID of a mesh that is to be used for calculating Shannon entropy. The mesh should cover all possible fissionable materials in the problem and is specified using a <mesh> Element.

<event_based>

Determines whether to use event-based parallelism instead of the default history-based parallelism.

Default: false
<generations_per_batch> Element

The <generations_per_batch> element indicates the number of total fission source iterations per batch for an eigenvalue calculation. This element is ignored for all run modes other than “eigenvalue”.

Default: 1
<inactive> Element

The <inactive> element indicates the number of inactive batches used in a k-eigenvalue calculation. In general, the starting fission source iterations in an eigenvalue calculation can not be used to contribute to tallies since the fission source distribution and eigenvalue are generally not converged immediately. This element is ignored for all run modes other than “eigenvalue”.

Default: 0
<keff_trigger> Element

The <keff_trigger> element (ignored for all run modes other than “eigenvalue”.) specifies a precision trigger on the combined \(k_{eff}\). The trigger is a convergence criterion on the uncertainty of the estimated eigenvalue. It has the following attributes/sub-elements:

type:

The type of precision trigger. Accepted options are “variance”, “std_dev”, and “rel_err”.

variance:Variance of the batch mean \(\sigma^2\)
std_dev:Standard deviation of the batch mean \(\sigma\)
rel_err:Relative error of the batch mean \(\frac{\sigma}{\mu}\)

Default: None

threshold:

The precision trigger’s convergence criterion for the combined \(k_{eff}\).

Default: None

Note

See section on the <trigger> Element for more information.

<log_grid_bins> Element

The <log_grid_bins> element indicates the number of bins to use for the logarithmic-mapped energy grid. Using more bins will result in energy grid searches over a smaller range at the expense of more memory. The default is based on the recommended value in LA-UR-14-24530.

Default: 8000

Note

This element is not used in the multi-group <energy_mode> Element.

<material_cell_offsets>

By default, OpenMC will count the number of instances of each cell filled with a material and generate “offset tables” that are used for cell instance tallies. The <material_cell_offsets> element allows a user to override this default setting and turn off the generation of offset tables, if desired, by setting it to false.

Default: true
<max_particles_in_flight> Element

This element indicates the number of neutrons to run in flight concurrently when using event-based parallelism. A higher value uses more memory, but may be more efficient computationally.

Default: 100000
<max_order> Element

The <max_order> element allows the user to set a maximum scattering order to apply to every nuclide/material in the problem. That is, if the data library has \(P_3\) data available, but <max_order> was set to 1, then, OpenMC will only use up to the \(P_1\) data.

Default: Use the maximum order in the data library

Note

This element is not used in the continuous-energy <energy_mode> Element.

<mesh> Element

The <mesh> element describes a mesh that is used either for calculating Shannon entropy, applying the uniform fission site method, or in tallies. For Shannon entropy meshes, the mesh should cover all possible fissionable materials in the problem. It has the following attributes/sub-elements:

id:

A unique integer that is used to identify the mesh.

dimension:

The number of mesh cells in the x, y, and z directions, respectively.

Default: If this tag is not present, the number of mesh cells is automatically determined by the code.

lower_left:

The Cartesian coordinates of the lower-left corner of the mesh.

Default: None

upper_right:

The Cartesian coordinates of the upper-right corner of the mesh.

Default: None

<no_reduce> Element

The <no_reduce> element has no attributes and has an accepted value of “true” or “false”. If set to “true”, all user-defined tallies and global tallies will not be reduced across processors in a parallel calculation. This means that the accumulate score in one batch on a single processor is considered as an independent realization for the tally random variable. For a problem with large tally data, this option can significantly improve the parallel efficiency.

Default: false
<output> Element

The <output> element determines what output files should be written to disk during the run. The sub-elements are described below, where “true” will write out the file and “false” will not.

summary:

Writes out an HDF5 summary file describing all of the user input files that were read in.

Default: true

tallies:

Write out an ASCII file of tally results.

Default: true

Note

The tally results will always be written to a binary/HDF5 state point file.

path:

Absolute or relative path where all output files should be written to. The specified path must exist or else OpenMC will abort.

Default: Current working directory

<particles> Element

This element indicates the number of neutrons to simulate per fission source iteration when a k-eigenvalue calculation is performed or the number of particles per batch for a fixed source simulation.

Default: None
<photon_transport> Element

The <photon_transport> element determines whether photon transport is enabled. This element has no attributes or sub-elements and can be set to either “false” or “true”.

Default: false
<ptables> Element

The <ptables> element determines whether probability tables should be used in the unresolved resonance range if available. This element has no attributes or sub-elements and can be set to either “false” or “true”.

Default: true

Note

This element is not used in the multi-group <energy_mode> Element.

<resonance_scattering> Element

The resonance_scattering element indicates to OpenMC that a method be used to properly account for resonance elastic scattering (typically for nuclides with Z > 40). This element can contain one or more of the following attributes or sub-elements:

enable:

Indicates whether a resonance elastic scattering method should be turned on. Accepts values of “true” or “false”.

Default: If the <resonance_scattering> element is present, “true”.

method:

Which resonance elastic scattering method is to be applied: “rvs” (relative velocity sampling) or “dbrc” (Doppler broadening rejection correction). Descriptions of each of these methods are documented here.

Default: “rvs”

energy_min:

The energy in eV above which the resonance elastic scattering method should be applied.

Default: 0.01 eV

energy_max:

The energy in eV below which the resonance elastic scattering method should be applied.

Default: 1000.0 eV

nuclides:

A list of nuclides to which the resonance elastic scattering method should be applied.

Default: If <resonance_scattering> is present but the <nuclides> sub-element is not given, the method is applied to all nuclides with 0 K elastic scattering data present.

Note

If the resonance_scattering element is not given, the free gas, constant cross section scattering model, which has historically been used by Monte Carlo codes to sample target velocities, is used to treat the target motion of all nuclides. If resonance_scattering is present, the constant cross section method is applied below energy_min and the target-at-rest (asymptotic) kernel is used above energy_max.

Note

This element is not used in the multi-group <energy_mode> Element.

<run_mode> Element

The <run_mode> element indicates which run mode should be used when OpenMC is executed. This element has no attributes or sub-elements and can be set to “eigenvalue”, “fixed source”, “plot”, “volume”, or “particle restart”.

Default: None
<seed> Element

The seed element is used to set the seed used for the linear congruential pseudo-random number generator.

Default: 1
<source> Element

The source element gives information on an external source distribution to be used either as the source for a fixed source calculation or the initial source guess for criticality calculations. Multiple <source> elements may be specified to define different source distributions. Each one takes the following attributes/sub-elements:

strength:

The strength of the source. If multiple sources are present, the source strength indicates the relative probability of choosing one source over the other.

Default: 1.0

particle:

The source particle type, either neutron or photon.

Default: neutron

file:

If this attribute is given, it indicates that the source is to be read from a binary source file whose path is given by the value of this element. Note, the number of source sites needs to be the same as the number of particles simulated in a fission source generation.

Default: None

library:

If this attribute is given, it indicates that the source is to be instantiated from an externally compiled source function. This source can be as complex as is required to define the source for your problem. The library has a few basic requirements:

  • It must contain a class that inherits from openmc::Source;
  • The class must implement a function called sample();
  • There must be an openmc_create_source() function that creates the source as a unique pointer. This function can be used to pass parameters through to the source from the XML, if needed.

More documentation on how to build sources can be found in Custom Sources.

Default: None

parameters:

If this attribute is given, it provides the parameters to pass through to the class generated using the library parameter . More documentation on how to build parametrized sources can be found in Custom Parameterized Sources.

Default: None

space:

An element specifying the spatial distribution of source sites. This element has the following attributes:

type:

The type of spatial distribution. Valid options are “box”, “fission”, “point”, “cartesian”, “cylindrical”, and “spherical”. A “box” spatial distribution has coordinates sampled uniformly in a parallelepiped. A “fission” spatial distribution samples locations from a “box” distribution but only locations in fissionable materials are accepted. A “point” spatial distribution has coordinates specified by a triplet. A “cartesian” spatial distribution specifies independent distributions of x-, y-, and z-coordinates. A “cylindrical” spatial distribution specifies independent distributions of r-, phi-, and z-coordinates where phi is the azimuthal angle and the origin for the cylindrical coordinate system is specified by origin. A “spherical” spatial distribution specifies independent distributions of r-, theta-, and phi-coordinates where theta is the angle with respect to the z-axis, phi is the azimuthal angle, and the sphere is centered on the coordinate (x0,y0,z0).

Default: None

parameters:

For a “box” or “fission” spatial distribution, parameters should be given as six real numbers, the first three of which specify the lower-left corner of a parallelepiped and the last three of which specify the upper-right corner. Source sites are sampled uniformly through that parallelepiped.

For a “point” spatial distribution, parameters should be given as three real numbers which specify the (x,y,z) location of an isotropic point source.

For an “cartesian” distribution, no parameters are specified. Instead, the x, y, and z elements must be specified.

For a “cylindrical” distribution, no parameters are specified. Instead, the r, phi, z, and origin elements must be specified.

For a “spherical” distribution, no parameters are specified. Instead, the r, theta, phi, and origin elements must be specified.

Default: None

x:

For an “cartesian” distribution, this element specifies the distribution of x-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

y:

For an “cartesian” distribution, this element specifies the distribution of y-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

z:

For both “cartesian” and “cylindrical” distributions, this element specifies the distribution of z-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

r:

For “cylindrical” and “spherical” distributions, this element specifies the distribution of r-coordinates (cylindrical radius and spherical radius, respectively). The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

theta:

For a “spherical” distribution, this element specifies the distribution of theta-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

phi:

For “cylindrical” and “spherical” distributions, this element specifies the distribution of phi-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

origin:

For “cylindrical and “spherical” distributions, this element specifies the coordinates for the origin of the coordinate system.

angle:

An element specifying the angular distribution of source sites. This element has the following attributes:

type:

The type of angular distribution. Valid options are “isotropic”, “monodirectional”, and “mu-phi”. The angle of the particle emitted from a source site is isotropic if the “isotropic” option is given. The angle of the particle emitted from a source site is the direction specified in the reference_uvw element/attribute if “monodirectional” option is given. The “mu-phi” option produces directions with the cosine of the polar angle and the azimuthal angle explicitly specified.

Default: isotropic

reference_uvw:

The direction from which the polar angle is measured. Represented by the x-, y-, and z-components of a unit vector. For a monodirectional distribution, this defines the direction of all sampled particles.

mu:

An element specifying the distribution of the cosine of the polar angle. Only relevant when the type is “mu-phi”. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

phi:

An element specifying the distribution of the azimuthal angle. Only relevant when the type is “mu-phi”. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

energy:

An element specifying the energy distribution of source sites. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).

Default: Watt spectrum with \(a\) = 0.988 MeV and \(b\) = 2.249 MeV -1

write_initial:

An element specifying whether to write out the initial source bank used at the beginning of the first batch. The output file is named “initial_source.h5”

Default: false

Univariate Probability Distributions

Various components of a source distribution involve probability distributions of a single random variable, e.g. the distribution of the energy, the distribution of the polar angle, and the distribution of x-coordinates. Each of these components supports the same syntax with an element whose tag signifies the variable and whose sub-elements/attributes are as follows:

type:

The type of the distribution. Valid options are “uniform”, “discrete”, “tabular”, “maxwell”, and “watt”. The “uniform” option produces variates sampled from a uniform distribution over a finite interval. The “discrete” option produces random variates that can assume a finite number of values (i.e., a distribution characterized by a probability mass function). The “tabular” option produces random variates sampled from a tabulated distribution where the density function is either a histogram or linearly-interpolated between tabulated points. The “watt” option produces random variates is sampled from a Watt fission spectrum (only used for energies). The “maxwell” option produce variates sampled from a Maxwell fission spectrum (only used for energies).

Default: None

parameters:

For a “uniform” distribution, parameters should be given as two real numbers \(a\) and \(b\) that define the interval \([a,b]\) over which random variates are sampled.

For a “discrete” or “tabular” distribution, parameters provides the \((x,p)\) pairs defining the discrete/tabular distribution. All \(x\) points are given first followed by corresponding \(p\) points.

For a “watt” distribution, parameters should be given as two real numbers \(a\) and \(b\) that parameterize the distribution \(p(x) dx = c e^{-x/a} \sinh \sqrt{b \, x} dx\).

For a “maxwell” distribution, parameters should be given as one real number \(a\) that parameterizes the distribution \(p(x) dx = c x e^{-x/a} dx\).

Note

The above format should be used even when using the multi-group <energy_mode> Element.

interpolation:

For a “tabular” distribution, interpolation can be set to “histogram” or “linear-linear” thereby specifying how tabular points are to be interpolated.

Default: histogram

<state_point> Element

The <state_point> element indicates at what batches a state point file should be written. A state point file can be used to restart a run or to get tally results at any batch. The default behavior when using this tag is to write out the source bank in the state_point file. This behavior can be customized by using the <source_point> element. This element has the following attributes/sub-elements:

batches:

A list of integers separated by spaces indicating at what batches a state point file should be written.

Default: Last batch only

<source_point> Element

The <source_point> element indicates at what batches the source bank should be written. The source bank can be either written out within a state point file or separately in a source point file. This element has the following attributes/sub-elements:

batches:

A list of integers separated by spaces indicating at what batches a state point file should be written. It should be noted that if the separate attribute is not set to “true”, this list must be a subset of state point batches.

Default: Last batch only

separate:

If this element is set to “true”, a separate binary source point file will be written. Otherwise, the source sites will be written in the state point directly.

Default: false

write:

If this element is set to “false”, source sites are not written to the state point or source point file. This can substantially reduce the size of state points if large numbers of particles per batch are used.

Default: true

overwrite_latest:
 

If this element is set to “true”, a source point file containing the source bank will be written out to a separate file named source.binary or source.h5 depending on if HDF5 is enabled. This file will be overwritten at every single batch so that the latest source bank will be available. It should be noted that a user can set both this element to “true” and specify batches to write a permanent source bank.

Default: false

<surf_src_read> Element

The <surf_src_read> element specifies a surface source file for OpenMC to read source bank for initializing histories. This element has the following attributes/sub-elements:

path:

Absolute or relative path to a surface source file to read in source bank.

Default: surface_source.h5 in current working directory

<surf_src_write> Element

The <surf_src_write> element triggers OpenMC to bank particles crossing certain surfaces and write out the source bank in a separate file called surface_source.h5. This element has the following attributes/sub-elements:

surface_ids:

A list of integers separated by spaces indicating the unique IDs of surfaces for which crossing particles will be banked.

Default: None

max_particles:

An integer indicating the maximum number of particles to be banked on specified surfaces per processor. The size of source bank in surface_source.h5 is limited to this value times the number of processors.

Default: None

<survival_biasing> Element

The <survival_biasing> element has no attributes and has an accepted value of “true” or “false”. If set to “true”, this option will enable the use of survival biasing, otherwise known as implicit capture or absorption.

Default: false
<tabular_legendre> Element

The optional <tabular_legendre> element specifies how the multi-group Legendre scattering kernel is represented if encountered in a multi-group problem. Specifically, the options are to either convert the Legendre expansion to a tabular representation or leave it as a set of Legendre coefficients. Converting to a tabular representation will cost memory but can allow for a decrease in runtime compared to leaving as a set of Legendre coefficients. This element has the following attributes/sub-elements:

enable:

This attribute/sub-element denotes whether or not the conversion of a Legendre scattering expansion to the tabular format should be performed or not. A value of “true” means the conversion should be performed, “false” means it will not.

Default: true

num_points:

If the conversion is to take place the number of tabular points is required. This attribute/sub-element allows the user to set the desired number of points.

Default: 33

Note

This element is only used in the multi-group <energy_mode> Element.

<temperature_default> Element

The <temperature_default> element specifies a default temperature in Kelvin that is to be applied to cells in the absence of an explicit cell temperature or a material default temperature.

Default: 293.6 K
<temperature_method> Element

The <temperature_method> element has an accepted value of “nearest” or “interpolation”. A value of “nearest” indicates that for each cell, the nearest temperature at which cross sections are given is to be applied, within a given tolerance (see <temperature_tolerance> Element). A value of “interpolation” indicates that cross sections are to be linear-linear interpolated between temperatures at which nuclear data are present (see Temperature Treatment).

Default: “nearest”
<temperature_multipole> Element

The <temperature_multipole> element toggles the windowed multipole capability on or off. If this element is set to “True” and the relevant data is available, OpenMC will use the windowed multipole method to evaluate and Doppler broaden cross sections in the resolved resonance range. This override other methods like “nearest” and “interpolation” in the resolved resonance range.

Default: False
<temperature_range> Element

The <temperature_range> element specifies a minimum and maximum temperature in Kelvin above and below which cross sections should be loaded for all nuclides and thermal scattering tables. This can be used for multi-physics simulations where the temperatures might change from one iteration to the next.

Default: None
<temperature_tolerance> Element

The <temperature_tolerance> element specifies a tolerance in Kelvin that is to be applied when the “nearest” temperature method is used. For example, if a cell temperature is 340 K and the tolerance is 15 K, then the closest temperature in the range of 325 K to 355 K will be used to evaluate cross sections.

Default: 10 K
<trace> Element

The <trace> element can be used to print out detailed information about a single particle during a simulation. This element should be followed by three integers: the batch number, generation number, and particle number.

Default: None
<track> Element

The <track> element specifies particles for which OpenMC will output binary files describing particle position at every step of its transport. This element should be followed by triplets of integers. Each triplet describes one particle. The integers in each triplet specify the batch number, generation number, and particle number, respectively.

Default: None
<trigger> Element

OpenMC includes tally precision triggers which allow the user to define uncertainty thresholds on \(k_{eff}\) in the <keff_trigger> subelement of settings.xml, and/or tallies in tallies.xml. When using triggers, OpenMC will run until it completes as many batches as defined by <batches>. At this point, the uncertainties on all tallied values are computed and compared with their corresponding trigger thresholds. If any triggers have not been met, OpenMC will continue until either all trigger thresholds have been satisfied or <max_batches> has been reached.

The <trigger> element provides an active “toggle switch” for tally precision trigger(s), the maximum number of batches and the batch interval. It has the following attributes/sub-elements:

active:

This determines whether or not to use trigger(s). Trigger(s) are used when this tag is set to “true”.

max_batches:

This describes the maximum number of batches allowed when using trigger(s).

Note

When max_batches is set, the number of batches shown in the <batches> element represents minimum number of batches to simulate when using the trigger(s).

batch_interval:

This tag describes the number of batches in between convergence checks. OpenMC will check if the trigger has been reached at each batch defined by batch_interval after the minimum number of batches is reached.

Note

If this tag is not present, the batch_interval is predicted dynamically by OpenMC for each convergence check. The predictive model assumes no correlation between fission sources distributions from batch-to-batch. This assumption is reasonable for fixed source and small criticality calculations, but is very optimistic for highly coupled full-core reactor problems.

<ufs_mesh> Element

The <ufs_mesh> element indicates the ID of a mesh that is used for re-weighting source sites at every generation based on the uniform fission site methodology described in Kelly et al., “MC21 Analysis of the Nuclear Energy Agency Monte Carlo Performance Benchmark Problem,” Proceedings of Physor 2012, Knoxville, TN (2012). The mesh should cover all possible fissionable materials in the problem and is specified using a <mesh> Element.

<verbosity> Element

The <verbosity> element tells the code how much information to display to the standard output. A higher verbosity corresponds to more information being displayed. The text of this element should be an integer between between 1 and 10. The verbosity levels are defined as follows:

1:don’t display any output
2:only show OpenMC logo
3:all of the above + headers
4:all of the above + results
5:all of the above + file I/O
6:all of the above + timing statistics and initialization messages
7:all of the above + \(k\) by generation
9:all of the above + indicate when each particle starts
10:all of the above + event information

Default: 7

<volume_calc> Element

The <volume_calc> element indicates that a stochastic volume calculation should be run at the beginning of the simulation. This element has the following sub-elements/attributes:

cells:

The unique IDs of cells for which the volume should be estimated.

Default: None

samples:

The number of samples used to estimate volumes.

Default: None

lower_left:

The lower-left Cartesian coordinates of a bounding box that is used to sample points within.

Default: None

upper_right:

The upper-right Cartesian coordinates of a bounding box that is used to sample points within.

Default: None

Tallies Specification – tallies.xml

The tallies.xml file allows the user to tell the code what results he/she is interested in, e.g. the fission rate in a given cell or the current across a given surface. There are two pieces of information that determine what quantities should be scored. First, one needs to specify what region of phase space should count towards the tally and secondly, the actual quantity to be scored also needs to be specified. The first set of parameters we call filters since they effectively serve to filter events, allowing some to score and preventing others from scoring to the tally.

The structure of tallies in OpenMC is flexible in that any combination of filters can be used for a tally. The following types of filter are available: cell, universe, material, surface, birth region, pre-collision energy, post-collision energy, and an arbitrary structured mesh.

The five valid elements in the tallies.xml file are <tally>, <filter>, <mesh>, <derivative>, and <assume_separate>.

<tally> Element

The <tally> element accepts the following sub-elements:

name:

An optional string name to identify the tally in summary output files. This string is limited to 52 characters for formatting purposes.

Default: “”

filters:

A space-separated list of the IDs of filter elements.

nuclides:

If specified, the scores listed will be for particular nuclides, not the summation of reactions from all nuclides. The format for nuclides should be [Atomic symbol]-[Mass number], e.g. “U-235”. The reaction rate for all nuclides can be obtained with “total”. For example, to obtain the reaction rates for U-235, Pu-239, and all nuclides in a material, this element should be:

<nuclides>U-235 Pu-239 total</nuclides>

Default: total

estimator:

The estimator element is used to force the use of either analog, collision, or tracklength tally estimation. analog is generally the least efficient though it can be used with every score type. tracklength is generally the most efficient, but neither tracklength nor collision can be used to score a tally that requires post-collision information. For example, a scattering tally with outgoing energy filters cannot be used with tracklength or collision because the code will not know the outgoing energy distribution.

Default: tracklength but will revert to analog if necessary.

scores:

A space-separated list of the desired responses to be accumulated. A full list of valid scores can be found in the user’s guide.

trigger:

Precision trigger applied to all filter bins and nuclides for this tally. It must specify the trigger’s type, threshold and scores to which it will be applied. It has the following attributes/sub-elements:

type:

The type of the trigger. Accepted options are “variance”, “std_dev”, and “rel_err”.

variance:Variance of the batch mean \(\sigma^2\)
std_dev:Standard deviation of the batch mean \(\sigma\)
rel_err:Relative error of the batch mean \(\frac{\sigma}{\mu}\)

Default: None

threshold:

The precision trigger’s convergence criterion for tallied values.

Default: None

scores:

The score(s) in this tally to which the trigger should be applied.

Note

The scores in trigger must have been defined in scores in tally. An optional “all” may be used to select all scores in this tally.

Default: “all”

derivative:

The id of a derivative element. This derivative will be applied to all scores in the tally. Differential tallies are currently only implemented for collision and analog estimators.

Default: None

<filter> Element

Filters can be used to modify tally behavior. Most tallies (e.g. cell, energy, and material) restrict the tally so that only particles within certain regions of phase space contribute to the tally. Others (e.g. delayedgroup and energyfunction) can apply some other function to the scored values. The filter element has the following attributes/sub-elements:

type:The type of the filter. Accepted options are “cell”, “cellfrom”, “cellborn”, “surface”, “material”, “universe”, “energy”, “energyout”, “mu”, “polar”, “azimuthal”, “mesh”, “distribcell”, “delayedgroup”, “energyfunction”, and “particle”.
bins:A description of the bins for each type of filter can be found in Filter Types.
energy:energyfunction filters multiply tally scores by an arbitrary function. The function is described by a piecewise linear-linear set of (energy, y) values. This entry specifies the energy values. The function will be evaluated as zero outside of the bounds of this energy grid. (Only used for energyfunction filters)
y:energyfunction filters multiply tally scores by an arbitrary function. The function is described by a piecewise linear-linear set of (energy, y) values. This entry specifies the y values. (Only used for energyfunction filters)
Filter Types

For each filter type, the following table describes what the bins attribute should be set to:

cell:

A list of unique IDs for cells in which the tally should be accumulated.

surface:

This filter allows the tally to be scored when crossing a surface. A list of surface IDs should be given. By default, net currents are tallied, and to tally a partial current from one cell to another, this should be used in combination with a cell or cell_from filter that defines the other cell. This filter should not be used in combination with a meshfilter.

cellfrom:

This filter allows the tally to be scored when crossing a surface and the particle came from a specified cell. A list of cell IDs should be given. To tally a partial current from a cell to another, this filter should be used in combination with a cell filter, to define the other cell. This filter should not be used in combination with a meshfilter.

cellborn:

This filter allows the tally to be scored to only when particles were originally born in a specified cell. A list of cell IDs should be given.

material:

A list of unique IDs for materials in which the tally should be accumulated.

universe:

A list of unique IDs for universes in which the tally should be accumulated.

energy:

In continuous-energy mode, this filter should be provided as a monotonically increasing list of bounding pre-collision energies for a number of groups. For example, if this filter is specified as

<filter type="energy" bins="0.0 1.0e6 20.0e6" />

then two energy bins will be created, one with energies between 0 and 1 MeV and the other with energies between 1 and 20 MeV.

In multi-group mode the bins provided must match group edges defined in the multi-group library.

energyout:

In continuous-energy mode, this filter should be provided as a monotonically increasing list of bounding post-collision energies for a number of groups. For example, if this filter is specified as

<filter type="energyout" bins="0.0 1.0e6 20.0e6" />

then two post-collision energy bins will be created, one with energies between 0 and 1 MeV and the other with energies between 1 and 20 MeV.

In multi-group mode the bins provided must match group edges defined in the multi-group library.

mu:

A monotonically increasing list of bounding post-collision cosines of the change in a particle’s angle (i.e., \(\mu = \hat{\Omega} \cdot \hat{\Omega}'\)), which represents a portion of the possible values of \([-1,1]\). For example, spanning all of \([-1,1]\) with five equi-width bins can be specified as:

<filter type="mu" bins="-1.0 -0.6 -0.2 0.2 0.6 1.0" />

Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([-1,1]\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([-1,1]\) can be instead written as:

<filter type="mu" bins="5" />
polar:

A monotonically increasing list of bounding particle polar angles which represents a portion of the possible values of \([0,\pi]\). For example, spanning all of \([0,\pi]\) with five equi-width bins can be specified as:

<filter type="polar" bins="0.0 0.6283 1.2566 1.8850 2.5132 3.1416"/>

Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([0,\pi]\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([0,\pi]\) can be instead written as:

<filter type="polar" bins="5" />
azimuthal:

A monotonically increasing list of bounding particle azimuthal angles which represents a portion of the possible values of \([-\pi,\pi)\). For example, spanning all of \([-\pi,\pi)\) with two equi-width bins can be specified as:

<filter type="azimuthal" bins="0.0 3.1416 6.2832" />

Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([-\pi,\pi)\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([-\pi,\pi)\) can be instead written as:

<filter type="azimuthal" bins="2" />
mesh:

The unique ID of a mesh to be tallied over.

distribcell:

The single cell which should be tallied uniquely for all instances.

Note

The distribcell filter will take a single cell ID and will tally each unique occurrence of that cell separately. This filter will not accept more than one cell ID. It is not recommended to combine this filter with a cell or mesh filter.

delayedgroup:

A list of delayed neutron precursor groups for which the tally should be accumulated. For instance, to tally to all 6 delayed groups in the ENDF/B-VII.1 library the filter is specified as:

<filter type="delayedgroup" bins="1 2 3 4 5 6" />
energyfunction:

energyfunction filters do not use the bins entry. Instead they use energy and y.

particle:

A list of integers indicating the type of particles to tally (‘neutron’ = 1, ‘photon’ = 2, ‘electron’ = 3, ‘positron’ = 4).

<mesh> Element

If a mesh is desired as a filter for a tally, it must be specified in a separate element with the tag name <mesh>. This element has the following attributes/sub-elements:

type:The type of mesh. This can be either “regular”, “rectilinear”, or “unstructured”.
dimension:The number of mesh cells in each direction. (For regular mesh only.)
lower_left:The lower-left corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh. (For regular mesh only.)
upper_right:The upper-right corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh. (For regular mesh only.)
width:The width of mesh cells in each direction. (For regular mesh only.)
x_grid:The mesh divisions along the x-axis. (For rectilinear mesh only.)
y_grid:The mesh divisions along the y-axis. (For rectilinear mesh only.)
z_grid:The mesh divisions along the z-axis. (For rectilinear mesh only.)
mesh_file:The name of the mesh file to be loaded at runtime. (For unstructured mesh only.)

Note

One of <upper_right> or <width> must be specified, but not both (even if they are consistent with one another).

<derivative> Element

OpenMC can take the first-order derivative of many tallies with respect to material perturbations. It works by propagating a derivative through the transport equation. Essentially, OpenMC keeps track of how each particle’s weight would change as materials are perturbed, and then accounts for that weight change in the tallies. Note that this assumes material perturbations are small enough not to change the distribution of fission sites. This element has the following attributes/sub-elements:

id:A unique integer that can be used to identify the derivative.
variable:The independent variable of the derivative. Accepted options are “density”, “nuclide_density”, and “temperature”. A “density” derivative will give the derivative with respect to the density of the material in [g / cm^3]. A “nuclide_density” derivative will give the derivative with respect to the density of a particular nuclide in units of [atom / b / cm]. A “temperature” derivative is with respect to a material temperature in units of [K]. The temperature derivative requires windowed multipole to be turned on. Note also that the temperature derivative only accounts for resolved resonance Doppler broadening. It does not account for thermal expansion, S(a, b) scattering, resonance scattering, or unresolved Doppler broadening.
material:The perturbed material. (Necessary for all derivative types)
nuclide:The perturbed nuclide. (Necessary only for “nuclide_density”)
<assume_separate> Element

In cases where the user needs to specify many different tallies each of which are spatially separate, this tag can be used to cut down on some of the tally overhead. The effect of assuming all tallies are spatially separate is that once one tally is scored to, the same event is assumed not to score to any other tallies. This element should be followed by “true” or “false”.

Warning

If used incorrectly, the assumption that all tallies are spatially separate can lead to incorrect results.

Default: false

Geometry Plotting Specification – plots.xml

Basic plotting capabilities are available in OpenMC by creating a plots.xml file and subsequently running with the --plot command-line flag. The root element of the plots.xml is simply <plots> and any number output plots can be defined with <plot> sub-elements. Two plot types are currently implemented in openMC:

  • slice 2D pixel plot along one of the major axes. Produces a PPM image file.
  • voxel 3D voxel data dump. Produces a binary file containing voxel xyz position and cell or material id.
<plot> Element

Each plot is specified by a combination of the following attributes or sub-elements:

id:

The unique id of the plot.

Default: None - Required entry

filename:

Filename for the output plot file.

Default: “plot”

color_by:

Keyword for plot coloring. This can be either “cell” or “material”, which colors regions by cells and materials, respectively. For voxel plots, this determines which id (cell or material) is associated with each position.

Default: “cell”

level:

Universe depth to plot at (optional). This parameter controls how many universe levels deep to pull cell and material ids from when setting plot colors. If a given location does not have as many levels as specified, colors will be taken from the lowest level at that location. For example, if level is set to zero colors will be taken from top-level (universe zero) cells only. However, if level is set to 1 colors will be taken from cells in universes that fill top-level fill-cells, and from top-level cells that contain materials.

Default: Whatever the deepest universe is in the model

origin:

Specifies the (x,y,z) coordinate of the center of the plot. Should be three floats separated by spaces.

Default: None - Required entry

width:

Specifies the width of the plot along each of the basis directions. Should be two or three floats separated by spaces for 2D plots and 3D plots, respectively.

Default: None - Required entry

type:

Keyword for type of plot to be produced. Currently only “slice” and “voxel” plots are implemented. The “slice” plot type creates 2D pixel maps saved in the PPM file format. PPM files can be displayed in most viewers (e.g. the default Gnome viewer, IrfanView, etc.). The “voxel” plot type produces a binary datafile containing voxel grid positioning and the cell or material (specified by the color tag) at the center of each voxel. These datafiles can be processed into VTK files using the openmc-voxel-to-vtk script provided with OpenMC, and subsequently viewed with a 3D viewer such as VISIT or Paraview. See the Voxel Plot File Format for information about the datafile structure.

Note

Since the PPM format is saved without any kind of compression, the resulting file sizes can be quite large. Saving the image in the PNG format can often times reduce the file size by orders of magnitude without any loss of image quality. Likewise, high-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller.

Default: “slice”

<plot> elements of type “slice” and “voxel” must contain the pixels attribute or sub-element:

pixels:

Specifies the number of pixels or voxels to be used along each of the basis directions for “slice” and “voxel” plots, respectively. Should be two or three integers separated by spaces.

Warning

The pixels input determines the output file size. For the PPM format, 10 million pixels will result in a file just under 30 MB in size. A 10 million voxel binary file will be around 40 MB.

Warning

If the aspect ratio defined in pixels does not match the aspect ratio defined in width the plot may appear stretched or squeezed.

Warning

Geometry features along a basis direction smaller than width/pixels along that basis direction may not appear in the plot.

Default: None - Required entry for “slice” and “voxel” plots

<plot> elements of type “slice” can also contain the following attributes or sub-elements. These are not used in “voxel” plots:

basis:

Keyword specifying the plane of the plot for “slice” type plots. Can be one of: “xy”, “xz”, “yz”.

Default: “xy”

background:

Specifies the RGB color of the regions where no OpenMC cell can be found. Should be three integers separated by spaces.

Default: 0 0 0 (black)

color:

Any number of this optional tag may be included in each <plot> element, which can override the default random colors for cells or materials. Each color element must contain id and rgb sub-elements.

id:Specifies the cell or material unique id for the color specification.
rgb:Specifies the custom color for the cell or material. Should be 3 integers separated by spaces.

As an example, if your plot is colored by material and you want material 23 to be blue, the corresponding color element would look like:

<color id="23" rgb="0 0 255" />

Default: None

mask:

The special mask sub-element allows for the selective plotting of only user-specified cells or materials. Only one mask element is allowed per plot element, and it must contain as attributes or sub-elements a background masking color and a list of cells or materials to plot:

components:List of unique id numbers of the cells or materials to plot. Should be any number of integers separated by spaces.
background:Color to apply to all cells or materials not in the components list of cells or materials to plot. This overrides any color color specifications.

Default: 255 255 255 (white)

meshlines:

The meshlines sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only one meshlines element is allowed per plot element, and it must contain as attributes or sub-elements a mesh type and a linewidth. Optionally, a color may be specified for the overlay:

meshtype:

The type of the mesh to be plotted. Valid options are “tally”, “entropy”, “ufs”, and “cmfd”. If plotting “tally” meshes, the id of the mesh to plot must be specified with the id sub-element.

id:

A single integer id number for the mesh specified on tallies.xml that should be plotted. This element is only required for meshtype="tally".

linewidth:

A single integer number of pixels of linewidth to specify for the mesh boundaries. Specifying this as 0 indicates that lines will be 1 pixel thick, specifying 1 indicates 3 pixels thick, specifying 2 indicates 5 pixels thick, etc.

color:

Specifies the custom color for the meshlines boundaries. Should be 3 integers separated by whitespace. This element is optional.

Default: 0 0 0 (black)

Default: None

Data Files

Cross Sections Listing – cross_sections.xml

<directory> Element

The <directory> element specifies a root directory to which the path for all files listed in a <library> Element are given relative to. This element has no attributes or sub-elements; the directory should be given within the text node. For example,

<directory>/opt/data/cross_sections/</directory>
<library> Element

The <library> element indicates where an HDF5 data file is located, whether it contains incident neutron, incident photon, thermal scattering, or windowed multipole data, and what materials are listed within. It has the following attributes:

materials:

A space-separated list of nuclides or thermal scattering tables. For example,

<library materials="U234 U235 U238" />
<library materials="c_H_in_H2O c_D_in_G2O" />

Often, just a single nuclide or thermal scattering table is contained in a given file.

path:

Path to the HDF5 file. If the <directory> Element is specified, the path is relative to the directory given. Otherwise, it is relative to the directory containing the cross_sections.xml file.

type:

The type of data contained in the file. Accepted values are ‘neutron’, ‘thermal’, ‘photon’, and ‘wmp’.

<depletion_chain> Element

The <depletion_chain> element indicates the location of the depletion chain file. This file contains information describing how nuclides decay and transmute to other nuclides through the depletion process. This element has a single attribute, path, pointing to the location of the chain file.

<depletion_chain path="/opt/data/chain_endfb7.xml"/>

The structure of the depletion chain file is explained in Depletion Chain – chain.xml.

Depletion Chain – chain.xml

A depletion chain file has a <depletion_chain> root element with one or more <nuclide> child elements. The decay, reaction, and fission product data for each nuclide appears as child elements of <nuclide>.

<nuclide> Element

The <nuclide> element contains information on the decay modes, reactions, and fission product yields for a given nuclide in the depletion chain. This element may have the following attributes:

name:Name of the nuclide
half_life:Half-life of the nuclide in [s]
decay_modes:Number of decay modes present
decay_energy:Decay energy released in [eV]
reactions:Number of reactions present

For each decay mode, a <decay> Element appears as a child of <nuclide>. For each reaction present, a <reaction> Element appears as a child of <nuclide>. If the nuclide is fissionable, a <neutron_fission_yields> Element appears as well.

<decay> Element

The <decay> element represents a single decay mode and has the following attributes:

type:The type of the decay, e.g. ‘ec/beta+’
target:The daughter nuclide produced from the decay
branching_ratio:
 The branching ratio for this decay mode
<reaction> Element

The <reaction> element represents a single transmutation reaction. This element has the following attributes:

type:The type of the reaction, e.g., ‘(n,gamma)’
Q:The Q value of the reaction in [eV]
target:The nuclide produced in the reaction (absent if the type is ‘fission’)
branching_ratio:
 The branching ratio for the reaction
<neutron_fission_yields> Element

The <neutron_fission_yields> element provides yields of fission products for fissionable nuclides. Normally, it has the follow sub-elements:

energies:

Energies in [eV] at which yields for products are tabulated

fission_yields:

Fission product yields for a single energy point. This element itself has a number of attributes/sub-elements:

energy:Energy in [eV] at which yields are tabulated
products:Names of fission products
data:Independent yields for each fission product

In the event that a nuclide doesn’t have any known fission product yields, it is possible to have that nuclide borrow yields from another nuclide by indicating the other nuclide in a single parent attribute. For example:

<neutron_fission_yields parent="U235"/>

Nuclear Data File Formats

Incident Neutron Data

/

Attributes:
  • filetype (char[]) – String indicating the type of file
  • version (int[2]) – Major and minor version of the data

/<nuclide name>/

Attributes:
  • Z (int) – Atomic number
  • A (int) – Mass number. For a natural element, A=0 is given.
  • metastable (int) – Metastable state (0=ground, 1=first excited, etc.)
  • atomic_weight_ratio (double) – Mass in units of neutron masses
  • n_reaction (int) – Number of reactions
Datasets:
  • energy (double[]) – Energies in [eV] at which cross sections are tabulated

/<nuclide name>/kTs/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Datasets:
  • <TTT>K (double) – kT values in [eV] for each temperature TTT (in Kelvin)

/<nuclide name>/reactions/reaction_<mt>/

Attributes:
  • mt (int) – ENDF MT reaction number
  • label (char[]) – Name of the reaction
  • Q_value (double) – Q value in eV
  • center_of_mass (int) – Whether the reference frame for scattering is center-of-mass (1) or laboratory (0)
  • n_product (int) – Number of reaction products
  • redundant (int) – Whether reaction is redundant

/<nuclide name>/reactions/reaction_<mt>/<TTT>K/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Datasets:
  • xs (double[]) – Cross section values tabulated against the nuclide energy grid for temperature TTT (in Kelvin)
    Attributes:
    • threshold_idx (int) – Index on the energy grid that the reaction threshold corresponds to for temperature TTT (in Kelvin)

/<nuclide name>/reactions/reaction_<mt>/product_<j>/

Reaction product data is described in Reaction Products.

/<nuclide name>/urr/<TTT>K/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Attributes:
  • interpolation (int) – interpolation scheme
  • inelastic (int) – flag indicating inelastic scattering
  • other_absorb (int) – flag indicating other absorption
  • factors (int) – flag indicating whether tables are absolute or multipliers
Datasets:
  • energy (double[]) – Energy at which probability tables exist
  • table (double[][][]) – Probability tables

/<nuclide name>/total_nu/

This special product is used to define the total number of neutrons produced from fission. It is formatted as a reaction product, described in Reaction Products.

/<nuclide name>/fission_energy_release/

Datasets:
  • fragments (function) – Energy released in the form of fragments as a function of incident neutron energy.
  • prompt_neutrons (function) – Energy released in the form of prompt neutrons as a function of incident neutron energy.
  • delayed_neutrons (function) – Energy released in the form of delayed neutrons as a function of incident neutron energy.
  • prompt_photons (function) – Energy released in the form of prompt photons as a function of incident neutron energy.
  • delayed_photons (function) – Energy released in the form of delayed photons as a function of incident neutron energy.
  • betas (function) – Energy released in the form of betas as a function of incident neutron energy.
  • neutrinos (function) – Energy released in the form of neutrinos as a function of incident neutron energy.
  • q_prompt (function) – The prompt fission Q-value (fragments + prompt neutrons + prompt photons - incident energy)
  • q_recoverable (function) – The recoverable fission Q-value (Q_prompt + delayed neutrons + delayed photons + betas)
Incident Photon Data

/

Attributes:
  • filetype (char[]) – String indicating the type of file
  • version (int[2]) – Major and minor version of the data

/<element>/

Attributes:
  • Z (int) – Atomic number
Datasets:
  • energy (double[]) – Energies in [eV] at which cross sections are tabulated

/<element>/bremsstrahlung/

Attributes:
  • I (double) – Mean excitation energy in [eV]
Datasets:
  • electron_energy (double[]) – Incident electron energy in [eV]
  • photon_energy (double[]) – Outgoing photon energy as fraction of incident electron energy
  • dcs (double[][]) – Bremsstrahlung differential cross section at each incident energy in [mb/eV]
  • ionization_energy (double[]) – Ionization potential of each subshell in [eV]
  • num_electrons (int[]) – Number of electrons per subshell, with conduction electrons indicated by a negative value

/<element>/coherent/

Datasets:
  • xs (double[]) – Coherent scattering cross section in [b]
  • integrated_scattering_factor (tabulated) – Integrated coherent scattering form factor
  • anomalous_real (tabulated) – Real part of the anomalous scattering factor
  • anomalous_imag (tabulated) – Imaginary part of the anomalous scattering factor

/<element>/compton_profiles/

Datasets:
  • binding_energy (double[]) – Binding energy for each subshell in [eV]
  • num_electrons (double[]) – Number of electrons in each subshell
  • pz (double[]) – Projection of the electron momentum on the scattering vector in units of \(me^2 / \hbar\) where \(m\) is the electron rest mass and \(e\) is the electron charge
  • J (double[][]) – Compton profile for each subshell in units of \(\hbar / (me^2)\)

/<element>/heating/

Datasets:
  • xs (double[]) – Total heating cross section in [b-eV]

/<element>/incoherent/

Datasets:
  • xs (double[]) – Incoherent scattering cross section in [b]
  • scattering_factor (tabulated) –

/<element>/pair_production_electron/

Datasets:
  • xs (double[]) – Pair production (electron field) cross section in [b]

/<element>/pair_production_nuclear/

Datasets:
  • xs (double[]) – Pair production (nuclear field) cross section in [b]

/<element>/photoelectric/

Datasets:
  • xs (double[]) – Total photoionization cross section in [b]

/<element>/subshells/

Attributes:
  • designators (char[][]) – Designator for each shell, e.g. ‘M2’

/<element>/subshells/<designator>/

Attributes:
  • binding_energy (double) – Binding energy of the subshell in [eV]
  • num_electrons (double) – Number of electrons in the subshell
Datasets:
  • transitions (double[][]) – Atomic relaxation data
  • xs (double[]) – Photoionization cross section for subshell in [b] tabulated against the main energy grid
    Attributes:
    • threshold_idx (int) – Index on the energy grid of the reaction threshold
Thermal Neutron Scattering Data

/

Attributes:
  • version (int[2]) – Major and minor version of the data

/<thermal name>/

Attributes:
  • atomic_weight_ratio (double) – Mass in units of neutron masses
  • energy_max (double) – Maximum energy in [eV]
  • nuclides (char[][]) – Names of nuclides for which the thermal scattering data applies to

/<thermal name>/kTs/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Datasets:
  • <TTT>K (double) – kT values (in eV) for each temperature TTT (in Kelvin)

/<thermal name>/elastic/<TTT>K/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Datasets:
  • xs (function) – Thermal elastic scattering cross section for temperature TTT (in Kelvin)
Groups:

/<thermal name>/inelastic/<TTT>K/

<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.

Datasets:
  • xs (function) – Thermal inelastic scattering cross section for temperature TTT (in Kelvin)
Groups:
Reaction Products
Object type:

Group

Attributes:
  • particle (char[]) – Type of particle
  • emission_mode (char[]) – Emission mode (prompt, delayed, total)
  • decay_rate (double) – Rate of decay in inverse seconds
  • n_distribution (int) – Number of angle/energy distributions
Datasets:
  • yield (function) – Energy-dependent yield of the product.
Groups:
  • distribution_<k> – Formats for angle-energy distributions are detailed in Angle-Energy Distributions. When multiple angle-energy distributions occur, one dataset also may appear for each distribution:
    Datasets:
    • applicability (function) – Probability of selecting this distribution as a function of incident energy
One-dimensional Functions
Scalar
Object type:

Dataset

Datatype:

double

Attributes:
  • type (char[]) – ‘constant’
Tabulated
Object type:

Dataset

Datatype:

double[2][]

Description:

x-values are listed first followed by corresponding y-values

Attributes:
  • type (char[]) – ‘Tabulated1D’
  • breakpoints (int[]) – Region breakpoints
  • interpolation (int[]) – Region interpolation codes
Polynomial
Object type:

Dataset

Datatype:

double[]

Description:

Polynomial coefficients listed in order of increasing power

Attributes:
  • type (char[]) – ‘Polynomial’
Coherent elastic scattering
Object type:

Dataset

Datatype:

double[2][]

Description:

The first row lists Bragg edges and the second row lists structure factor cumulative sums.

Attributes:
  • type (char[]) – ‘CoherentElastic’
Incoherent elastic scattering
Object type:

Dataset

Datatype:

double[2]

Description:

The first value is the characteristic bound cross section in [b] and the second value is the Debye-Waller integral in [eV\(^{-1}\)].

Attributes:
  • type (char[]) – ‘IncoherentElastic’
Angle-Energy Distributions
Uncorrelated Angle-Energy
Object type:

Group

Attributes:
  • type (char[]) – ‘uncorrelated’
Datasets:
  • angle/energy (double[]) – energies at which angle distributions exist
  • angle/mu (double[3][]) – tabulated angular distributions for each energy. The first row gives \(\mu\) values, the second row gives the probability density, and the third row gives the cumulative distribution.
    Attributes:
    • offsets (int[]) – indices indicating where each angular distribution starts
    • interpolation (int[]) – interpolation code for each angular distribution
Groups:
Correlated Angle-Energy
Object type:

Group

Attributes:
  • type (char[]) – ‘correlated’
Datasets:
  • energy (double[]) – Incoming energies at which distributions exist
    Attributes:
    • interpolation (double[2][]) – Breakpoints and interpolation codes for incoming energy regions
  • energy_out (double[5][]) – Distribution of outgoing energies corresponding to each incoming energy. The distributions are flattened into a single array; the start of a given distribution can be determined using the offsets attribute. The first row gives outgoing energies, the second row gives the probability density, the third row gives the cumulative distribution, the fourth row gives interpolation codes for angular distributions, and the fifth row gives offsets for angular distributions.
    Attributes:
    • offsets (double[]) – Offset for each distribution
    • interpolation (int[]) – Interpolation code for each distribution
    • n_discrete_lines (int[]) – Number of discrete lines in each distribution
  • mu (double[3][]) – Distribution of angular cosines corresponding to each pair of incoming and outgoing energies. The distributions are flattened into a single array; the start of a given distribution can be determined using offsets in the fifth row of the energy_out dataset. The first row gives angular cosines, the second row gives the probability density, and the third row gives the cumulative distribution.
Kalbach-Mann
Object type:

Group

Attributes:
  • type (char[]) – ‘kalbach-mann’
Datasets:
  • energy (double[]) – Incoming energies at which distributions exist
    Attributes:
    • interpolation (double[2][]) – Breakpoints and interpolation codes for incoming energy regions
  • distribution (double[5][]) – Distribution of outgoing energies and angles corresponding to each incoming energy. The distributions are flattened into a single array; the start of a given distribution can be determined using the offsets attribute. The first row gives outgoing energies, the second row gives the probability density, the third row gives the cumulative distribution, the fourth row gives Kalbach-Mann precompound factors, and the fifth row gives Kalbach-Mann angular distribution slopes.
    Attributes:
    • offsets (double[]) – Offset for each distribution
    • interpolation (int[]) – Interpolation code for each distribution
    • n_discrete_lines (int[]) – Number of discrete lines in each distribution
N-Body Phase Space
Object type:

Group

Attributes:
  • type (char[]) – ‘nbody’
  • total_mass (double) – Total mass of product particles
  • n_particles (int) – Number of product particles
  • atomic_weight_ratio (double) – Atomic weight ratio of the target nuclide in neutron masses
  • q_value (double) – Q value for the reaction in eV
Coherent Elastic

This angle-energy distribution is used specifically for coherent elastic thermal neutron scattering.

Object type:

Group

Attributes:
  • type (char[]) – “coherent_elastic”
Hard link:
  • xs – Link to the coherent elastic scattering cross section
Incoherent Elastic

This angle-energy distribution is used specifically for incoherent elastic thermal neutron scattering (derived from an ENDF file directly).

Object type:

Group

Attributes:
  • type (char[]) – “incoherent_elastic”
Datasets:
  • debye_waller (double) – Debye-Waller integral in [eV\(^{-1}\)]
Incoherent Elastic (Discrete)

This angle-energy distribution is used for discretized incoherent elastic thermal neutron scattering distributions that are present in ACE files.

Object type:

Group

Attributes:
  • type (char[]) – “incoherent_elastic_discrete”
Datasets:
  • mu_out (double[][]) – Equiprobable discrete outgoing angles for each incident neutron energy tabulated
Incoherent Inelastic

This angle-energy distribution is used specifically for (continuous) incoherent inelastic thermal neutron scattering.

Object type:

Group

Attributes:
  • type (char[]) – “incoherent_inelastic”
Datasets:

The datasets for this angle-energy distribution are the same as for correlated angle-energy distributions.

Incoherent Inelastic (Discrete)

This angle-energy distribution is used specifically for incoherent inelastic thermal neutron scattering where the distributions have been discretized into equiprobable bins.

Object type:

Group

Attributes:
  • type (char[]) – “incoherent_inelastic_discrete”
Datasets:
  • energy_out (double[][]) – Distribution of outgoing energies for each incoming energy.
  • mu_out (double[][][]) – Distribution of scattering cosines for each pair of incoming and outgoing energies.
  • skewed (int8_t) – Whether discrete angles are equi-probable (0) or have a skewed distribution (1).
Energy Distributions
Maxwell
Object type:

Group

Attributes:
  • type (char[]) – ‘maxwell’
  • u (double) – Restriction energy in eV
Datasets:
  • theta (tabulated) – Maxwellian temperature as a function of energy
Evaporation
Object type:

Group

Attributes:
  • type (char[]) – ‘evaporation’
  • u (double) – Restriction energy in eV
Datasets:
  • theta (tabulated) – Evaporation temperature as a function of energy
Watt Fission Spectrum
Object type:

Group

Attributes:
  • type (char[]) – ‘watt’
  • u (double) – Restriction energy in eV
Datasets:
  • a (tabulated) – Watt parameter \(a\) as a function of incident energy
  • b (tabulated) – Watt parameter \(b\) as a function of incident energy
Madland-Nix
Object type:

Group

Attributes:
  • type (char[]) – ‘watt’
  • efl (double) – Average energy of light fragment in eV
  • efh (double) – Average energy of heavy fragment in eV
Discrete Photon
Object type:

Group

Attributes:
  • type (char[]) – ‘discrete_photon’
  • primary_flag (int) – Whether photon is a primary
  • energy (double) – Photon energy in eV
  • atomic_weight_ratio (double) – Atomic weight ratio of target nuclide in neutron masses
Level Inelastic
Object type:

Group

Attributes:
  • type (char[]) – ‘level’
  • threshold (double) – Energy threshold in the laboratory system in eV
  • mass_ratio (double) – \((A/(A + 1))^2\)
Continuous Tabular
Object type:

Group

Attributes:
  • type (char[]) – ‘continuous’
Datasets:
  • energy (double[]) – Incoming energies at which distributions exist
    Attributes:
    • interpolation (double[2][]) – Breakpoints and interpolation codes for incoming energy regions
  • distribution (double[3][]) – Distribution of outgoing energies corresponding to each incoming energy. The distributions are flattened into a single array; the start of a given distribution can be determined using the offsets attribute. The first row gives outgoing energies, the second row gives the probability density, and the third row gives the cumulative distribution.
    Attributes:
    • offsets (double[]) – Offset for each distribution
    • interpolation (int[]) – Interpolation code for each distribution
    • n_discrete_lines (int[]) – Number of discrete lines in each distribution

Multi-Group Cross Section Library Format

OpenMC can be run in continuous-energy mode or multi-group mode, provided the nuclear data is available. In continuous-energy mode, the cross_sections.xml file contains necessary meta-data for each dataset, including the name and a file system location where the complete library can be found. In multi-group mode, the multi-group meta-data and the nuclear data itself is contained within an mgxs.h5 file. This portion of the manual describes the format of the multi-group data library required to be used in the mgxs.h5 file.

The multi-group library is provided in the HDF5 format. This library must provide some meta-data about the library itself (such as the number of energy groups, delayed groups, and the energy group structure, etc.) as well as the actual cross section data itself for each of the necessary nuclides or materials.

The current version of the multi-group library file format is 1.0.

MGXS Library Specification

/

Attributes:
  • filetype (char[]) – String indicating the type of file; for this library it will be ‘mgxs’.
  • version (int[2]) – Major and minor version of the multi-group library file format.
  • energy_groups (int) – Number of energy groups
  • delayed_groups (int) – Number of delayed groups (optional)
  • group structure (double[]) – Monotonically increasing list of group boundaries, in units of eV. The length of this array should be the number of groups plus 1.

/<library name>/

The data within <library name> contains the temperature-dependent multi-group data for the nuclide or material that it represents.

Attributes:
  • atomic_weight_ratio (double) – The atomic weight ratio (optional, i.e. it is not meaningful for material-wise data).
  • fissionable (bool) – Whether the dataset is fissionable (True) or not (False).
  • representation (char[]) – The method used to generate and represent the multi-group cross sections. That is, whether they were generated with scalar flux weighting (or reduced to a similar representation) and thus are angle-independent, or if the data was generated with angular dependent fluxes and thus the data is angle-dependent. Valid values are either “isotropic” or “angle”.
  • num_azimuthal (int) – Number of equal width angular bins that the azimuthal angular domain is subdivided if the representation attribute is “angle”. This parameter is ignored otherwise.
  • num_polar (int) – Number of equal width angular bins that the polar angular domain is subdivided if the representation attribute is “angle”. This parameter is ignored otherwise.
  • scatter_format (char[]) – The representation of the scattering angular distribution. The options are either “legendre”, “histogram”, or “tabular”. If not provided, the default of “legendre” will be assumed.
  • order (int) – Either the Legendre order, number of bins, or number of points (depending on the value of scatter_format) used to describe the angular distribution associated with each group-to-group transfer probability.
  • scatter_shape (char[]) – The shape of the provided scatter and multiplicity matrix. The values provided are strings describing the ordering the scattering array is provided in row-major (i.e., C/C++ and Python) indexing. Valid values are “[Order][G][G’]” or “[Order][G’][G]” where “G’” denotes the secondary/outgoing energy groups, “G” denotes the incoming energy groups, and “Order” is the angular distribution index. This value is not required; if not the default value of “[Order][G][G’]” will be assumed.

/<library name>/kTs/

Datasets:
  • <TTT>K (double) – kT values (in eV) for each temperature TTT (in Kelvin), rounded to the nearest integer

/<library name>/<TTT>K/

Temperature-dependent data, provided for temperature <TTT>K.

Datasets:
  • total (double[] or double[][][]) – Total cross section. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [polar][azimuthal][groups].
  • absorption (double[] or double[][][]) – Absorption cross section. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [groups][azimuthal][polar].
  • fission (double[] or double[][][]) – Fission cross section. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [polar][azimuthal][groups]. This is only required if the dataset is fissionable and fission-tallies are expected to be used.
  • kappa-fission (double[] or double[][][]) – Kappa-Fission (energy-release from fission) cross section. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [polar][azimuthal][groups]. This is only required if the dataset is fissionable and fission-tallies are expected to be used.
  • chi (double[] or double[][][]) – Fission neutron energy spectra. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [polar][azimuthal][groups]. This is only required if the dataset is fissionable and fission-tallies are expected to be used.
  • nu-fission (double[] to double[][][][]) – Nu-Fission cross section. If chi is provided, then nu-fission has the same dimensionality as fission. If chi is not provided, then the nu-fission data must represent the fission neutron energy spectra as well and thus will have one additional dimension for the outgoing energy group. In this case, nu-fission has the same dimensionality as multiplicity matrix.
  • inverse-velocity (double[] or double[][][]) – Average inverse velocity for each of the groups in the library. This dataset is optional. This is a 1-D vector if representation is “isotropic”, or a 3-D vector if representation is “angle” with dimensions of [polar][azimuthal][groups].

/<library name>/<TTT>K/scatter_data/

Data specific to neutron scattering for the temperature <TTT>K

Datasets:
  • g_min (int[] or int[][][]) – Minimum (most energetic) groups with non-zero values of the scattering matrix provided. If scatter_shape is “[Order][G][G’]” then g_min will describe the minimum values of “G’” for each “G”; if scatter_shape is “[Order][G’][G]” then g_min will describe the minimum values of “G” for each “G’”. These group numbers use the standard ordering where the fastest neutron energy group is group 1 while the slowest neutron energy group is group G. The dimensionality of g_min is: g_min[g], or g_min[num_polar][num_azimuthal][g]. The former is used when representation is “isotropic”, and the latter when representation is “angle”.
  • g_max (int[] or int[][][]) – Similar to g_min, except this dataset describes the maximum (least energetic) groups with non-zero values of the scattering matrix.
  • scatter_matrix (double[]) – Flattened representation of the scattering moment matrices. The pre-flattened array corresponds to the shape provied in scatter_shape, but if representation is “angle” the dimensionality in scatter_shape is prepended by “[num_polar][num_azimuthal]” dimensions. The right-most energy group dimension will only include the entries between g_min and g_max. dimension has a dimensionality of g_min to g_max.
  • multiplicity_matrix (double[]) – Flattened representation of the scattering moment matrices. This dataset provides the code with a scaling factor to account for neutrons being produced in (n,xn) reactions. This is assumed isotropic and therefore is not repeated for every Legendre moment or histogram/tabular bin. This dataset is optional, if it is not provided no multiplication (i.e., values of 1.0) will be assumed. The pre-flattened array is shapes consistent with scatter_matrix except the “[Order]” dimension in scatter_shape is ignored since this data is assumed isotropic.

Windowed Multipole Library Format

/

Attributes:
  • filetype (char[]) – String indicating the type of file
  • version (int[2]) – Major and minor version of the data

/<nuclide name>/

Datasets:
  • broaden_poly (int[])

    If 1, Doppler broaden curve fit for window with corresponding index. If 0, do not.

  • curvefit (double[][][])

    Curve fit coefficients. Indexed by (window index, coefficient index, reaction type).

  • data (complex[][])

    Complex poles and residues. Each pole has a corresponding set of residues. For example, the \(i\)-th pole and corresponding residues are stored as

    \[\text{data}[:,i] = [\text{pole},~\text{residue}_1,~\text{residue}_2, ~\ldots] \]

    The residues are in the order: scattering, absorption, fission. Complex numbers are stored by forming a type with “\(r\)” and “\(i\)” identifiers, similar to how h5py does it.

  • E_max (double)

    Highest energy the windowed multipole part of the library is valid for.

  • E_min (double)

    Lowest energy the windowed multipole part of the library is valid for.

  • spacing (double)
    \[\frac{\sqrt{E_{max}} - \sqrt{E_{min}}}{n_w} \]

    Where \(E_{max}\) is the maximum energy the windows go up to. \(E_{min}\) is the minimum energy, and \(n_w\) is the number of windows, given by windows.

  • sqrtAWR (double)

    Square root of the atomic weight ratio.

  • windows (int[][])

    The poles to start from and end at for each window. windows[i, 0] and windows[i, 1] are, respectively, the indexes (1-based) of the first and last pole in window i.

Output Files

State Point File Format

The current version of the statepoint file format is 17.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the statepoint file format.
  • openmc_version (int[3]) – Major, minor, and release version number for OpenMC.
  • git_sha1 (char[40]) – Git commit SHA-1 hash.
  • date_and_time (char[]) – Date and time the summary was written.
  • path (char[]) – Path to directory containing input files.
  • tallies_present (int) – Flag indicating whether tallies are present (1) or not (0).
  • source_present (int) – Flag indicating whether the source bank is present (1) or not (0).
Datasets:
  • seed (int8_t) – Pseudo-random number generator seed.
  • energy_mode (char[]) – Energy mode of the run, either ‘continuous-energy’ or ‘multi-group’.
  • run_mode (char[]) – Run mode used, either ‘eigenvalue’ or ‘fixed source’.
  • n_particles (int8_t) – Number of particles used per generation.
  • n_batches (int) – Number of batches to simulate.
  • current_batch (int) – The number of batches already simulated.
  • n_inactive (int) – Number of inactive batches. Only present when run_mode is ‘eigenvalue’.
  • generations_per_batch (int) – Number of generations per batch. Only present when run_mode is ‘eigenvalue’.
  • k_generation (double[]) – k-effective for each generation simulated.
  • entropy (double[]) – Shannon entropy for each generation simulated.
  • k_col_abs (double) – Sum of product of collision/absorption estimates of k-effective.
  • k_col_tra (double) – Sum of product of collision/track-length estimates of k-effective.
  • k_abs_tra (double) – Sum of product of absorption/track-length estimates of k-effective.
  • k_combined (double[2]) – Mean and standard deviation of a combined estimate of k-effective.
  • n_realizations (int) – Number of realizations for global tallies.
  • global_tallies (double[][2]) – Accumulated sum and sum-of-squares for each global tally.
  • source_bank (Compound type) – Source bank information for each particle. The compound type has fields r, u, E, wgt, delayed_group, surf_id, and particle, which represent the position, direction, energy, weight, delayed group, surface ID, and particle type (0=neutron, 1=photon, 2=electron, 3=positron), respectively. Only present when run_mode is ‘eigenvalue’.

/tallies/

Attributes:
  • n_tallies (int) – Number of user-defined tallies.
  • ids (int[]) – User-defined unique ID of each tally.

/tallies/meshes/

Attributes:
  • n_meshes (int) – Number of meshes in the problem.
  • ids (int[]) – User-defined unique ID of each mesh.

/tallies/meshes/mesh <uid>/

Datasets:
  • type (char[]) – Type of mesh.
  • dimension (int) – Number of mesh cells in each dimension.
  • lower_left (double[]) – Coordinates of lower-left corner of mesh.
  • upper_right (double[]) – Coordinates of upper-right corner of mesh.
  • width (double[]) – Width of each mesh cell in each dimension.
  • Unstructured Mesh Only:
    • volumes (double[]) – Volume of each mesh cell.
    • centroids (double[]) – Location of the mesh cell centroids.

/tallies/filters/

Attributes:
  • n_filters (int) – Number of filters in the problem.
  • ids (int[]) – User-defined unique ID of each filter.

/tallies/filters/filter <uid>/

Datasets:
  • type (char[]) – Type of the j-th filter. Can be ‘universe’, ‘material’, ‘cell’, ‘cellborn’, ‘surface’, ‘mesh’, ‘energy’, ‘energyout’, ‘distribcell’, ‘mu’, ‘polar’, ‘azimuthal’, ‘delayedgroup’, or ‘energyfunction’.
  • n_bins (int) – Number of bins for the j-th filter. Not present for ‘energyfunction’ filters.
  • bins (int[] or double[]) – Value for each filter bin of this type. Not present for ‘energyfunction’ filters.
  • energy (double[]) – Energy grid points for energyfunction interpolation. Only used for ‘energyfunction’ filters.
  • y (double[]) – Interpolant values for energyfunction interpolation. Only used for ‘energyfunction’ filters.

/tallies/derivatives/derivative <id>/

Datasets:
  • independent variable (char[]) – Independent variable of tally derivative.
  • material (int) – ID of the perturbed material.
  • nuclide (char[]) – Alias of the perturbed nuclide.
  • estimator (char[]) – Type of tally estimator, either ‘analog’, ‘tracklength’, or ‘collision’.

/tallies/tally <uid>/

Attributes:
  • internal (int) – Flag indicating the presence of tally data (0) or absence of tally data (1). All user defined tallies will have a value of 0 unless otherwise instructed.
Datasets:
  • n_realizations (int) – Number of realizations.
  • n_filters (int) – Number of filters used.
  • filters (int[]) – User-defined unique IDs of the filters on the tally
  • nuclides (char[][]) – Array of nuclides to tally. Note that if no nuclide is specified in the user input, a single ‘total’ nuclide appears here.
  • derivative (int) – ID of the derivative applied to the tally.
  • n_score_bins (int) – Number of scoring bins for a single nuclide.
  • score_bins (char[][]) – Values of specified scores.
  • results (double[][][2]) – Accumulated sum and sum-of-squares for each bin of the i-th tally. The first dimension represents combinations of filter bins, the second dimensions represents scoring bins, and the third dimension has two entries for the sum and the sum-of-squares.

/runtime/

All values are given in seconds and are measured on the master process.

Datasets:
  • total initialization (double) – Time spent reading inputs, allocating arrays, etc.
  • reading cross sections (double) – Time spent loading cross section libraries (this is a subset of initialization).
  • simulation (double) – Time spent between initialization and finalization.
  • transport (double) – Time spent transporting particles.
  • inactive batches (double) – Time spent in the inactive batches (including non-transport activities like communcating sites).
  • active batches (double) – Time spent in the active batches (including non-transport activities like communicating sites).
  • synchronizing fission bank (double) – Time spent sampling source particles from fission sites and communicating them to other processes for load balancing.
  • sampling source sites (double) – Time spent sampling source particles from fission sites.
  • SEND-RECV source sites (double) – Time spent communicating source sites between processes for load balancing.
  • accumulating tallies (double) – Time spent communicating tally results and evaluating their statistics.
  • writing statepoints (double) – Time spent writing statepoint files

Source File Format

Normally, source data is stored in a state point file. However, it is possible to request that the source be written separately, in which case the format used is that documented here.

When surface source writing is triggered, a source file named surface_source.h5 is written with only the sources on specified surfaces, following the same format.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
Datasets:
  • source_bank (Compound type) – Source bank information for each particle. The compound type has fields r, u, E, wgt, delayed_group, surf_id and particle, which represent the position, direction, energy, weight, delayed group, surface ID, and particle type (0=neutron, 1=photon, 2=electron, 3=positron), respectively.

Summary File Format

The current version of the summary file format is 6.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the summary file format.
  • openmc_version (int[3]) – Major, minor, and release version number for OpenMC.
  • git_sha1 (char[40]) – Git commit SHA-1 hash.
  • date_and_time (char[]) – Date and time the summary was written.

/geometry/

Attributes:
  • n_cells (int) – Number of cells in the problem.
  • n_surfaces (int) – Number of surfaces in the problem.
  • n_universes (int) – Number of unique universes in the problem.
  • n_lattices (int) – Number of lattices in the problem.
  • dagmc (int) – Indicates that a DAGMC geometry was used if present.

/geometry/cells/cell <uid>/

Datasets:
  • name (char[]) – User-defined name of the cell.
  • universe (int) – Universe assigned to the cell. If none is specified, the default universe (0) is assigned.
  • fill_type (char[]) – Type of fill for the cell. Can be ‘material’, ‘universe’, or ‘lattice’.
  • material (int or int[]) – Unique ID of the material(s) assigned to the cell. This dataset is present only if fill_type is set to ‘normal’. The value ‘-1’ signifies void material. The data is an array if the cell uses distributed materials, otherwise it is a scalar.
  • temperature (double[]) – Temperature of the cell in Kelvin.
  • translation (double[3]) – Translation applied to the fill universe. This dataset is present only if fill_type is set to ‘universe’.
  • rotation (double[3]) – Angles in degrees about the x-, y-, and z-axes for which the fill universe should be rotated. This dataset is present only if fill_type is set to ‘universe’.
  • lattice (int) – Unique ID of the lattice which fills the cell. Only present if fill_type is set to ‘lattice’.
  • region (char[]) – Region specification for the cell.

/geometry/surfaces/surface <uid>/

Datasets:
  • name (char[]) – Name of the surface.
  • type (char[]) – Type of the surface. Can be ‘x-plane’, ‘y-plane’, ‘z-plane’, ‘plane’, ‘x-cylinder’, ‘y-cylinder’, ‘z-cylinder’, ‘sphere’, ‘x-cone’, ‘y-cone’, ‘z-cone’, or ‘quadric’.
  • coefficients (double[]) – Array of coefficients that define the surface. See <surface> Element for what coefficients are defined for each surface type.
  • boundary_condition (char[]) – Boundary condition applied to the surface. Can be ‘transmission’, ‘vacuum’, ‘reflective’, or ‘periodic’.

/geometry/universes/universe <uid>/

Datasets:
  • cells (int[]) – Array of unique IDs of cells that appear in the universe.

/geometry/lattices/lattice <uid>/

Datasets:
  • name (char[]) – Name of the lattice.
  • type (char[]) – Type of the lattice, either ‘rectangular’ or ‘hexagonal’.
  • pitch (double[]) – Pitch of the lattice in centimeters.
  • outer (int) – Outer universe assigned to lattice cells outside the defined range.
  • universes (int[][][]) – Three-dimensional array of universes assigned to each cell of the lattice.
  • dimension (int[]) – The number of lattice cells in each direction. This dataset is present only when the ‘type’ dataset is set to ‘rectangular’.
  • lower_left (double[]) – The coordinates of the lower-left corner of the lattice. This dataset is present only when the ‘type’ dataset is set to ‘rectangular’.
  • n_rings (int) – Number of radial ring positions in the xy-plane. This dataset is present only when the ‘type’ dataset is set to ‘hexagonal’.
  • n_axial (int) – Number of lattice positions along the z-axis. This dataset is present only when the ‘type’ dataset is set to ‘hexagonal’.
  • center (double[]) – Coordinates of the center of the lattice. This dataset is present only when the ‘type’ dataset is set to ‘hexagonal’.

/materials/

Attributes:
  • n_materials (int) – Number of materials in the problem.

/materials/material <uid>/

Datasets:
  • name (char[]) – Name of the material.
  • atom_density (double[]) – Total atom density of the material in atom/b-cm.
  • nuclides (char[][]) – Array of nuclides present in the material, e.g., ‘U235’. This data set is only present if nuclides are used.
  • nuclide_densities (double[]) – Atom density of each nuclide. This data set is only present if ‘nuclides’ data set is present.
  • macroscopics (char[][]) – Array of macroscopic data sets present in the material. This dataset is only present if macroscopic data sets are used in multi-group mode.
  • sab_names (char[][]) – Names of S(\(\alpha,\beta\)) tables assigned to the material.
Attributes:
  • volume (double[]) – Volume of this material [cm^3]. Only present if volume supplied
  • temperature (double[]) – Temperature of this material [K]. Only present in temperature supplied
  • depletable (int[]) – 1 if the material can be depleted, 0 otherwise. Always present

/nuclides/

Attributes:
  • n_nuclides (int) – Number of nuclides in the problem.
Datasets:
  • names (char[][]) – Names of nuclides.
  • awrs (float[]) – Atomic weight ratio of each nuclide.

/macroscopics/

Attributes:
  • n_macroscopics (int) – Number of macroscopic data sets in the problem.
Datasets:
  • names (char[][]) – Names of the macroscopic data sets.

/tallies/tally <uid>/

Datasets:
  • name (char[]) – Name of the tally.

Depletion Results File Format

The current version of the depletion results file format is 1.1.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the statepoint file format.
Datasets:
  • eigenvalues (double[][][2]) – k-eigenvalues at each time/stage. This array has shape (number of timesteps, number of stages, value). The last axis contains the eigenvalue and the associated uncertainty
  • number (double[][][][]) – Total number of atoms. This array has shape (number of timesteps, number of stages, number of materials, number of nuclides).
  • reaction rates (double[][][][][]) – Reaction rates used to build depletion matrices. This array has shape (number of timesteps, number of stages, number of materials, number of nuclides, number of reactions).
  • time (double[][2]) – Time in [s] at beginning/end of each step.
  • source_rate (double[][]) – Power in [W] or source rate in [neutron/sec]. This array has shape (number of timesteps, number of stages).
  • depletion time (double[]) – Average process time in [s] spent depleting a material across all burnable materials and, if applicable, MPI processes.

/materials/<id>/

Attributes:
  • index (int) – Index used in results for this material
  • volume (double) – Volume of this material in [cm^3]

/nuclides/<name>/

Attributes:
  • atom number index (int) – Index in array of total atoms for this nuclide
  • reaction rate index (int) – Index in array of reaction rates for this nuclide

/reactions/<name>/

Attributes:
  • index (int) – Index user in results for this reaction

Note

The reaction rates for some isotopes not originally present may be non-zero, but should be negligible compared to other atoms. This can be controlled by changing the openmc.deplete.Operator dilute_initial attribute.

Particle Restart File Format

The current version of the particle restart file format is 2.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the particle restart file format.
  • openmc_version (int[3]) – Major, minor, and release version number for OpenMC.
  • git_sha1 (char[40]) – Git commit SHA-1 hash.
Datasets:
  • current_batch (int) – The number of batches already simulated.
  • generations_per_batch (int) – Number of generations per batch.
  • current_generation (int) – The number of generations already simulated.
  • n_particles (int8_t) – Number of particles used per generation.
  • run_mode (char[]) – Run mode used, either ‘fixed source’, ‘eigenvalue’, or ‘particle restart’.
  • id (int8_t) – Unique identifier of the particle.
  • weight (double) – Weight of the particle.
  • energy (double) – Energy of the particle in eV for continuous-energy mode, or the energy group of the particle for multi-group mode.
  • xyz (double[3]) – Position of the particle.
  • uvw (double[3]) – Direction of the particle.

Track File Format

The current revision of the particle track file format is 2.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the track file format.
  • n_particles (int) – Number of particles for which tracks are recorded.
  • n_coords (int[]) – Number of coordinates for each particle.
Datasets:
  • coordinates_<i> (double[][3]) – (x,y,z) coordinates for the i-th particle.

Voxel Plot File Format

The current version of the voxel file format is 1.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the voxel file format.
  • openmc_version (int[3]) – Major, minor, and release version number for OpenMC.
  • git_sha1 (char[40]) – Git commit SHA-1 hash.
  • date_and_time (char[]) – Date and time the summary was written.
  • num_voxels (int[3]) – Number of voxels in the x-, y-, and z- directions.
  • voxel_width (double[3]) – Width of a voxel in centimeters.
  • lower_left (double[3]) – Cartesian coordinates of the lower-left corner of the plot.
Datasets:
  • data (int[][][]) – Data for each voxel that represents a material or cell ID.

Volume File Format

The current version of the volume file format is 1.0.

/

Attributes:
  • filetype (char[]) – String indicating the type of file.
  • version (int[2]) – Major and minor version of the summary file format.
  • openmc_version (int[3]) – Major, minor, and release version number for OpenMC.
  • git_sha1 (char[40]) – Git commit SHA-1 hash.
  • date_and_time (char[]) – Date and time the summary was written.
  • domain_type (char[]) – The type of domain for which volumes are calculated, either ‘cell’, ‘material’, or ‘universe’.
  • samples (int) – Number of samples
  • lower_left (double[3]) – Lower-left coordinates of bounding box
  • upper_right (double[3]) – Upper-right coordinates of bounding box
  • threshold (double) – Threshold used for volume uncertainty
  • trigger_type (char[]) – Trigger type used for volume uncertainty

/domain_<id>/

Datasets:
  • volume (double[2]) – Calculated volume and its uncertainty in cubic centimeters
  • nuclides (char[][]) – Names of nuclides identified in the domain
  • atoms (double[][2]) – Total number of atoms of each nuclide and its uncertainty

Publications

Overviews

  • Paul K. Romano, Nicholas E. Horelik, Bryan R. Herman, Adam G. Nelson, Benoit Forget, and Kord Smith, “OpenMC: A State-of-the-Art Monte Carlo Code for Research and Development,” Ann. Nucl. Energy, 82, 90–97 (2015).
  • Paul K. Romano, Bryan R. Herman, Nicholas E. Horelik, Benoit Forget, Kord Smith, and Andrew R. Siegel, “Progress and Status of the OpenMC Monte Carlo Code,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).
  • Paul K. Romano and Benoit Forget, “The OpenMC Monte Carlo Particle Transport Code,” Ann. Nucl. Energy, 51, 274–281 (2013).

Benchmarking

  • Travis J. Labossiere-Hickman and Benoit Forget, “Selected VERA Core Physics Benchmarks in OpenMC,” Trans. Am. Nucl. Soc., 117, 1520-1523 (2017).
  • Khurrum S. Chaudri and Sikander M. Mirza, “Burnup dependent Monte Carlo neutron physics calculations of IAEA MTR benchmark,” Prog. Nucl. Energy, 81, 43-52 (2015).
  • Daniel J. Kelly, Brian N. Aviles, Paul K. Romano, Bryan R. Herman, Nicholas E. Horelik, and Benoit Forget, “Analysis of select BEAVRS PWR benchmark cycle 1 results using MC21 and OpenMC,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
  • Bryan R. Herman, Benoit Forget, Kord Smith, Paul K. Romano, Thomas M. Sutton, Daniel J. Kelly, III, and Brian N. Aviles, “Analysis of tally correlations in large light water reactors,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
  • Nicholas Horelik, Bryan Herman, Benoit Forget, and Kord Smith, “Benchmark for Evaluation and Validation of Reactor Simulations,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).
  • Jonathan A. Walsh, Benoit Forget, and Kord S. Smith, “Validation of OpenMC Reactor Physics Simulations with the B&W 1810 Series Benchmarks,” Trans. Am. Nucl. Soc., 109, 1301–1304 (2013).

Coupling and Multi-physics

Geometry and Visualization

  • Patrick C. Shriwise, Xiaokang Zhang, and Andrew Davis, “DAG-OpenMC: CAD-Based Geometry in OpenMC”, Trans. Am. Nucl. Soc., 122, 395-398 (2020).
  • Sterling Harper, Paul Romano, Benoit Forget, and Kord Smith, “Efficient dynamic threadsafe neighbor lists for Monte Carlo ray tracing,” Proc. M&C, 918-926, Portland, Oregon, Aug. 25-29 (2019).
  • Jin-Yang Li, Long Gu, Hu-Shan Xu, Nadezha Korepanova, Rui Yu, Yan-Lei Zhu, and Chang-Ping Qin, “CAD modeling study on FLUKA and OpenMC for accelerator driven system simulation”, Ann. Nucl. Energy, 114, 329-341 (2018).
  • Logan Abel, William Boyd, Benoit Forget, and Kord Smith, “Interactive Visualization of Multi-Group Cross Sections on High-Fidelity Spatial Meshes,” Trans. Am. Nucl. Soc., 114, 391-394 (2016).
  • Derek M. Lax, “Memory efficient indexing algorithm for physical properties in OpenMC,” S. M. Thesis, Massachusetts Institute of Technology (2015).
  • Derek Lax, William Boyd, Nicholas Horelik, Benoit Forget, and Kord Smith, “A memory efficient algorithm for classifying unique regions in constructive solid geometries,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).

Miscellaneous

Multigroup Cross Section Generation

Doppler Broadening

Nuclear Data

Parallelism

  • Paul K. Romano and Andrew R. Siegel, “Limits on the efficiency of event-based algorithms for Monte Carlo neutron transport,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • Paul K. Romano, John R. Tramm, and Andrew R. Siegel, “Efficacy of hardware threading for Monte Carlo particle transport calculations on multi- and many-core systems,” PHYSOR 2016, Sun Valley, Idaho, May 1-5, 2016.
  • David Ozog, Allen D. Malony, and Andrew R. Siegel, “A performance analysis of SIMD algorithms for Monte Carlo simulations of nuclear reactor cores,” Proc. IEEE Int. Parallel and Distributed Processing Symposium, Hyderabad, India, May 25–29 (2015).
  • David Ozog, Allen D. Malony, and Andrew Siegel, “Full-core PWR transport simulations on Xeon Phi clusters,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Paul K. Romano, Andrew R. Siegel, and Ronald O. Rahaman, “Influence of the memory subsystem on Monte Carlo code performance,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Hajime Fujita, Nan Dun, Aiman Fang, Zachary A. Rubinstein, Ziming Zheng, Kamil Iskra, Jeff Hammonds, Anshu Dubey, Pavan Balaji, and Andrew A. Chien, “Using Global View Resilience (GVR) to add Resilience to Exascale Applications,” Proc. Supercomputing, New Orleans, Louisiana, Nov. 16–21, 2014.
  • Nicholas Horelik, Benoit Forget, Kord Smith, and Andrew Siegel, “Domain decomposition and terabyte tallies with the OpenMC Monte Carlo neutron transport code,” Proc. PHYSOR, Kyoto Japan, Sep. 28–Oct. 3 (2014).
  • John R. Tramm, Andrew R. Siegel, Tanzima Islam, and Martin Schulz, “XSBench – the development and verification of a performance abstraction for Monte Carlo reactor analysis,” Proc. PHYSOR, Kyoto, Japan, Sep 28–Oct. 3, 2014.
  • Nicholas Horelik, Andrew Siegel, Benoit Forget, and Kord Smith, “Monte Carlo domain decomposition for robust nuclear reactor analysis,” Parallel Comput., 40, 646–660 (2014).
  • Andrew Siegel, Kord Smith, Kyle Felker, Paul Romano, Benoit Forget, and Peter Beckman, “Improved cache performance in Monte Carlo transport calculations using energy banding,” Comput. Phys. Commun., 185 (4), 1195–1199 (2014).
  • Paul K. Romano, Benoit Forget, Kord Smith, and Andrew Siegel, “On the use of tally servers in Monte Carlo simulations of light-water reactors,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
  • Kyle G. Felker, Andrew R. Siegel, Kord S. Smith, Paul K. Romano, and Benoit Forget, “The energy band memory server algorithm for parallel Monte Carlo calculations,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
  • John R. Tramm and Andrew R. Siegel, “Memory Bottlenecks and Memory Contention in Multi-Core Monte Carlo Transport Codes,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
  • Andrew R. Siegel, Kord Smith, Paul K. Romano, Benoit Forget, and Kyle Felker, “Multi-core performance studies of a Monte Carlo neutron transport code,” Int. J. High Perform. Comput. Appl., 28 (1), 87–96 (2014).
  • Paul K. Romano, Andrew R. Siegel, Benoit Forget, and Kord Smith, “Data decomposition of Monte Carlo particle transport simulations via tally servers,” J. Comput. Phys., 252, 20–36 (2013).
  • Andrew R. Siegel, Kord Smith, Paul K. Romano, Benoit Forget, and Kyle Felker, “The effect of load imbalances on the performance of Monte Carlo codes in LWR analysis,” J. Comput. Phys., 235, 901–911 (2013).
  • Paul K. Romano and Benoit Forget, “Reducing Parallel Communication in Monte Carlo Simulations via Batch Statistics,” Trans. Am. Nucl. Soc., 107, 519–522 (2012).
  • Paul K. Romano and Benoit Forget, “Parallel Fission Bank Algorithms in Monte Carlo Criticality Calculations,” Nucl. Sci. Eng., 170, 125–135 (2012).

Depletion

Sensitivity Analysis

License Agreement

Copyright © 2011-2021 Massachusetts Institute of Technology and OpenMC contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.