The OpenMC Monte Carlo Code

OpenMC is a Monte Carlo particle transport simulation code focused on neutron criticality calculations. It is capable of simulating 3D models based on constructive solid geometry with second-order surfaces. OpenMC supports either continuous-energy or multi-group transport. The continuous-energy particle interaction data is based on a native HDF5 format that can be generated from ACE files used by the MCNP and Serpent Monte Carlo codes.

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 send a message to the User’s Group mailing list.

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

OpenMC can then be installed with:

conda install openmc

Installing on Ubuntu through PPA

For users with Ubuntu 15.04 or later, a binary package for OpenMC is available through a Personal Package Archive (PPA) and can be installed through the APT package manager. First, add the following PPA to the repository sources:

sudo apt-add-repository ppa:paulromano/staging

Next, resynchronize the package index files:

sudo apt-get update

Now OpenMC should be recognized within the repository and can be installed:

sudo apt-get install openmc

Binary packages from this PPA may exist for earlier versions of Ubuntu, but they are no longer supported.

Installing from Source on Ubuntu 15.04+

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

sudo apt-get install gfortran
sudo apt-get install cmake
sudo apt-get install libhdf5-dev

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

Note

Before Ubuntu 15.04, the HDF5 package included in the Ubuntu Package archive was not built with support for the Fortran 2003 HDF5 interface, which is needed by OpenMC. If you are using Ubuntu 14.10 or before you will need to build HDF5 from source.

Installing from Source on Linux or Mac OS X

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

git clone https://github.com/mit-crpg/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 ..

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

Example Notebooks

The following series of Jupyter Notebooks provide examples for usage of OpenMC features via the Python API.

Basic Usage

Modeling a Pin-Cell

pincell

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.

In [1]:
%matplotlib inline
import openmc

Defining Materials

Materials in OpenMC are defined as a set of nuclides or elements with specified atom/weight fractions. There are two ways we can go about adding nuclides or elements to materials. The first way involves creating Nuclide or Element objects explicitly.

In [2]:
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
o16 = openmc.Nuclide('O16')
zr = openmc.Element('Zr')
h1 = openmc.Nuclide('H1')

Now that we have all the nuclides/elements that we need, we can start creating materials. In OpenMC, many objects 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. Assigning an ID is required -- we can also give a name as well.

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

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:

In [4]:
mat = openmc.Material()
print(mat)
Material
	ID             =	10000
	Name           =	
	Temperature    =	None
	Density        =	None []
	S(a,b) Tables  
	Nuclides       
	Elements       

We see that an ID of 10000 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 nuclide and second argument is the atom or weight fraction.

In [5]:
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 or openmc.Nuclide
        Nuclide to add
    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.

In [6]:
# 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.

In [7]:
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.

In [8]:
zirconium = openmc.Material(2, "zirconium")
zirconium.add_element(zr, 1.0)
zirconium.set_density('g/cm3', 6.6)

water = openmc.Material(3, "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".

In [9]:
water.add_s_alpha_beta('c_H_in_H2O')

So far you've seen the "hard" way to create a material. The "easy" way is to just pass strings to add_nuclide() and add_element() -- they are implicitly coverted to Nuclide and Element objects. For example, we could have created our UO2 material as follows:

In [10]:
uo2 = openmc.Material(1, "uo2")
uo2.add_nuclide('U235', 0.03)
uo2.add_nuclide('U238', 0.97)
uo2.add_nuclide('O16', 2.0)
uo2.set_density('g/cm3', 10.0)

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.

In [11]:
mats = 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.

In [12]:
mats = openmc.Materials()
mats.append(uo2)
mats += [zirconium, water]
isinstance(mats, list)
Out[12]:
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.

In [13]:
mats.export_to_xml()
!cat materials.xml
<?xml version='1.0' encoding='utf-8'?>
<materials>
    <material 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="2" 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="3" 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.

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

mats.export_to_xml()
!cat materials.xml
<?xml version='1.0' encoding='utf-8'?>
<materials>
    <material 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="2" 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="3" 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:

In [15]:
!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="Ac225" path="Ac225.h5" type="neutron" />
    <library materials="Ac226" path="Ac226.h5" type="neutron" />
    <library materials="Ac227" path="Ac227.h5" type="neutron" />
    <library materials="Ag107" path="Ag107.h5" type="neutron" />
    <library materials="Ag109" path="Ag109.h5" type="neutron" />
    <library materials="Ag110_m1" path="Ag110_m1.h5" type="neutron" />
    <library materials="Ag111" path="Ag111.h5" type="neutron" />
    <library materials="Al27" path="Al27.h5" type="neutron" />
    ...
    <library materials="c_O_in_BeO" path="c_O_in_BeO.h5" type="thermal" />
    <library materials="c_ortho_D" path="c_ortho_D.h5" type="thermal" />
    <library materials="c_ortho_H" path="c_ortho_H.h5" type="thermal" />
    <library materials="c_O_in_UO2" path="c_O_in_UO2.h5" type="thermal" />
    <library materials="c_para_D" path="c_para_D.h5" type="thermal" />
    <library materials="c_para_H" path="c_para_H.h5" type="thermal" />
    <library materials="c_solid_CH4" path="c_solid_CH4.h5" type="thermal" />
    <library materials="c_U_in_UO2" path="c_U_in_UO2.h5" type="thermal" />
    <library materials="c_Zr_in_ZrH" path="c_Zr_in_ZrH.h5" type="thermal" />
</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:

In [16]:
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)

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 perpendicular 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 (torii 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 of 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.

In [17]:
sph = 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).

In [18]:
inside_sphere = -sph
outside_sphere = +sph

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

In [19]:
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$.

In [20]:
z_plane = openmc.ZPlane(z0=0)
northern_hemisphere = -sph & +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:

In [21]:
northern_hemisphere.bounding_box
Out[21]:
(array([-1., -1.,  0.]), array([ 1.,  1.,  1.]))

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

In [22]:
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.

In [23]:
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.

In [24]:
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:

In [25]:
universe.plot(width=(2.0, 2.0))

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

In [26]:
universe.plot(width=(2.0, 2.0), basis='xz')

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

In [27]:
universe.plot(width=(2.0, 2.0), basis='xz',
              colors={cell: 'fuchsia'})

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

In [28]:
fuel_or = openmc.ZCylinder(R=0.39)
clad_ir = openmc.ZCylinder(R=0.40)
clad_or = 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:

In [29]:
fuel_region = -fuel_or
gap_region = +fuel_or & -clad_ir
clad_region = +clad_ir & -clad_or

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

In [30]:
fuel = openmc.Cell(1, 'fuel')
fuel.fill = uo2
fuel.region = fuel_region

gap = openmc.Cell(2, 'air gap')
gap.region = gap_region

clad = openmc.Cell(3, '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.

In [31]:
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.

In [32]:
water_region = +left & -right & +bottom & -top & +clad_or

moderator = openmc.Cell(4, '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.

In [33]:
box = openmc.get_rectangular_prism(width=pitch, height=pitch,
                                   boundary_type='reflective')
type(box)
Out[33]:
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.

In [34]:
water_region = box & +clad_or

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.

In [35]:
root = openmc.Universe(cells=(fuel, gap, clad, moderator))

geom = openmc.Geometry()
geom.root_universe = root

# or...
geom = openmc.Geometry(root)
geom.export_to_xml()
!cat geometry.xml
<?xml version='1.0' encoding='utf-8'?>
<geometry>
    <cell id="1" material="1" name="fuel" region="-10002" universe="10002" />
    <cell id="2" material="void" name="air gap" region="10002 -10003" universe="10002" />
    <cell id="3" material="2" name="clad" region="10003 -10004" universe="10002" />
    <cell id="4" material="3" name="moderator" region="10005 -10006 10007 -10008 10004" universe="10002" />
    <surface coeffs="0.0 0.0 0.39" id="10002" type="z-cylinder" />
    <surface coeffs="0.0 0.0 0.4" id="10003" type="z-cylinder" />
    <surface coeffs="0.0 0.0 0.46" id="10004" type="z-cylinder" />
    <surface boundary="reflective" coeffs="-0.63" id="10005" type="x-plane" />
    <surface boundary="reflective" coeffs="0.63" id="10006" type="x-plane" />
    <surface boundary="reflective" coeffs="-0.63" id="10007" type="y-plane" />
    <surface boundary="reflective" coeffs="0.63" id="10008" 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.

In [36]:
point = openmc.stats.Point((0, 0, 0))
src = 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.

In [37]:
settings = openmc.Settings()
settings.source = src
settings.batches = 100
settings.inactive = 10
settings.particles = 1000
In [38]:
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,$\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.

In [39]:
cell_filter = openmc.CellFilter(fuel)

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

The what is the total, fission, absorption, and (n,$\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.

In [40]:
t.nuclides = ['U235']
t.scores = ['total', 'fission', 'absorption', '(n,gamma)']

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

In [41]:
tallies = openmc.Tallies([t])
tallies.export_to_xml()
!cat tallies.xml
<?xml version='1.0' encoding='utf-8'?>
<tallies>
    <tally id="1">
        <filter bins="1" type="cell" />
        <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.

In [42]:
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 | bc4683be1c853fe6d0e31bccad416ef219e3efaf
         Date/Time | 2017-04-13 17:17:32
     MPI Processes | 1
    OpenMP Threads | 4

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from /home/smharper/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from /home/smharper/openmc/data/nndc_hdf5/U238.h5
 Reading O16 from /home/smharper/openmc/data/nndc_hdf5/O16.h5
 Reading Zr90 from /home/smharper/openmc/data/nndc_hdf5/Zr90.h5
 Reading Zr91 from /home/smharper/openmc/data/nndc_hdf5/Zr91.h5
 Reading Zr92 from /home/smharper/openmc/data/nndc_hdf5/Zr92.h5
 Reading Zr94 from /home/smharper/openmc/data/nndc_hdf5/Zr94.h5
 Reading Zr96 from /home/smharper/openmc/data/nndc_hdf5/Zr96.h5
 Reading H1 from /home/smharper/openmc/data/nndc_hdf5/H1.h5
 Reading O17 from /home/smharper/openmc/data/nndc_hdf5/O17.h5
 Reading c_H_in_H2O from /home/smharper/openmc/data/nndc_hdf5/c_H_in_H2O.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.32572                       
        2/1    1.46138                       
        3/1    1.46068                       
        4/1    1.39592                       
        5/1    1.37519                       
        6/1    1.38777                       
        7/1    1.50242                       
        8/1    1.42042                       
        9/1    1.47458                       
       10/1    1.49148                       
       11/1    1.39339                       
       12/1    1.40637    1.39988 +/- 0.00649
       13/1    1.42972    1.40983 +/- 0.01063
       14/1    1.46319    1.42317 +/- 0.01531
       15/1    1.41538    1.42161 +/- 0.01196
       16/1    1.38163    1.41494 +/- 0.01182
       17/1    1.41257    1.41461 +/- 0.01000
       18/1    1.43455    1.41710 +/- 0.00901
       19/1    1.33136    1.40757 +/- 0.01241
       20/1    1.41560    1.40837 +/- 0.01113
       21/1    1.38911    1.40662 +/- 0.01021
       22/1    1.28621    1.39659 +/- 0.01370
       23/1    1.45693    1.40123 +/- 0.01343
       24/1    1.46839    1.40603 +/- 0.01333
       25/1    1.46738    1.41012 +/- 0.01306
       26/1    1.43977    1.41197 +/- 0.01236
       27/1    1.44066    1.41366 +/- 0.01173
       28/1    1.39358    1.41254 +/- 0.01112
       29/1    1.39142    1.41143 +/- 0.01057
       30/1    1.38525    1.41012 +/- 0.01012
       31/1    1.38025    1.40870 +/- 0.00973
       32/1    1.45348    1.41074 +/- 0.00949
       33/1    1.35893    1.40848 +/- 0.00935
       34/1    1.32332    1.40493 +/- 0.00963
       35/1    1.46285    1.40725 +/- 0.00952
       36/1    1.33760    1.40457 +/- 0.00953
       37/1    1.41117    1.40482 +/- 0.00917
       38/1    1.45574    1.40664 +/- 0.00903
       39/1    1.43472    1.40760 +/- 0.00876
       40/1    1.30110    1.40405 +/- 0.00918
       41/1    1.41765    1.40449 +/- 0.00889
       42/1    1.45300    1.40601 +/- 0.00874
       43/1    1.40491    1.40597 +/- 0.00847
       44/1    1.42053    1.40640 +/- 0.00823
       45/1    1.38805    1.40588 +/- 0.00801
       46/1    1.34293    1.40413 +/- 0.00798
       47/1    1.35441    1.40279 +/- 0.00787
       48/1    1.29370    1.39991 +/- 0.00818
       49/1    1.48467    1.40209 +/- 0.00826
       50/1    1.41759    1.40248 +/- 0.00806
       51/1    1.37151    1.40172 +/- 0.00790
       52/1    1.42403    1.40225 +/- 0.00773
       53/1    1.38826    1.40193 +/- 0.00755
       54/1    1.48944    1.40392 +/- 0.00764
       55/1    1.41452    1.40415 +/- 0.00747
       56/1    1.47337    1.40566 +/- 0.00746
       57/1    1.35700    1.40462 +/- 0.00738
       58/1    1.40305    1.40459 +/- 0.00722
       59/1    1.41608    1.40482 +/- 0.00708
       60/1    1.47254    1.40618 +/- 0.00706
       61/1    1.36847    1.40544 +/- 0.00696
       62/1    1.34103    1.40420 +/- 0.00694
       63/1    1.39510    1.40403 +/- 0.00681
       64/1    1.40228    1.40399 +/- 0.00668
       65/1    1.29401    1.40200 +/- 0.00686
       66/1    1.42693    1.40244 +/- 0.00675
       67/1    1.36447    1.40177 +/- 0.00666
       68/1    1.37498    1.40131 +/- 0.00656
       69/1    1.36958    1.40077 +/- 0.00647
       70/1    1.38585    1.40053 +/- 0.00637
       71/1    1.42133    1.40087 +/- 0.00627
       72/1    1.44900    1.40164 +/- 0.00622
       73/1    1.37696    1.40125 +/- 0.00613
       74/1    1.48851    1.40261 +/- 0.00619
       75/1    1.38933    1.40241 +/- 0.00610
       76/1    1.41780    1.40264 +/- 0.00601
       77/1    1.41054    1.40276 +/- 0.00592
       78/1    1.38194    1.40246 +/- 0.00584
       79/1    1.38446    1.40219 +/- 0.00576
       80/1    1.37504    1.40181 +/- 0.00569
       81/1    1.40550    1.40186 +/- 0.00561
       82/1    1.49785    1.40319 +/- 0.00569
       83/1    1.35613    1.40255 +/- 0.00565
       84/1    1.41786    1.40275 +/- 0.00557
       85/1    1.38444    1.40251 +/- 0.00550
       86/1    1.40459    1.40254 +/- 0.00543
       87/1    1.39923    1.40249 +/- 0.00536
       88/1    1.44540    1.40304 +/- 0.00532
       89/1    1.45962    1.40376 +/- 0.00530
       90/1    1.37057    1.40335 +/- 0.00525
       91/1    1.38115    1.40307 +/- 0.00519
       92/1    1.35758    1.40252 +/- 0.00516
       93/1    1.34508    1.40182 +/- 0.00514
       94/1    1.31471    1.40079 +/- 0.00519
       95/1    1.41434    1.40095 +/- 0.00513
       96/1    1.33895    1.40023 +/- 0.00512
       97/1    1.44716    1.40077 +/- 0.00509
       98/1    1.38455    1.40058 +/- 0.00503
       99/1    1.52127    1.40194 +/- 0.00516
      100/1    1.35488    1.40141 +/- 0.00513
 Creating state point statepoint.100.h5...

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

 Total time for initialization     =  5.3000E-01 seconds
   Reading cross sections          =  4.7425E-01 seconds
 Total time in simulation          =  5.9503E+00 seconds
   Time in transport only          =  5.6693E+00 seconds
   Time in inactive batches        =  5.9508E-01 seconds
   Time in active batches          =  5.3552E+00 seconds
   Time synchronizing fission bank =  4.5385E-03 seconds
     Sampling source sites         =  2.4558E-03 seconds
     SEND/RECV source sites        =  1.0922E-03 seconds
   Time accumulating tallies       =  4.2963E-04 seconds
 Total time for finalization       =  4.4542E-04 seconds
 Total time elapsed                =  6.4846E+00 seconds
 Calculation Rate (inactive)       =  16804.5 neutrons/second
 Calculation Rate (active)         =  16806.1 neutrons/second

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

 k-effective (Collision)     =  1.39737 +/-  0.00470
 k-effective (Track-length)  =  1.40141 +/-  0.00513
 k-effective (Absorption)    =  1.39596 +/-  0.00308
 Combined k-effective        =  1.39719 +/-  0.00286
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[42]:
0

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.

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

 Cell 1
   U235
     Total Reaction Rate                  0.731003       +/- 2.53759E-03
     Fission Rate                         0.547587       +/- 2.10114E-03
     Absorption Rate                      0.657406       +/- 2.45390E-03
     (n,gamma)                            0.109821       +/- 3.68054E-04

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

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

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

In [45]:
plots = openmc.Plots([p])
plots.export_to_xml()
!cat plots.xml
<?xml version='1.0' encoding='utf-8'?>
<plots>
    <plot basis="xy" color_by="material" filename="pinplot" id="10000" 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="3" 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.

In [46]:
openmc.plot_geometry()
                               %%%%%%%%%%%%%%%
                          %%%%%%%%%%%%%%%%%%%%%%%%
                       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
                                    %%%%%%%%%%%%%%%%%%%%%%%%
                ###############      %%%%%%%%%%%%%%%%%%%%%%%%
               ##################     %%%%%%%%%%%%%%%%%%%%%%%
               ###################     %%%%%%%%%%%%%%%%%%%%%%%
               ####################     %%%%%%%%%%%%%%%%%%%%%%
               #####################     %%%%%%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%%
                #######################     %%%%%%%%%%%%%%%%%
                ######################     %%%%%%%%%%%%%%%%%
                 ####################     %%%%%%%%%%%%%%%%%
                   #################     %%%%%%%%%%%%%%%%%
                    ###############     %%%%%%%%%%%%%%%%
                      ############     %%%%%%%%%%%%%%%
                         ########     %%%%%%%%%%%%%%
                                     %%%%%%%%%%%

                   | 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 | bc4683be1c853fe6d0e31bccad416ef219e3efaf
         Date/Time | 2017-04-13 17:18:01
     MPI Processes | 1
    OpenMP Threads | 4

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading tallies XML file...
 Reading plot XML file...
 Building neighboring cells lists for each surface...

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

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

 Processing plot 10000: pinplot.ppm ...
Out[46]:
0

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.

In [47]:
!convert pinplot.ppm pinplot.png

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

In [48]:
from IPython.display import Image
Image("pinplot.png")
Out[48]:

That was a little bit cumbersome. Thankfully, OpenMC provides us with a function that does all that "boilerplate" work.

In [49]:
openmc.plot_inline(p)

Post Processing

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.

In [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. Before defining a material, we must create nuclides that are used in the material.

In [2]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pin.

In [3]:
# 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.

In [4]:
# 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.

In [5]:
# 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=-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.

In [6]:
# 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.

In [7]:
# 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.

In [8]:
# Create Geometry and set root Universe
geometry = openmc.Geometry()
geometry.root_universe = root_universe
In [9]:
# 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.

In [10]:
# OpenMC simulation parameters
batches = 100
inactive = 10
particles = 5000

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles

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

In [11]:
# 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 = 'mat'

# Instantiate a Plots collection and export to "plots.xml"
plot_file = openmc.Plots([plot])
plot_file.export_to_xml()

With the plots.xml file, we can now generate and view the plot. OpenMC outputs plots in .ppm format, which can be converted into a compressed format like .png with the convert utility.

In [12]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[12]:
0
In [13]:
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png

# Display the materials plot inline
Image(filename='materials-xy.png')
Out[13]:

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.

In [14]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
In [15]:
# Create mesh which will be used for tally
mesh = openmc.Mesh()
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_file.append(tally)
In [16]:
# Export to "tallies.xml"
tallies_file.export_to_xml()

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

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

                   | The OpenMC Monte Carlo Code
         Copyright | 2011-2016 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.8.0
          Git SHA1 | da5563eddb5f2c2d6b2c9839d518de40962b78f2
         Date/Time | 2016-10-31 12:47:14
    OpenMP Threads | 4

 ===========================================================================
 ========================>     INITIALIZATION     <=========================
 ===========================================================================

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from /home/romano/openmc/scripts/nndc_hdf5/U235.h5
 Reading U238 from /home/romano/openmc/scripts/nndc_hdf5/U238.h5
 Reading O16 from /home/romano/openmc/scripts/nndc_hdf5/O16.h5
 Reading H1 from /home/romano/openmc/scripts/nndc_hdf5/H1.h5
 Reading B10 from /home/romano/openmc/scripts/nndc_hdf5/B10.h5
 Reading Zr90 from /home/romano/openmc/scripts/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.04359                       
        2/1    1.04244                       
        3/1    1.03020                       
        4/1    1.03630                       
        5/1    1.06478                       
        6/1    1.05450                       
        7/1    1.02369                       
        8/1    1.03454                       
        9/1    1.05309                       
       10/1    1.01741                       
       11/1    1.04344                       
       12/1    1.01457    1.02901 +/- 0.01444
       13/1    1.01643    1.02481 +/- 0.00933
       14/1    1.04657    1.03025 +/- 0.00855
       15/1    1.06420    1.03704 +/- 0.00949
       16/1    1.06048    1.04095 +/- 0.00867
       17/1    1.01627    1.03742 +/- 0.00813
       18/1    1.03667    1.03733 +/- 0.00705
       19/1    1.03977    1.03760 +/- 0.00622
       20/1    1.03996    1.03784 +/- 0.00557
       21/1    1.05663    1.03954 +/- 0.00532
       22/1    1.03944    1.03954 +/- 0.00485
       23/1    1.07702    1.04242 +/- 0.00532
       24/1    1.02378    1.04109 +/- 0.00510
       25/1    1.02817    1.04023 +/- 0.00482
       26/1    1.08000    1.04271 +/- 0.00515
       27/1    1.02733    1.04181 +/- 0.00492
       28/1    1.02993    1.04115 +/- 0.00469
       29/1    1.01755    1.03991 +/- 0.00461
       30/1    1.05836    1.04083 +/- 0.00447
       31/1    1.05936    1.04171 +/- 0.00434
       32/1    1.03683    1.04149 +/- 0.00414
       33/1    1.05112    1.04191 +/- 0.00398
       34/1    1.02927    1.04138 +/- 0.00385
       35/1    1.05326    1.04186 +/- 0.00372
       36/1    1.06014    1.04256 +/- 0.00364
       37/1    1.02320    1.04184 +/- 0.00358
       38/1    1.04297    1.04188 +/- 0.00345
       39/1    1.04544    1.04201 +/- 0.00333
       40/1    1.05178    1.04233 +/- 0.00323
       41/1    1.01744    1.04153 +/- 0.00323
       42/1    1.02376    1.04097 +/- 0.00317
       43/1    1.02344    1.04044 +/- 0.00312
       44/1    1.05813    1.04096 +/- 0.00307
       45/1    1.02370    1.04047 +/- 0.00303
       46/1    1.03536    1.04033 +/- 0.00294
       47/1    1.04344    1.04041 +/- 0.00286
       48/1    1.03879    1.04037 +/- 0.00279
       49/1    1.07122    1.04116 +/- 0.00283
       50/1    1.03861    1.04110 +/- 0.00276
       51/1    1.00812    1.04029 +/- 0.00281
       52/1    1.04620    1.04043 +/- 0.00274
       53/1    1.07050    1.04113 +/- 0.00277
       54/1    1.04038    1.04111 +/- 0.00270
       55/1    1.03770    1.04104 +/- 0.00264
       56/1    1.05627    1.04137 +/- 0.00261
       57/1    1.04191    1.04138 +/- 0.00255
       58/1    1.03633    1.04128 +/- 0.00250
       59/1    1.02002    1.04084 +/- 0.00249
       60/1    1.04293    1.04088 +/- 0.00244
       61/1    1.00919    1.04026 +/- 0.00247
       62/1    1.09274    1.04127 +/- 0.00262
       63/1    1.05344    1.04150 +/- 0.00258
       64/1    1.07892    1.04219 +/- 0.00263
       65/1    1.09293    1.04312 +/- 0.00274
       66/1    1.04485    1.04315 +/- 0.00269
       67/1    1.04794    1.04323 +/- 0.00264
       68/1    1.04183    1.04321 +/- 0.00260
       69/1    1.03052    1.04299 +/- 0.00256
       70/1    1.03630    1.04288 +/- 0.00252
       71/1    1.04611    1.04293 +/- 0.00248
       72/1    1.00214    1.04228 +/- 0.00253
       73/1    1.02488    1.04200 +/- 0.00250
       74/1    1.03607    1.04191 +/- 0.00246
       75/1    1.05488    1.04211 +/- 0.00243
       76/1    1.02252    1.04181 +/- 0.00242
       77/1    1.03196    1.04166 +/- 0.00238
       78/1    1.05769    1.04190 +/- 0.00236
       79/1    1.03101    1.04174 +/- 0.00233
       80/1    1.04693    1.04182 +/- 0.00230
       81/1    1.06110    1.04209 +/- 0.00228
       82/1    1.06273    1.04237 +/- 0.00227
       83/1    1.07699    1.04285 +/- 0.00229
       84/1    1.05740    1.04304 +/- 0.00226
       85/1    1.04040    1.04301 +/- 0.00223
       86/1    1.02147    1.04273 +/- 0.00222
       87/1    1.00842    1.04228 +/- 0.00224
       88/1    1.06173    1.04253 +/- 0.00222
       89/1    1.08649    1.04309 +/- 0.00227
       90/1    1.05826    1.04328 +/- 0.00224
       91/1    1.07673    1.04369 +/- 0.00225
       92/1    1.02425    1.04345 +/- 0.00224
       93/1    1.05359    1.04357 +/- 0.00222
       94/1    1.06085    1.04378 +/- 0.00220
       95/1    1.05319    1.04389 +/- 0.00218
       96/1    1.02321    1.04365 +/- 0.00216
       97/1    1.04124    1.04362 +/- 0.00214
       98/1    1.06307    1.04384 +/- 0.00213
       99/1    1.04616    1.04387 +/- 0.00210
      100/1    1.03278    1.04375 +/- 0.00208
 Creating state point statepoint.100.h5...

 ===========================================================================
 ======================>     SIMULATION FINISHED     <======================
 ===========================================================================


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

 Total time for initialization     =  5.3314E-01 seconds
   Reading cross sections          =  3.6117E-01 seconds
 Total time in simulation          =  3.5193E+02 seconds
   Time in transport only          =  3.5172E+02 seconds
   Time in inactive batches        =  4.7990E+00 seconds
   Time in active batches          =  3.4714E+02 seconds
   Time synchronizing fission bank =  2.3264E-02 seconds
     Sampling source sites         =  1.6661E-02 seconds
     SEND/RECV source sites        =  6.3975E-03 seconds
   Time accumulating tallies       =  2.3345E-02 seconds
 Total time for finalization       =  1.7400E-01 seconds
 Total time elapsed                =  3.5269E+02 seconds
 Calculation Rate (inactive)       =  10418.8 neutrons/second
 Calculation Rate (active)         =  1296.32 neutrons/second

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

 k-effective (Collision)     =  1.04220 +/-  0.00169
 k-effective (Track-length)  =  1.04375 +/-  0.00208
 k-effective (Absorption)    =  1.04213 +/-  0.00151
 Combined k-effective        =  1.04229 +/-  0.00127
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[17]:
0

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.

In [18]:
# 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.

In [19]:
tally = sp.get_tally(scores=['flux'])
print(tally)
Tally
	ID             =	10000
	Name           =	flux
	Filters        =	
                		MeshFilter	[10000]
	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:

In [20]:
tally.sum
Out[20]:
array([[[ 0.41167874,  0.        ]],

       [[ 0.40853332,  0.        ]],

       [[ 0.41140779,  0.        ]],

       ..., 
       [[ 0.40957563,  0.        ]],

       [[ 0.40983338,  0.        ]],

       [[ 0.40877195,  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.

In [21]:
print(tally.mean.shape)
(tally.mean, tally.std_dev)
(10000, 1, 2)
Out[21]:
(array([[[ 0.00457421,  0.        ]],
 
        [[ 0.00453926,  0.        ]],
 
        [[ 0.0045712 ,  0.        ]],
 
        ..., 
        [[ 0.00455084,  0.        ]],
 
        [[ 0.0045537 ,  0.        ]],
 
        [[ 0.00454191,  0.        ]]]),
 array([[[  1.84557765e-05,   0.00000000e+00]],
 
        [[  1.71073587e-05,   0.00000000e+00]],
 
        [[  1.76546641e-05,   0.00000000e+00]],
 
        ..., 
        [[  1.82859458e-05,   0.00000000e+00]],
 
        [[  1.80961726e-05,   0.00000000e+00]],
 
        [[  2.01200499e-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.

In [22]:
flux = tally.get_slice(scores=['flux'])
fission = tally.get_slice(scores=['fission'])
print(flux)
Tally
	ID             =	10001
	Name           =	flux
	Filters        =	
                		MeshFilter	[10000]
	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.

In [23]:
flux.std_dev.shape = (100, 100)
flux.mean.shape = (100, 100)
fission.std_dev.shape = (100, 100)
fission.mean.shape = (100, 100)
In [24]:
fig = plt.subplot(121)
fig.imshow(flux.mean)
fig2 = plt.subplot(122)
fig2.imshow(fission.mean)
Out[24]:
<matplotlib.image.AxesImage at 0x7f28799b7ef0>

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.

In [25]:
# 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)

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.

In [26]:
sp.source
Out[26]:
array([ (1.0, [-0.04091602298055571, -0.28281765875028614, -0.23969748862718712], [-0.5162507278389928, 0.4979959592686946, 0.6967676876533259], 1167993.2181739977, 0),
       (1.0, [-0.04091602298055571, -0.28281765875028614, -0.23969748862718712], [0.9314318185369252, -0.3421578395220699, 0.12394668317702494], 2076480.0407698506, 0),
       (1.0, [0.07588719867579076, 0.3442630703689722, 0.24037509956303865], [-0.4263739205747279, 0.8745850440707152, -0.23088152923428204], 3435875.6567740417, 0),
       ...,
       (1.0, [-0.2976222011116176, 0.06744830909702038, 0.017418564319938733], [0.5146424164525509, 0.8138247541106745, -0.26987488357492534], 3072272.553085709, 0),
       (1.0, [-0.21734045199173016, -0.10720270103504186, -0.532882255592573], [-0.8802303390142701, -0.22473292046402116, 0.4179589270951574], 5120128.793040418, 0),
       (1.0, [-0.21734045199173016, -0.10720270103504186, -0.532882255592573], [-0.16842874876345693, 0.9852495482818028, 0.030250358683490713], 575131.1093690266, 0)], 
      dtype=[('wgt', '<f8'), ('xyz', '<f8', (3,)), ('uvw', '<f8', (3,)), ('E', '<f8'), ('delayed_group', '<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:

In [27]:
sp.source['E']
Out[27]:
array([ 1167993.218174  ,  2076480.04076985,  3435875.65677404, ...,
        3072272.55308571,  5120128.79304042,   575131.10936903])

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.

In [28]:
# Create log-spaced energy bins from 1 keV to 100 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), linestyle='steps')
plt.xlabel('Energy (eV)')
plt.ylabel('Probability/eV')
1.0
Out[28]:
<matplotlib.text.Text at 0x7f2878085908>

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.

In [29]:
plt.quiver(sp.source['xyz'][:,0], sp.source['xyz'][:,1],
           sp.source['uvw'][:,0], sp.source['uvw'][:,1],
           np.log(sp.source['E']), cmap='jet', scale=20.0)
plt.colorbar()
plt.xlim((-0.5,0.5))
plt.ylim((-0.5,0.5))
Out[29]:
(-0.5, 0.5)

Pandas Dataframes

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.

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

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

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

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

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

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

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

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

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

In [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 = 'mat'

# Instantiate a Plots collection and export to "plots.xml"
plot_file = openmc.Plots([plot])
plot_file.export_to_xml()

With the plots.xml file, we can now generate and view the plot. OpenMC outputs plots in .ppm format, which can be converted into a compressed format like .png with the convert utility.

In [11]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[11]:
0
In [12]:
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png

# Display the materials plot inline
Image(filename='materials-xy.png')
Out[12]:

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.

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

Instantiate a fission rate mesh Tally

In [14]:
# Instantiate a tally Mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
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

In [15]:
# 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-y2']
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.

In [16]:
# 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)
In [17]:
# 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.

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

# 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 | bc4683be1c853fe6d0e31bccad416ef219e3efaf
         Date/Time | 2017-04-13 17:23:58
     MPI Processes | 1
    OpenMP Threads | 1

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from /home/smharper/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from /home/smharper/openmc/data/nndc_hdf5/U238.h5
 Reading O16 from /home/smharper/openmc/data/nndc_hdf5/O16.h5
 Reading H1 from /home/smharper/openmc/data/nndc_hdf5/H1.h5
 Reading B10 from /home/smharper/openmc/data/nndc_hdf5/B10.h5
 Reading Zr90 from /home/smharper/openmc/data/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    0.55921                       
        2/1    0.63816                       
        3/1    0.68834                       
        4/1    0.71192                       
        5/1    0.67935                       
        6/1    0.68254                       
        7/1    0.65804    0.67029 +/- 0.01225
        8/1    0.66225    0.66761 +/- 0.00756
        9/1    0.66336    0.66655 +/- 0.00545
       10/1    0.68037    0.66931 +/- 0.00505
       11/1    0.71728    0.67731 +/- 0.00899
       12/1    0.66098    0.67498 +/- 0.00795
       13/1    0.69969    0.67806 +/- 0.00755
       14/1    0.70998    0.68161 +/- 0.00754
       15/1    0.70092    0.68354 +/- 0.00702
       16/1    0.71586    0.68648 +/- 0.00699
       17/1    0.65949    0.68423 +/- 0.00677
       18/1    0.67696    0.68367 +/- 0.00625
       19/1    0.65444    0.68158 +/- 0.00615
       20/1    0.69766    0.68266 +/- 0.00583
 Triggers unsatisfied, max unc./thresh. is 1.17617 for absorption in tally 10002
 The estimated number of batches is 26
 Creating state point statepoint.020.h5...
       21/1    0.64126    0.68007 +/- 0.00603
       22/1    0.69287    0.68082 +/- 0.00572
       23/1    0.70254    0.68203 +/- 0.00552
       24/1    0.68198    0.68203 +/- 0.00523
       25/1    0.67214    0.68153 +/- 0.00498
       26/1    0.68171    0.68154 +/- 0.00474
 Triggers satisfied for batch 26
 Creating state point statepoint.026.h5...

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

 Total time for initialization     =  2.4022E-01 seconds
   Reading cross sections          =  1.9056E-01 seconds
 Total time in simulation          =  7.5438E+00 seconds
   Time in transport only          =  7.5290E+00 seconds
   Time in inactive batches        =  1.0482E+00 seconds
   Time in active batches          =  6.4956E+00 seconds
   Time synchronizing fission bank =  2.3117E-03 seconds
     Sampling source sites         =  1.3344E-03 seconds
     SEND/RECV source sites        =  6.8338E-04 seconds
   Time accumulating tallies       =  3.9461E-04 seconds
 Total time for finalization       =  1.3648E-05 seconds
 Total time elapsed                =  7.7964E+00 seconds
 Calculation Rate (inactive)       =  11925.2 neutrons/second
 Calculation Rate (active)         =  5773.18 neutrons/second

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

 k-effective (Collision)     =  0.67976 +/-  0.00436
 k-effective (Track-length)  =  0.68154 +/-  0.00474
 k-effective (Absorption)    =  0.68320 +/-  0.00518
 Combined k-effective        =  0.68122 +/-  0.00432
 Leakage Fraction            =  0.34011 +/-  0.00283

Out[18]:
0

Tally Data Processing

In [19]:
# 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

In [20]:
# 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             =	10000
	Name           =	mesh tally
	Filters        =	MeshFilter, EnergyFilter
	Nuclides       =	total 
	Scores         =	['fission', 'nu-fission']
	Estimator      =	tracklength

Use the new Tally data retrieval API with pure NumPy

In [21]:
# 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.22614381]]

 [[ 0.3789572 ]]

 [[ 0.05763899]]

 [[ 0.14265074]]]
In [22]:
# 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)
Out[22]:
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.28e-04 5.15e-05
1 1 1 1 0.00e+00 6.25e-01 nu-fission 5.55e-04 1.26e-04
2 1 1 1 6.25e-01 2.00e+07 fission 8.65e-05 9.06e-06
3 1 1 1 6.25e-01 2.00e+07 nu-fission 2.28e-04 2.19e-05
4 1 2 1 0.00e+00 6.25e-01 fission 1.83e-04 3.54e-05
5 1 2 1 0.00e+00 6.25e-01 nu-fission 4.47e-04 8.62e-05
6 1 2 1 6.25e-01 2.00e+07 fission 6.91e-05 6.94e-06
7 1 2 1 6.25e-01 2.00e+07 nu-fission 1.81e-04 1.77e-05
8 1 3 1 0.00e+00 6.25e-01 fission 1.98e-04 2.55e-05
9 1 3 1 0.00e+00 6.25e-01 nu-fission 4.82e-04 6.20e-05
10 1 3 1 6.25e-01 2.00e+07 fission 7.76e-05 9.73e-06
11 1 3 1 6.25e-01 2.00e+07 nu-fission 2.05e-04 2.48e-05
12 1 4 1 0.00e+00 6.25e-01 fission 2.32e-04 3.38e-05
13 1 4 1 0.00e+00 6.25e-01 nu-fission 5.66e-04 8.23e-05
14 1 4 1 6.25e-01 2.00e+07 fission 6.54e-05 4.61e-06
15 1 4 1 6.25e-01 2.00e+07 nu-fission 1.73e-04 1.23e-05
16 1 5 1 0.00e+00 6.25e-01 fission 2.18e-04 3.68e-05
17 1 5 1 0.00e+00 6.25e-01 nu-fission 5.32e-04 8.97e-05
18 1 5 1 6.25e-01 2.00e+07 fission 5.92e-05 6.63e-06
19 1 5 1 6.25e-01 2.00e+07 nu-fission 1.56e-04 1.82e-05
In [23]:
# Create a boxplot to view the distribution of
# fission and nu-fission rates in the pins
bp = df.boxplot(column='mean', by='score')
In [24]:
# 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()
Out[24]:
<matplotlib.colorbar.Colorbar at 0x7f7d7dd40a20>

Analyze the cell+nuclides scatter-y2 rate tally

In [25]:
# 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             =	10001
	Name           =	cell tally
	Filters        =	CellFilter
	Nuclides       =	U235 U238 
	Scores         =	['scatter-Y0,0', 'scatter-Y1,-1', 'scatter-Y1,0', 'scatter-Y1,1', 'scatter-Y2,-2', 'scatter-Y2,-1', 'scatter-Y2,0', 'scatter-Y2,1', 'scatter-Y2,2']
	Estimator      =	analog

In [26]:
# Get a pandas dataframe for the cell tally data
df = tally.get_pandas_dataframe()

# Print the first twenty rows in the dataframe
df.head(100)
Out[26]:
cell nuclide score mean std. dev.
0 10000 U235 scatter-Y0,0 3.87e-02 8.61e-04
1 10000 U235 scatter-Y1,-1 6.88e-04 3.63e-04
2 10000 U235 scatter-Y1,0 -3.38e-04 4.37e-04
3 10000 U235 scatter-Y1,1 -4.27e-04 3.74e-04
4 10000 U235 scatter-Y2,-2 1.88e-04 1.86e-04
5 10000 U235 scatter-Y2,-1 8.96e-05 2.02e-04
6 10000 U235 scatter-Y2,0 4.03e-04 2.04e-04
7 10000 U235 scatter-Y2,1 1.48e-04 2.24e-04
8 10000 U235 scatter-Y2,2 1.11e-04 2.01e-04
9 10000 U238 scatter-Y0,0 2.34e+00 1.08e-02
10 10000 U238 scatter-Y1,-1 2.93e-02 2.81e-03
11 10000 U238 scatter-Y1,0 4.25e-03 1.83e-03
12 10000 U238 scatter-Y1,1 -2.60e-02 3.28e-03
13 10000 U238 scatter-Y2,-2 -1.10e-03 1.58e-03
14 10000 U238 scatter-Y2,-1 -4.81e-04 1.67e-03
15 10000 U238 scatter-Y2,0 -7.95e-04 1.53e-03
16 10000 U238 scatter-Y2,1 1.06e-03 1.96e-03
17 10000 U238 scatter-Y2,2 -7.65e-04 1.67e-03

Use the new Tally data retrieval API with pure NumPy

In [27]:
# Get the standard deviations for two of the spherical harmonic
# scattering reaction rates 
data = tally.get_values(scores=['scatter-Y2,2', 'scatter-Y0,0'], 
                        nuclides=['U238', 'U235'], value='std_dev')
print(data)
[[[ 0.0016699   0.01076055]
  [ 0.00020076  0.00086098]]]

Analyze the distribcell tally

In [28]:
# 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             =	10002
	Name           =	distribcell tally
	Filters        =	DistribcellFilter
	Nuclides       =	total 
	Scores         =	['absorption', 'scatter']
	Estimator      =	tracklength

Use the new Tally data retrieval API with pure NumPy

In [29]:
# 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=[(i,) for i in range(10)], value='rel_err')
print(data)
[[[ 0.04126529]]]

Print the distribcell tally dataframe

In [30]:
# 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)
Out[30]:
level 1 level 2 level 3 distribcell score mean std. dev.
univ cell lat univ cell
id id id x y id id
558 10002 10003 10001 16 7 10000 10002 279 absorption 7.37e-05 7.77e-06
559 10002 10003 10001 16 7 10000 10002 279 scatter 1.27e-02 6.76e-04
560 10002 10003 10001 16 8 10000 10002 280 absorption 9.14e-05 9.21e-06
561 10002 10003 10001 16 8 10000 10002 280 scatter 1.39e-02 5.27e-04
562 10002 10003 10001 16 9 10000 10002 281 absorption 8.98e-05 8.99e-06
563 10002 10003 10001 16 9 10000 10002 281 scatter 1.47e-02 6.07e-04
564 10002 10003 10001 16 10 10000 10002 282 absorption 1.13e-04 1.28e-05
565 10002 10003 10001 16 10 10000 10002 282 scatter 1.54e-02 6.58e-04
566 10002 10003 10001 16 11 10000 10002 283 absorption 1.13e-04 9.64e-06
567 10002 10003 10001 16 11 10000 10002 283 scatter 1.75e-02 5.84e-04
568 10002 10003 10001 16 12 10000 10002 284 absorption 1.10e-04 1.12e-05
569 10002 10003 10001 16 12 10000 10002 284 scatter 1.72e-02 7.15e-04
570 10002 10003 10001 16 13 10000 10002 285 absorption 1.27e-04 1.92e-05
571 10002 10003 10001 16 13 10000 10002 285 scatter 1.75e-02 8.93e-04
572 10002 10003 10001 16 14 10000 10002 286 absorption 1.24e-04 1.26e-05
573 10002 10003 10001 16 14 10000 10002 286 scatter 1.78e-02 9.03e-04
574 10002 10003 10001 16 15 10000 10002 287 absorption 1.24e-04 1.64e-05
575 10002 10003 10001 16 15 10000 10002 287 scatter 1.85e-02 1.01e-03
576 10002 10003 10001 16 16 10000 10002 288 absorption 1.32e-04 1.37e-05
577 10002 10003 10001 16 16 10000 10002 288 scatter 1.87e-02 8.24e-04
In [31]:
# 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-4 threshold set by the tally trigger
Out[31]:
mean std. dev.
count 2.89e+02 2.89e+02
mean 4.19e-04 2.40e-05
std 2.42e-04 1.03e-05
min 2.31e-05 4.39e-06
25% 2.03e-04 1.64e-05
50% 4.01e-04 2.39e-05
75% 6.17e-04 3.02e-05
max 9.28e-04 5.88e-05

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

In [32]:
# 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.7108210033985298

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.

In [33]:
# 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: 7.454144155212731e-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.

In [34]:
# 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')
/usr/local/lib/python3.5/dist-packages/ipykernel/__main__.py: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: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7d809ce2e8>
In [35]:
# 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'])
Out[35]:
<matplotlib.legend.Legend at 0x7f7d7dc8d630>

Tally Arithmetic

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.

In [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. Before defining a material, we must create nuclides that are used in the material.

In [2]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pin.

In [3]:
# 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.

In [4]:
# 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.

In [5]:
# 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.

In [6]:
# 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.

In [7]:
# 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.

In [8]:
# Create Geometry and set root Universe
geometry = openmc.Geometry()
geometry.root_universe = root_universe
In [9]:
# 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.

In [10]:
# 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.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.

In [11]:
# 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 = 'mat'

# Instantiate a Plots collection and export to "plots.xml"
plot_file = openmc.Plots([plot])
plot_file.export_to_xml()

With the plots.xml file, we can now generate and view the plot. OpenMC outputs plots in .ppm format, which can be converted into a compressed format like .png with the convert utility.

In [12]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[12]:
0
In [13]:
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png

# Display the materials plot inline
Image(filename='materials-xy.png')
Out[13]:

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.

In [14]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
In [15]:
# 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.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [1, 1, 1]
mesh.lower_left = [-0.63, -0.63, -100.]
mesh.width = [1.26, 1.26, 200.]
mesh_filter = openmc.MeshFilter(mesh)

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

thermal_leak = openmc.Tally(name='thermal leakage')
thermal_leak.filters = [mesh_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 = [mesh_filter, openmc.EnergyFilter([0.625, 20.0e6])]
fast_leak.scores = ['current']
tallies_file.append(fast_leak)
In [16]:
# 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)
In [17]:
# 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)
In [18]:
# 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)
In [19]:
# 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)
In [20]:
# 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)
In [21]:
# Export to "tallies.xml"
tallies_file.export_to_xml()

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

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

# 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 | bc4683be1c853fe6d0e31bccad416ef219e3efaf
         Date/Time | 2017-04-13 17:26:05
     MPI Processes | 1
    OpenMP Threads | 1

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from /home/smharper/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from /home/smharper/openmc/data/nndc_hdf5/U238.h5
 Reading O16 from /home/smharper/openmc/data/nndc_hdf5/O16.h5
 Reading H1 from /home/smharper/openmc/data/nndc_hdf5/H1.h5
 Reading B10 from /home/smharper/openmc/data/nndc_hdf5/B10.h5
 Reading Zr90 from /home/smharper/openmc/data/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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     =  2.2576E-01 seconds
   Reading cross sections          =  1.8005E-01 seconds
 Total time in simulation          =  9.6105E+00 seconds
   Time in transport only          =  9.5952E+00 seconds
   Time in inactive batches        =  1.4678E+00 seconds
   Time in active batches          =  8.1427E+00 seconds
   Time synchronizing fission bank =  2.6325E-03 seconds
     Sampling source sites         =  1.5038E-03 seconds
     SEND/RECV source sites        =  8.8069E-04 seconds
   Time accumulating tallies       =  2.0568E-04 seconds
 Total time for finalization       =  1.5435E-03 seconds
 Total time elapsed                =  9.8506E+00 seconds
 Calculation Rate (inactive)       =  8516.30 neutrons/second
 Calculation Rate (active)         =  4605.35 neutrons/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

Out[22]:
0

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.

In [23]:
# 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, $\langle \cdot \rangle^a_b$ represents an OpenMC that is integrated over region $a$ and energy range $b$. If $a$ or $b$ is not reported, it means the value represents an integral over all space or all energy, respectively.

In [24]:
# 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.SurfaceFilter, remove_filter=True)
leak = leak.summation(filter_type=openmc.MeshFilter, remove_filter=True)

# Compute k-infinity using tally arithmetic
keff = fiss_rate / (abs_rate + leak)
keff.get_pandas_dataframe()
Out[24]:
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 $T$ means thermal energies.

In [25]:
# 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.SurfaceFilter, remove_filter=True)
thermal_leak = thermal_leak.summation(filter_type=openmc.MeshFilter, remove_filter=True)
res_esc = (therm_abs_rate + thermal_leak) / (abs_rate + thermal_leak)
res_esc.get_pandas_dataframe()
Out[25]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total (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}$$

In [26]:
# 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()
Out[26]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total 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 $F$ denotes fuel.

In [27]:
# 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()
Out[27]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 10000 total 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}$$

In [28]:
# Compute neutrons produced per absorption (eta) using tally arithmetic
eta = therm_fiss_rate / fuel_therm_abs_rate
eta.get_pandas_dataframe()
Out[28]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 10000 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}$$

In [29]:
p_fnl = (abs_rate + thermal_leak) / (abs_rate + leak)
p_fnl.get_pandas_dataframe()
Out[29]:
energy low [eV] energy high [eV] nuclide score mean std. dev.
0 0.0 0.625 total (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}$$

In [30]:
p_tnl = therm_abs_rate / (therm_abs_rate + thermal_leak)
p_tnl.get_pandas_dataframe()
Out[30]:
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.

In [31]:
keff = res_esc * fast_fiss * therm_util * eta * p_fnl * p_tnl
keff.get_pandas_dataframe()
Out[31]:
energy low [eV] energy high [eV] cell nuclide score mean std. dev.
0 0.0 0.625 10000 total ((((((absorption + current) * nu-fission) * ab... 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.

In [32]:
# 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')
In [33]:
fuel_xs = fuel_rxn_rates / flux
fuel_xs.get_pandas_dataframe()
Out[33]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 10000 0.000 6.250000e-01 (U238 / total) (nu-fission / flux) 6.659486e-07 5.627975e-09
1 10000 0.000 6.250000e-01 (U238 / total) (scatter / flux) 2.099901e-01 1.748379e-03
2 10000 0.000 6.250000e-01 (U235 / total) (nu-fission / flux) 3.566329e-01 3.030782e-03
3 10000 0.000 6.250000e-01 (U235 / total) (scatter / flux) 5.555466e-03 4.635318e-05
4 10000 0.625 2.000000e+07 (U238 / total) (nu-fission / flux) 7.251304e-03 5.161998e-05
5 10000 0.625 2.000000e+07 (U238 / total) (scatter / flux) 2.272661e-01 9.576939e-04
6 10000 0.625 2.000000e+07 (U235 / total) (nu-fission / flux) 7.920169e-03 5.751231e-05
7 10000 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.

In [34]:
# 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.

In [35]:
# 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]]]
In [36]:
# 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.

In [37]:
# "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()
Out[37]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 10000 0.000 6.250000e-01 U238 nu-fission 0.000002 9.679304e-09
1 10000 0.000 6.250000e-01 U235 nu-fission 0.854805 5.239673e-03
2 10000 0.625 2.000000e+07 U238 nu-fission 0.082978 5.346135e-04
3 10000 0.625 2.000000e+07 U235 nu-fission 0.090632 5.981942e-04
In [38]:
# "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()
Out[38]:
cell energy low [eV] energy high [eV] nuclide score mean std. dev.
0 10002 1.000000e-02 1.080060e-01 H1 scatter 4.541188 0.025230
1 10002 1.080060e-01 1.166529e+00 H1 scatter 2.001332 0.006754
2 10002 1.166529e+00 1.259921e+01 H1 scatter 1.639292 0.011374
3 10002 1.259921e+01 1.360790e+02 H1 scatter 1.821633 0.009590
4 10002 1.360790e+02 1.469734e+03 H1 scatter 2.032395 0.009953
5 10002 1.469734e+03 1.587401e+04 H1 scatter 2.120745 0.011090
6 10002 1.587401e+04 1.714488e+05 H1 scatter 2.181709 0.013602
7 10002 1.714488e+05 1.851749e+06 H1 scatter 2.013644 0.009219
8 10002 1.851749e+06 2.000000e+07 H1 scatter 0.372640 0.002903

Modeling TRISO Particles

triso

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.

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

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

In [3]:
# Create TRISO universe
spheres = [openmc.Sphere(R=r*1e-4)
           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)

Now that we have a universe that can be used for each TRISO particle, we need to randomly select locations. In this example, we will select locations at random within a 1 cm x 1 cm x 1 cm box centered at the origin with a packing fraction of 30%. Note that pack_trisos can handle up to the theoretical maximum of 60% (it will just be slow).

In [4]:
outer_radius = 425.*1e-4

trisos = openmc.model.pack_trisos(
    radius=outer_radius,
    fill=triso_univ,
    domain_shape='cube',
    domain_length=1,
    packing_fraction=0.3
)

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

In [5]:
print(trisos[0])
Cell
	ID             =	10005
	Name           =	
	Fill           =	10000
	Region         =	-10004
	Rotation       =	None
	Translation    =	[-0.33455672  0.31790187  0.24135378]

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

In [6]:
centers = np.vstack([t.center for t 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:

In [7]:
len(trisos)*4/3*pi*outer_radius**3
Out[7]:
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'll start by creating a box that the lattice will be placed within.

In [8]:
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')
box = openmc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z)

Our last step is to actually create a lattice containing TRISO particles which can be done with 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.

In [9]:
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:

In [10]:
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.

In [11]:
univ = openmc.Universe(cells=[box])

geom = openmc.Geometry(univ)
geom.export_to_xml()

mats = list(geom.get_all_materials().values())
openmc.Materials(mats).export_to_xml()

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

p = openmc.Plot.from_geometry(geom)
openmc.plot_inline(p)

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

In [12]:
p.color_by = 'material'
p.colors = {graphite: 'gray'}
openmc.plot_inline(p)

Modeling a CANDU Bundle

candu

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.

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

In [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 out 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.

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

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

In [5]:
plot_args = {'width': (2*calendria_or, 2*calendria_or)}
bundle_universe = openmc.Universe(cells=water_cells)
bundle_universe.plot(**plot_args)

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.

In [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))
In [7]:
pin_universe.plot(**plot_args)

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.

In [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)
In [9]:
bundle_universe.plot(**plot_args)

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.

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

In [11]:
geom = openmc.Geometry(root_universe)
geom.export_to_xml()

mats = openmc.Materials(geom.get_all_materials().values())
mats.export_to_xml()
In [12]:
p = openmc.Plot.from_geometry(geom)
p.color_by = 'material'
p.colors = {
    fuel: 'black',
    clad: 'silver',
    heavy_water: 'blue'
}
openmc.plot_inline(p)

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.

In [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()
In [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()
In [15]:
openmc.run(output=False)
Out[15]:
0

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.

In [16]:
sp = openmc.StatePoint('statepoint.{}.h5'.format(settings.batches))
In [17]:
t = sp.get_tally()
t.get_pandas_dataframe()
Out[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 10002 10043 10000 100 10001 10004 0 total flux 0.207805 0.007037
1 10002 10043 10000 200 10001 10004 1 total flux 0.197197 0.005272
2 10002 10043 10000 201 10001 10004 2 total flux 0.190310 0.007816
3 10002 10043 10000 202 10001 10004 3 total flux 0.194736 0.006469
4 10002 10043 10000 203 10001 10004 4 total flux 0.191097 0.006431
5 10002 10043 10000 204 10001 10004 5 total flux 0.189910 0.004891
6 10002 10043 10000 205 10001 10004 6 total flux 0.182203 0.003851
7 10002 10043 10000 300 10001 10004 7 total flux 0.165922 0.005815
8 10002 10043 10000 301 10001 10004 8 total flux 0.168933 0.008300
9 10002 10043 10000 302 10001 10004 9 total flux 0.159587 0.003085
10 10002 10043 10000 303 10001 10004 10 total flux 0.159158 0.005910
11 10002 10043 10000 304 10001 10004 11 total flux 0.148537 0.005308
12 10002 10043 10000 305 10001 10004 12 total flux 0.150945 0.006654
13 10002 10043 10000 306 10001 10004 13 total flux 0.154237 0.003665
14 10002 10043 10000 307 10001 10004 14 total flux 0.165888 0.004733
15 10002 10043 10000 308 10001 10004 15 total flux 0.156777 0.006540
16 10002 10043 10000 309 10001 10004 16 total flux 0.165277 0.005935
17 10002 10043 10000 310 10001 10004 17 total flux 0.156528 0.005732
18 10002 10043 10000 311 10001 10004 18 total flux 0.159610 0.004584
19 10002 10043 10000 400 10001 10004 19 total flux 0.096597 0.004466
20 10002 10043 10000 401 10001 10004 20 total flux 0.118214 0.005451
21 10002 10043 10000 402 10001 10004 21 total flux 0.106167 0.004722
22 10002 10043 10000 403 10001 10004 22 total flux 0.110814 0.004208
23 10002 10043 10000 404 10001 10004 23 total flux 0.112319 0.005079
24 10002 10043 10000 405 10001 10004 24 total flux 0.110232 0.004153
25 10002 10043 10000 406 10001 10004 25 total flux 0.099967 0.005085
26 10002 10043 10000 407 10001 10004 26 total flux 0.095444 0.003615
27 10002 10043 10000 408 10001 10004 27 total flux 0.092620 0.003997
28 10002 10043 10000 409 10001 10004 28 total flux 0.095517 0.004022
29 10002 10043 10000 410 10001 10004 29 total flux 0.113737 0.009530
30 10002 10043 10000 411 10001 10004 30 total flux 0.108368 0.007241
31 10002 10043 10000 412 10001 10004 31 total flux 0.106990 0.005716
32 10002 10043 10000 413 10001 10004 32 total flux 0.112050 0.005002
33 10002 10043 10000 414 10001 10004 33 total flux 0.115054 0.006239
34 10002 10043 10000 415 10001 10004 34 total flux 0.114394 0.004919
35 10002 10043 10000 416 10001 10004 35 total flux 0.114352 0.005322
36 10002 10043 10000 417 10001 10004 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.

Nuclear Data

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.

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

In [2]:
openmc.data.atomic_mass('Fe54')
Out[2]:
53.939608986
In [3]:
openmc.data.NATURAL_ABUNDANCE['H2']
Out[3]:
0.00015574
In [4]:
openmc.data.atomic_weight('C')
Out[4]:
12.011115164862904

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.

In [5]:
url = 'https://anl.box.com/shared/static/kxm7s57z3xgfbeq29h54n7q6js8rd11c.ace'
filename, headers = urllib.request.urlretrieve(url, 'gd157.ace')
In [6]:
# Load ACE data into object
gd157 = openmc.data.IncidentNeutron.from_ace('gd157.ace')
gd157
Out[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.

In [7]:
total = gd157[1]
total
Out[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.

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

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.

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

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

In [10]:
total.xs['294K']([1.0, 2.0, 3.0])
Out[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.

In [11]:
gd157.energy
Out[11]:
{'294K': array([  1.00000000e-05,   1.03250000e-05,   1.06500000e-05, ...,
          1.95000000e+07,   1.99000000e+07,   2.00000000e+07])}
In [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)')
Out[12]:
<matplotlib.text.Text at 0x7ffa934e0358>

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.

In [13]:
pprint(list(gd157.reactions.values())[:10])
[<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)>,
 <Reaction: MT=51 (n,n1)>,
 <Reaction: MT=52 (n,n2)>,
 <Reaction: MT=53 (n,n3)>]

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

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

In [15]:
n2n.xs
Out[15]:
{'294K': <openmc.data.function.Tabulated1D at 0x7ffa95cf70f0>}
In [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]))
Out[16]:
(6400881.0, 20000000.0)

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

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

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.

In [19]:
dist = neutron.distribution[0]
dist.energy_out
Out[19]:
[<openmc.stats.univariate.Tabular at 0x7ffa95cf71d0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7278>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7320>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7470>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7668>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf78d0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7be0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cf7f60>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d00390>,
 <openmc.stats.univariate.Tabular at 0x7ffa96449390>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d00748>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d00c18>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d071d0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d07780>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d07da0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d0d438>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d0db00>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d13240>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d13940>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d1b0f0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d1b898>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d210b8>,
 <openmc.stats.univariate.Tabular at 0x7ffa95d218d0>,
 <openmc.stats.univariate.Tabular at 0x7ffa95ca7160>,
 <openmc.stats.univariate.Tabular at 0x7ffa95ca79e8>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cae2e8>,
 <openmc.stats.univariate.Tabular at 0x7ffa95caeb00>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cb4400>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cb4cf8>,
 <openmc.stats.univariate.Tabular at 0x7ffa95cba668>]

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.

In [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(ymax=1e-6)
plt.legend()
plt.xlabel('Outgoing energy (eV)')
plt.ylabel('Probability/eV')
plt.show()

There is also summed_reactions attribute for cross sections (like total) which are built from summing up other cross sections.

In [21]:
pprint(list(gd157.summed_reactions.values()))
[<Reaction: MT=1 (n,total)>, <Reaction: MT=27 (n,absorption)>]

Note that the cross sections for these reactions are represented by the Sum class rather than Tabulated1D. They do not support the x and y attributes.

In [22]:
gd157[27].xs
Out[22]:
{'294K': <openmc.data.function.Tabulated1D at 0x7ffa9643d390>}

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.

In [23]:
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)')
Out[23]:
<matplotlib.text.Text at 0x7ffa92f3c470>

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.

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

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

In [25]:
gd157_reconstructed = openmc.data.IncidentNeutron.from_hdf5('gd157.h5')
np.all(gd157[16].xs['294K'].y == gd157_reconstructed[16].xs['294K'].y)
Out[25]:
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.

In [26]:
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)
In [27]:
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.

In [28]:
n2n_group['294K/xs'].value
Out[28]:
array([  0.00000000e+00,   3.02679600e-13,   1.29110100e-02,
         6.51111000e-02,   3.92627000e-01,   5.75226800e-01,
         6.96960000e-01,   7.39937800e-01,   9.63545000e-01,
         1.14213000e+00,   1.30802000e+00,   1.46350000e+00,
         1.55760000e+00,   1.64055000e+00,   1.68896000e+00,
         1.71140000e+00,   1.73945000e+00,   1.78207000e+00,
         1.81665000e+00,   1.84528000e+00,   1.86540900e+00,
         1.86724000e+00,   1.88155800e+00,   1.88156000e+00,
         1.88180000e+00,   1.89447000e+00,   1.86957000e+00,
         1.82120000e+00,   1.71600000e+00,   1.60054000e+00,
         1.43162000e+00,   1.28346000e+00,   1.10166000e+00,
         1.06530000e+00,   9.30730000e-01,   8.02980000e-01,
         7.77740000e-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:

In [29]:
# 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
Out[29]:
<IncidentNeutron: Gd157>

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

In [30]:
elastic = gd157_endf[2]

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

In [31]:
elastic.xs
Out[31]:
{'0K': <openmc.data.function.ResonancesWithBackground at 0x7ffa91a97c50>}

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

In [32]:
elastic.xs['0K'](0.0253)
Out[32]:
998.78711745214866

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

In [33]:
gd157_endf.resonances.ranges
Out[33]:
[<openmc.data.resonance.ReichMoore at 0x7ffa92689a90>,
 <openmc.data.resonance.Unresolved at 0x7ffa92946710>]

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:

In [34]:
[(r.energy_min, r.energy_max) for r in gd157_endf.resonances.ranges]
Out[34]:
[(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.

In [35]:
# 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)')
Out[35]:
<matplotlib.text.Text at 0x7ffa919b14a8>

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

In [36]:
resolved.parameters.head(10)
Out[36]:
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 ARES. 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.

In [37]:
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.

In [38]:
gd157[2].xs
Out[38]:
{'0K': <openmc.data.function.Tabulated1D at 0x7ffa9192abe0>,
 '294K': <openmc.data.function.Tabulated1D at 0x7ffa96449da0>}

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.

In [39]:
# 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.12  02Apr17                                       05/01/17 12:33:17
 *****************************************************************************

 reconr...                                                                0.0s

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

 heatr...                                                                 0.4s

 purr...                                                                  0.6s

 mat =  128                                                               0.6s

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

 acer...                                                                  0.6s

 acer...                                                                  0.7s

 acer...                                                                  0.8s
                                                                          0.8s
 *****************************************************************************

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

In [40]:
h2[2].xs
Out[40]:
{'0K': <openmc.data.function.Tabulated1D at 0x7ffa93130d30>,
 '300K': <openmc.data.function.Tabulated1D at 0x7ffa919485c0>,
 '400K': <openmc.data.function.Tabulated1D at 0x7ffa92fcb630>,
 '500K': <openmc.data.function.Tabulated1D at 0x7ffa91805240>}

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

Multi-Group Cross Section Generation

MGXS Part I: Introduction

mgxs-part-i

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.

In [1]:
from IPython.display import Image
Image(filename='images/mgxs.png', width=350)
Out[1]:

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

In [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. Before defining a material, we must create nuclides that are used in the material.

In [3]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create a material for the homogeneous medium.

In [4]:
# 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.

In [5]:
# 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.

In [6]:
# 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.

In [7]:
# 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.

In [8]:
# Instantiate Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(cell)

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

In [9]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry()
openmc_geometry.root_universe = 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.

In [10]:
# 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.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.

In [11]:
# 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.

In [12]:
# 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.

In [13]:
absorption.tallies
Out[13]:
OrderedDict([('flux', Tally
              	ID             =	10000
              	Name           =	
              	Filters        =	CellFilter, EnergyFilter
              	Nuclides       =	total 
              	Scores         =	['flux']
              	Estimator      =	tracklength), ('absorption', Tally
              	ID             =	10001
              	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.

In [14]:
# 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()

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

In [15]:
# 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 | 43b141e9ba542da8b28c078cf2df8a6777cfb2ad
         Date/Time | 2017-02-28 11:52:00
    OpenMP Threads | 4

 ===========================================================================
 ========================>     INITIALIZATION     <=========================
 ===========================================================================

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading H1 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/H1.h5
 Reading O16 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/O16.h5
 Reading U235 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U238.h5
 Reading Zr90 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for H1
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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...

 ===========================================================================
 ======================>     SIMULATION FINISHED     <======================
 ===========================================================================


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

 Total time for initialization     =  3.0114E-01 seconds
   Reading cross sections          =  1.8743E-01 seconds
 Total time in simulation          =  9.7641E+00 seconds
   Time in transport only          =  9.5168E+00 seconds
   Time in inactive batches        =  1.2602E+00 seconds
   Time in active batches          =  8.5039E+00 seconds
   Time synchronizing fission bank =  5.4293E-03 seconds
     Sampling source sites         =  4.3508E-03 seconds
     SEND/RECV source sites        =  9.9399E-04 seconds
   Time accumulating tallies       =  1.2758E-04 seconds
 Total time for finalization       =  3.6982E-04 seconds
 Total time elapsed                =  1.0075E+01 seconds
 Calculation Rate (inactive)       =  19838.7 neutrons/second
 Calculation Rate (active)         =  11759.3 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

Out[15]:
0

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

In [16]:
# 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.

In [17]:
# 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.

In [18]:
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.

In [19]:
df = scattering.get_pandas_dataframe()
df.head(10)
Out[19]:
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.

In [20]:
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.

In [21]:
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.

In [22]:
# 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()
Out[22]:
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... 1.776357e-15 0.002570

Similarly, we can use tally arithmetic to compute the ratio of AbsorptionXS and ScatterXS to the TotalXS.

In [23]:
# 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()
Out[23]:
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
In [24]:
# 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()
Out[24]:
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.

In [25]:
# 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 scattering-to-total ratio is a derived tally which can generate Pandas DataFrames for inspection
sum_ratio.get_pandas_dataframe()
Out[25]:
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

MGXS Part II: Advanced Features

mgxs-part-ii

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

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

In [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
/home/wbinventor/miniconda3/lib/python3.5/site-packages/matplotlib/__init__.py:1350: UserWarning:  This call to matplotlib.use() has no effect
because the backend has already been chosen;
matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
or matplotlib.backends is imported for the first time.

  warnings.warn(_use_error_msg)

First we need to define materials that will be used in the problem. Before defining a material, we must create nuclides that are used in the material.

In [2]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create three distinct materials for water, clad and fuel.

In [3]:
# 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.

In [4]:
# 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.

In [5]:
# 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.

In [6]:
# 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.

In [7]:
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.region = +min_x & -max_x & +min_y & -max_y
root_cell.fill = pin_cell_universe

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

In [8]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry()
openmc_geometry.root_universe = 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 190 active batches each with 10,000 particles.

In [9]:
# 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.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.

In [10]:
# Instantiate a "coarse" 2-group EnergyGroups object
coarse_groups = mgxs.EnergyGroups()
coarse_groups.group_edges = np.array([0., 0.625, 20.0e6])

# Instantiate a "fine" 8-group EnergyGroups object
fine_groups = mgxs.EnergyGroups()
fine_groups.group_edges = np.array([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.

In [11]:
# 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.

In [12]:
# 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.

In [13]:
# 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()

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

In [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 | 647bf77a57a3cc5cce24b39cb192e1b99f52e499
         Date/Time | 2017-02-27 13:35:52
    OpenMP Threads | 4

 ===========================================================================
 ========================>     INITIALIZATION     <=========================
 ===========================================================================

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U238.h5
 Reading O16 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/O16.h5
 Reading H1 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/H1.h5
 Reading Zr90 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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 10052
 The estimated number of batches is 66
 Creating state point statepoint.050.h5...
       51/1    1.20071    1.22113 +/- 0.00228
       52/1    1.21423    1.22097 +/- 0.00223
       53/1    1.25595    1.22178 +/- 0.00233
       54/1    1.21806    1.22170 +/- 0.00227
       55/1    1.22911    1.22186 +/- 0.00223
       56/1    1.23054    1.22205 +/- 0.00219
       57/1    1.19384    1.22145 +/- 0.00222
       58/1    1.20625    1.22114 +/- 0.00220
       59/1    1.21977    1.22111 +/- 0.00216
       60/1    1.20813    1.22085 +/- 0.00213
       61/1    1.22077    1.22085 +/- 0.00209
       62/1    1.21956    1.22082 +/- 0.00205
       63/1    1.22360    1.22087 +/- 0.00201
       64/1    1.23955    1.22122 +/- 0.00200
       65/1    1.21143    1.22104 +/- 0.00197
       66/1    1.21791    1.22099 +/- 0.00194
 Triggers unsatisfied, max unc./thresh. is 1.13207 for flux in tally 10052
 The estimated number of batches is 82
       67/1    1.24897    1.22148 +/- 0.00196
       68/1    1.22221    1.22149 +/- 0.00193
       69/1    1.25627    1.22208 +/- 0.00199
       70/1    1.21493    1.22196 +/- 0.00196
       71/1    1.23406    1.22216 +/- 0.00193
       72/1    1.23842    1.22242 +/- 0.00192
       73/1    1.24542    1.22279 +/- 0.00193
       74/1    1.21314    1.22263 +/- 0.00190
       75/1    1.26484    1.22328 +/- 0.00198
       76/1    1.22243    1.22327 +/- 0.00195
       77/1    1.21865    1.22320 +/- 0.00192
       78/1    1.23500    1.22338 +/- 0.00190
       79/1    1.22125    1.22334 +/- 0.00187
       80/1    1.23793    1.22355 +/- 0.00186
       81/1    1.24238    1.22382 +/- 0.00185
       82/1    1.23493    1.22397 +/- 0.00183
 Triggers satisfied for batch 82
 Creating state point statepoint.082.h5...

 ===========================================================================
 ======================>     SIMULATION FINISHED     <======================
 ===========================================================================


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

 Total time for initialization     =  4.8359E-01 seconds
   Reading cross sections          =  3.0552E-01 seconds
 Total time in simulation          =  1.4019E+02 seconds
   Time in transport only          =  1.3995E+02 seconds
   Time in inactive batches        =  8.5036E+00 seconds
   Time in active batches          =  1.3169E+02 seconds
   Time synchronizing fission bank =  2.6010E-02 seconds
     Sampling source sites         =  1.8806E-02 seconds
     SEND/RECV source sites        =  7.0698E-03 seconds
   Time accumulating tallies       =  2.8324E-03 seconds
 Total time for finalization       =  2.2540E-02 seconds
 Total time elapsed                =  1.4077E+02 seconds
 Calculation Rate (inactive)       =  11759.7 neutrons/second
 Calculation Rate (active)         =  3037.46 neutrons/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

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

In [15]:
# 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.

In [16]:
# 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.

In [17]:
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      =	10000
	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%



/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']

Our multi-group cross sections are capable of summing across all nuclides to provide us with macroscopic cross sections as well.

In [18]:
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      =	10000
	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 .

In [19]:
nuscatter = xs_library[moderator_cell.id]['nu-scatter']
df = nuscatter.get_pandas_dataframe(xs_type='micro')
df.head(10)
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
Out[19]:
cell group in group out nuclide mean std. dev.
126 10002 1 1 H1 0.233991 0.003752
127 10002 1 1 O16 1.569288 0.006360
124 10002 1 2 H1 1.587279 0.003098
125 10002 1 2 O16 0.285599 0.001422
122 10002 1 3 H1 0.010482 0.000220
123 10002 1 3 O16 0.000000 0.000000
120 10002 1 4 H1 0.000009 0.000006
121 10002 1 4 O16 0.000000 0.000000
118 10002 1 5 H1 0.000005 0.000005
119 10002 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.

In [20]:
# Extract the 16-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 16-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.

In [21]:
condensed_xs.print_xs()
Multi-Group XS
	Reaction Type  =	transport
	Domain Type    =	cell
	Domain ID      =	10000
	Nuclide        =	U235
	Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:	7.84e-03 +/- 4.34e-01%
            Group 2 [0.0        - 0.625     eV]:	1.82e-01 +/- 1.91e-01%

	Nuclide        =	U238
	Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:	2.17e-01 +/- 1.38e-01%
            Group 2 [0.0        - 0.625     eV]:	2.53e-01 +/- 2.27e-01%

	Nuclide        =	O16
	Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:	1.45e-01 +/- 1.54e-01%
            Group 2 [0.0        - 0.625     eV]:	1.74e-01 +/- 2.60e-01%



In [22]:
df = condensed_xs.get_pandas_dataframe(xs_type='micro')
df
Out[22]:
cell group in nuclide mean std. dev.
3 10000 1 U235 20.912730 0.090857
4 10000 1 U238 9.577234 0.013248
5 10000 1 O16 3.158619 0.004864
0 10000 2 U235 485.364898 0.925632
1 10000 2 U238 11.196946 0.025466
2 10000 2 O16 3.788841 0.009855

Verification with OpenMOC

Now, let's verify our cross sections using OpenMOC. First, we construct an equivalent OpenMOC geometry.

In [23]:
# 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.

In [24]:
# 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())
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

We are now ready to run OpenMOC to verify our cross-sections from OpenMC.

In [25]:
# 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 ]  Importing ray tracing data from file...
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:	k_eff = 0.423140	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.475976	res = 5.769E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.491509	res = 1.249E-01
[  NORMAL ]  Iteration 3:	k_eff = 0.487503	res = 3.263E-02
[  NORMAL ]  Iteration 4:	k_eff = 0.484002	res = 8.150E-03
[  NORMAL ]  Iteration 5:	k_eff = 0.477365	res = 7.181E-03
[  NORMAL ]  Iteration 6:	k_eff = 0.469036	res = 1.371E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.460429	res = 1.745E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.450711	res = 1.835E-02
[  NORMAL ]  Iteration 9:	k_eff = 0.441506	res = 2.111E-02
[  NORMAL ]  Iteration 10:	k_eff = 0.432127	res = 2.042E-02
[  NORMAL ]  Iteration 11:	k_eff = 0.423076	res = 2.124E-02
[  NORMAL ]  Iteration 12:	k_eff = 0.414637	res = 2.095E-02
[  NORMAL ]  Iteration 13:	k_eff = 0.406863	res = 1.995E-02
[  NORMAL ]  Iteration 14:	k_eff = 0.399537	res = 1.875E-02
[  NORMAL ]  Iteration 15:	k_eff = 0.393230	res = 1.801E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.387592	res = 1.579E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.382836	res = 1.434E-02
[  NORMAL ]  Iteration 18:	k_eff = 0.378910	res = 1.227E-02
[  NORMAL ]  Iteration 19:	k_eff = 0.375812	res = 1.026E-02
[  NORMAL ]  Iteration 20:	k_eff = 0.373658	res = 8.176E-03
[  NORMAL ]  Iteration 21:	k_eff = 0.372526	res = 5.730E-03
[  NORMAL ]  Iteration 22:	k_eff = 0.372142	res = 3.031E-03
[  NORMAL ]  Iteration 23:	k_eff = 0.372747	res = 1.030E-03
[  NORMAL ]  Iteration 24:	k_eff = 0.374220	res = 1.627E-03
[  NORMAL ]  Iteration 25:	k_eff = 0.376545	res = 3.951E-03
[  NORMAL ]  Iteration 26:	k_eff = 0.379722	res = 6.213E-03
[  NORMAL ]  Iteration 27:	k_eff = 0.383738	res = 8.437E-03
[  NORMAL ]  Iteration 28:	k_eff = 0.388532	res = 1.058E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.394086	res = 1.249E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.400378	res = 1.429E-02
[  NORMAL ]  Iteration 31:	k_eff = 0.407375	res = 1.597E-02
[  NORMAL ]  Iteration 32:	k_eff = 0.415020	res = 1.747E-02
[  NORMAL ]  Iteration 33:	k_eff = 0.423303	res = 1.877E-02
[  NORMAL ]  Iteration 34:	k_eff = 0.432177	res = 1.996E-02
[  NORMAL ]  Iteration 35:	k_eff = 0.441592	res = 2.096E-02
[  NORMAL ]  Iteration 36:	k_eff = 0.451546	res = 2.179E-02
[  NORMAL ]  Iteration 37:	k_eff = 0.461964	res = 2.254E-02
[  NORMAL ]  Iteration 38:	k_eff = 0.472835	res = 2.307E-02
[  NORMAL ]  Iteration 39:	k_eff = 0.484106	res = 2.353E-02
[  NORMAL ]  Iteration 40:	k_eff = 0.495748	res = 2.384E-02
[  NORMAL ]  Iteration 41:	k_eff = 0.507723	res = 2.405E-02
[  NORMAL ]  Iteration 42:	k_eff = 0.519997	res = 2.416E-02
[  NORMAL ]  Iteration 43:	k_eff = 0.532536	res = 2.418E-02
[  NORMAL ]  Iteration 44:	k_eff = 0.545307	res = 2.411E-02
[  NORMAL ]  Iteration 45:	k_eff = 0.558277	res = 2.398E-02
[  NORMAL ]  Iteration 46:	k_eff = 0.571414	res = 2.379E-02
[  NORMAL ]  Iteration 47:	k_eff = 0.584691	res = 2.353E-02
[  NORMAL ]  Iteration 48:	k_eff = 0.598077	res = 2.323E-02
[  NORMAL ]  Iteration 49:	k_eff = 0.611545	res = 2.289E-02
[  NORMAL ]  Iteration 50:	k_eff = 0.625068	res = 2.252E-02
[  NORMAL ]  Iteration 51:	k_eff = 0.638624	res = 2.211E-02
[  NORMAL ]  Iteration 52:	k_eff = 0.652187	res = 2.169E-02
[  NORMAL ]  Iteration 53:	k_eff = 0.665734	res = 2.124E-02
[  NORMAL ]  Iteration 54:	k_eff = 0.679246	res = 2.077E-02
[  NORMAL ]  Iteration 55:	k_eff = 0.692702	res = 2.030E-02
[  NORMAL ]  Iteration 56:	k_eff = 0.706083	res = 1.981E-02
[  NORMAL ]  Iteration 57:	k_eff = 0.719373	res = 1.932E-02
[  NORMAL ]  Iteration 58:	k_eff = 0.732554	res = 1.882E-02
[  NORMAL ]  Iteration 59:	k_eff = 0.745613	res = 1.832E-02
[  NORMAL ]  Iteration 60:	k_eff = 0.758535	res = 1.783E-02
[  NORMAL ]  Iteration 61:	k_eff = 0.771307	res = 1.733E-02
[  NORMAL ]  Iteration 62:	k_eff = 0.783919	res = 1.684E-02
[  NORMAL ]  Iteration 63:	k_eff = 0.796359	res = 1.635E-02
[  NORMAL ]  Iteration 64:	k_eff = 0.808617	res = 1.587E-02
[  NORMAL ]  Iteration 65:	k_eff = 0.820685	res = 1.539E-02
[  NORMAL ]  Iteration 66:	k_eff = 0.832556	res = 1.492E-02
[  NORMAL ]  Iteration 67:	k_eff = 0.844222	res = 1.446E-02
[  NORMAL ]  Iteration 68:	k_eff = 0.855678	res = 1.401E-02
[  NORMAL ]  Iteration 69:	k_eff = 0.866918	res = 1.357E-02
[  NORMAL ]  Iteration 70:	k_eff = 0.877939	res = 1.314E-02
[  NORMAL ]  Iteration 71:	k_eff = 0.888734	res = 1.271E-02
[  NORMAL ]  Iteration 72:	k_eff = 0.899303	res = 1.230E-02
[  NORMAL ]  Iteration 73:	k_eff = 0.909643	res = 1.189E-02
[  NORMAL ]  Iteration 74:	k_eff = 0.919752	res = 1.150E-02
[  NORMAL ]  Iteration 75:	k_eff = 0.929628	res = 1.111E-02
[  NORMAL ]  Iteration 76:	k_eff = 0.939271	res = 1.074E-02
[  NORMAL ]  Iteration 77:	k_eff = 0.948681	res = 1.037E-02
[  NORMAL ]  Iteration 78:	k_eff = 0.957857	res = 1.002E-02
[  NORMAL ]  Iteration 79:	k_eff = 0.966802	res = 9.673E-03
[  NORMAL ]  Iteration 80:	k_eff = 0.975514	res = 9.338E-03
[  NORMAL ]  Iteration 81:	k_eff = 0.983997	res = 9.012E-03
[  NORMAL ]  Iteration 82:	k_eff = 0.992252	res = 8.696E-03
[  NORMAL ]  Iteration 83:	k_eff = 1.000280	res = 8.389E-03
[  NORMAL ]  Iteration 84:	k_eff = 1.008085	res = 8.091E-03
[  NORMAL ]  Iteration 85:	k_eff = 1.015668	res = 7.803E-03
[  NORMAL ]  Iteration 86:	k_eff = 1.023034	res = 7.522E-03
[  NORMAL ]  Iteration 87:	k_eff = 1.030183	res = 7.252E-03
[  NORMAL ]  Iteration 88:	k_eff = 1.037121	res = 6.989E-03
[  NORMAL ]  Iteration 89:	k_eff = 1.043850	res = 6.735E-03
[  NORMAL ]  Iteration 90:	k_eff = 1.050374	res = 6.488E-03
[  NORMAL ]  Iteration 91:	k_eff = 1.056697	res = 6.250E-03
[  NORMAL ]  Iteration 92:	k_eff = 1.062823	res = 6.020E-03
[  NORMAL ]  Iteration 93:	k_eff = 1.068754	res = 5.797E-03
[  NORMAL ]  Iteration 94:	k_eff = 1.074496	res = 5.581E-03
[  NORMAL ]  Iteration 95:	k_eff = 1.080052	res = 5.372E-03
[  NORMAL ]  Iteration 96:	k_eff = 1.085427	res = 5.171E-03
[  NORMAL ]  Iteration 97:	k_eff = 1.090623	res = 4.976E-03
[  NORMAL ]  Iteration 98:	k_eff = 1.095647	res = 4.788E-03
[  NORMAL ]  Iteration 99:	k_eff = 1.100501	res = 4.606E-03
[  NORMAL ]  Iteration 100:	k_eff = 1.105190	res = 4.431E-03
[  NORMAL ]  Iteration 101:	k_eff = 1.109719	res = 4.261E-03
[  NORMAL ]  Iteration 102:	k_eff = 1.114091	res = 4.098E-03
[  NORMAL ]  Iteration 103:	k_eff = 1.118310	res = 3.940E-03
[  NORMAL ]  Iteration 104:	k_eff = 1.122382	res = 3.787E-03
[  NORMAL ]  Iteration 105:	k_eff = 1.126308	res = 3.640E-03
[  NORMAL ]  Iteration 106:	k_eff = 1.130095	res = 3.498E-03
[  NORMAL ]  Iteration 107:	k_eff = 1.133745	res = 3.362E-03
[  NORMAL ]  Iteration 108:	k_eff = 1.137262	res = 3.230E-03
[  NORMAL ]  Iteration 109:	k_eff = 1.140652	res = 3.102E-03
[  NORMAL ]  Iteration 110:	k_eff = 1.143916	res = 2.980E-03
[  NORMAL ]  Iteration 111:	k_eff = 1.147061	res = 2.862E-03
[  NORMAL ]  Iteration 112:	k_eff = 1.150087	res = 2.749E-03
[  NORMAL ]  Iteration 113:	k_eff = 1.153001	res = 2.638E-03
[  NORMAL ]  Iteration 114:	k_eff = 1.155805	res = 2.534E-03
[  NORMAL ]  Iteration 115:	k_eff = 1.158502	res = 2.432E-03
[  NORMAL ]  Iteration 116:	k_eff = 1.161096	res = 2.333E-03
[  NORMAL ]  Iteration 117:	k_eff = 1.163591	res = 2.239E-03
[  NORMAL ]  Iteration 118:	k_eff = 1.165989	res = 2.149E-03
[  NORMAL ]  Iteration 119:	k_eff = 1.168295	res = 2.061E-03
[  NORMAL ]  Iteration 120:	k_eff = 1.170511	res = 1.978E-03
[  NORMAL ]  Iteration 121:	k_eff = 1.172640	res = 1.896E-03
[  NORMAL ]  Iteration 122:	k_eff = 1.174685	res = 1.819E-03
[  NORMAL ]  Iteration 123:	k_eff = 1.176649	res = 1.744E-03
[  NORMAL ]  Iteration 124:	k_eff = 1.178535	res = 1.672E-03
[  NORMAL ]  Iteration 125:	k_eff = 1.180345	res = 1.603E-03
[  NORMAL ]  Iteration 126:	k_eff = 1.182083	res = 1.536E-03
[  NORMAL ]  Iteration 127:	k_eff = 1.183751	res = 1.473E-03
[  NORMAL ]  Iteration 128:	k_eff = 1.185352	res = 1.411E-03
[  NORMAL ]  Iteration 129:	k_eff = 1.186888	res = 1.352E-03
[  NORMAL ]  Iteration 130:	k_eff = 1.188360	res = 1.295E-03
[  NORMAL ]  Iteration 131:	k_eff = 1.189772	res = 1.241E-03
[  NORMAL ]  Iteration 132:	k_eff = 1.191127	res = 1.189E-03
[  NORMAL ]  Iteration 133:	k_eff = 1.192425	res = 1.138E-03
[  NORMAL ]  Iteration 134:	k_eff = 1.193670	res = 1.090E-03
[  NORMAL ]  Iteration 135:	k_eff = 1.194863	res = 1.044E-03
[  NORMAL ]  Iteration 136:	k_eff = 1.196006	res = 9.995E-04
[  NORMAL ]  Iteration 137:	k_eff = 1.197101	res = 9.566E-04
[  NORMAL ]  Iteration 138:	k_eff = 1.198150	res = 9.162E-04
[  NORMAL ]  Iteration 139:	k_eff = 1.199155	res = 8.766E-04
[  NORMAL ]  Iteration 140:	k_eff = 1.200116	res = 8.384E-04
[  NORMAL ]  Iteration 141:	k_eff = 1.201038	res = 8.017E-04
[  NORMAL ]  Iteration 142:	k_eff = 1.201920	res = 7.684E-04
[  NORMAL ]  Iteration 143:	k_eff = 1.202765	res = 7.343E-04
[  NORMAL ]  Iteration 144:	k_eff = 1.203573	res = 7.030E-04
[  NORMAL ]  Iteration 145:	k_eff = 1.204347	res = 6.718E-04
[  NORMAL ]  Iteration 146:	k_eff = 1.205087	res = 6.424E-04
[  NORMAL ]  Iteration 147:	k_eff = 1.205796	res = 6.147E-04
[  NORMAL ]  Iteration 148:	k_eff = 1.206473	res = 5.881E-04
[  NORMAL ]  Iteration 149:	k_eff = 1.207122	res = 5.614E-04
[  NORMAL ]  Iteration 150:	k_eff = 1.207741	res = 5.376E-04
[  NORMAL ]  Iteration 151:	k_eff = 1.208334	res = 5.136E-04
[  NORMAL ]  Iteration 152:	k_eff = 1.208901	res = 4.903E-04
[  NORMAL ]  Iteration 153:	k_eff = 1.209443	res = 4.695E-04
[  NORMAL ]  Iteration 154:	k_eff = 1.209961	res = 4.482E-04
[  NORMAL ]  Iteration 155:	k_eff = 1.210455	res = 4.282E-04
[  NORMAL ]  Iteration 156:	k_eff = 1.210928	res = 4.088E-04
[  NORMAL ]  Iteration 157:	k_eff = 1.211380	res = 3.910E-04
[  NORMAL ]  Iteration 158:	k_eff = 1.211813	res = 3.730E-04
[  NORMAL ]  Iteration 159:	k_eff = 1.212225	res = 3.573E-04
[  NORMAL ]  Iteration 160:	k_eff = 1.212620	res = 3.403E-04
[  NORMAL ]  Iteration 161:	k_eff = 1.212997	res = 3.256E-04
[  NORMAL ]  Iteration 162:	k_eff = 1.213356	res = 3.107E-04
[  NORMAL ]  Iteration 163:	k_eff = 1.213700	res = 2.965E-04
[  NORMAL ]  Iteration 164:	k_eff = 1.214028	res = 2.829E-04
[  NORMAL ]  Iteration 165:	k_eff = 1.214341	res = 2.706E-04
[  NORMAL ]  Iteration 166:	k_eff = 1.214640	res = 2.581E-04
[  NORMAL ]  Iteration 167:	k_eff = 1.214926	res = 2.465E-04
[  NORMAL ]  Iteration 168:	k_eff = 1.215199	res = 2.352E-04
[  NORMAL ]  Iteration 169:	k_eff = 1.215459	res = 2.241E-04
[  NORMAL ]  Iteration 170:	k_eff = 1.215707	res = 2.144E-04
[  NORMAL ]  Iteration 171:	k_eff = 1.215944	res = 2.036E-04
[  NORMAL ]  Iteration 172:	k_eff = 1.216170	res = 1.952E-04
[  NORMAL ]  Iteration 173:	k_eff = 1.216386	res = 1.854E-04
[  NORMAL ]  Iteration 174:	k_eff = 1.216592	res = 1.780E-04
[  NORMAL ]  Iteration 175:	k_eff = 1.216788	res = 1.689E-04
[  NORMAL ]  Iteration 176:	k_eff = 1.216976	res = 1.615E-04
[  NORMAL ]  Iteration 177:	k_eff = 1.217154	res = 1.545E-04
[  NORMAL ]  Iteration 178:	k_eff = 1.217325	res = 1.462E-04
[  NORMAL ]  Iteration 179:	k_eff = 1.217487	res = 1.400E-04
[  NORMAL ]  Iteration 180:	k_eff = 1.217642	res = 1.336E-04
[  NORMAL ]  Iteration 181:	k_eff = 1.217789	res = 1.271E-04
[  NORMAL ]  Iteration 182:	k_eff = 1.217931	res = 1.212E-04
[  NORMAL ]  Iteration 183:	k_eff = 1.218065	res = 1.160E-04
[  NORMAL ]  Iteration 184:	k_eff = 1.218193	res = 1.102E-04
[  NORMAL ]  Iteration 185:	k_eff = 1.218315	res = 1.054E-04
[  NORMAL ]  Iteration 186:	k_eff = 1.218431	res = 9.964E-05
[  NORMAL ]  Iteration 187:	k_eff = 1.218542	res = 9.572E-05
[  NORMAL ]  Iteration 188:	k_eff = 1.218647	res = 9.081E-05
[  NORMAL ]  Iteration 189:	k_eff = 1.218748	res = 8.632E-05
[  NORMAL ]  Iteration 190:	k_eff = 1.218844	res = 8.232E-05
[  NORMAL ]  Iteration 191:	k_eff = 1.218936	res = 7.887E-05
[  NORMAL ]  Iteration 192:	k_eff = 1.219023	res = 7.563E-05
[  NORMAL ]  Iteration 193:	k_eff = 1.219106	res = 7.154E-05
[  NORMAL ]  Iteration 194:	k_eff = 1.219185	res = 6.778E-05
[  NORMAL ]  Iteration 195:	k_eff = 1.219261	res = 6.484E-05
[  NORMAL ]  Iteration 196:	k_eff = 1.219333	res = 6.207E-05
[  NORMAL ]  Iteration 197:	k_eff = 1.219401	res = 5.898E-05
[  NORMAL ]  Iteration 198:	k_eff = 1.219466	res = 5.590E-05
[  NORMAL ]  Iteration 199:	k_eff = 1.219528	res = 5.305E-05
[  NORMAL ]  Iteration 200:	k_eff = 1.219587	res = 5.076E-05
[  NORMAL ]  Iteration 201:	k_eff = 1.219643	res = 4.829E-05
[  NORMAL ]  Iteration 202:	k_eff = 1.219696	res = 4.595E-05
[  NORMAL ]  Iteration 203:	k_eff = 1.219747	res = 4.399E-05
[  NORMAL ]  Iteration 204:	k_eff = 1.219796	res = 4.161E-05
[  NORMAL ]  Iteration 205:	k_eff = 1.219842	res = 3.973E-05
[  NORMAL ]  Iteration 206:	k_eff = 1.219886	res = 3.781E-05
[  NORMAL ]  Iteration 207:	k_eff = 1.219928	res = 3.629E-05
[  NORMAL ]  Iteration 208:	k_eff = 1.219968	res = 3.451E-05
[  NORMAL ]  Iteration 209:	k_eff = 1.220006	res = 3.274E-05
[  NORMAL ]  Iteration 210:	k_eff = 1.220042	res = 3.108E-05
[  NORMAL ]  Iteration 211:	k_eff = 1.220076	res = 2.951E-05
[  NORMAL ]  Iteration 212:	k_eff = 1.220108	res = 2.785E-05
[  NORMAL ]  Iteration 213:	k_eff = 1.220139	res = 2.664E-05
[  NORMAL ]  Iteration 214:	k_eff = 1.220169	res = 2.518E-05
[  NORMAL ]  Iteration 215:	k_eff = 1.220197	res = 2.463E-05
[  NORMAL ]  Iteration 216:	k_eff = 1.220224	res = 2.318E-05
[  NORMAL ]  Iteration 217:	k_eff = 1.220249	res = 2.189E-05
[  NORMAL ]  Iteration 218:	k_eff = 1.220273	res = 2.075E-05
[  NORMAL ]  Iteration 219:	k_eff = 1.220296	res = 1.964E-05
[  NORMAL ]  Iteration 220:	k_eff = 1.220318	res = 1.922E-05
[  NORMAL ]  Iteration 221:	k_eff = 1.220339	res = 1.776E-05
[  NORMAL ]  Iteration 222:	k_eff = 1.220358	res = 1.701E-05
[  NORMAL ]  Iteration 223:	k_eff = 1.220377	res = 1.615E-05
[  NORMAL ]  Iteration 224:	k_eff = 1.220395	res = 1.559E-05
[  NORMAL ]  Iteration 225:	k_eff = 1.220412	res = 1.443E-05
[  NORMAL ]  Iteration 226:	k_eff = 1.220429	res = 1.403E-05
[  NORMAL ]  Iteration 227:	k_eff = 1.220444	res = 1.354E-05
[  NORMAL ]  Iteration 228:	k_eff = 1.220459	res = 1.235E-05
[  NORMAL ]  Iteration 229:	k_eff = 1.220472	res = 1.204E-05
[  NORMAL ]  Iteration 230:	k_eff = 1.220485	res = 1.135E-05
[  NORMAL ]  Iteration 231:	k_eff = 1.220498	res = 1.085E-05
[  NORMAL ]  Iteration 232:	k_eff = 1.220510	res = 1.022E-05

We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.

In [26]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined[0]
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.220510
bias [pcm]: -397.4

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.

In [27]:
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())
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']
In [28]:
# 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 ]  Importing ray tracing data from file...
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:	k_eff = 0.366890	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.391200	res = 6.331E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.393015	res = 6.626E-02
[  NORMAL ]  Iteration 3:	k_eff = 0.381131	res = 4.640E-03
[  NORMAL ]  Iteration 4:	k_eff = 0.375055	res = 3.024E-02
[  NORMAL ]  Iteration 5:	k_eff = 0.369635	res = 1.594E-02
[  NORMAL ]  Iteration 6:	k_eff = 0.365588	res = 1.445E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.363102	res = 1.095E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.361522	res = 6.801E-03
[  NORMAL ]  Iteration 9:	k_eff = 0.361329	res = 4.350E-03
[  NORMAL ]  Iteration 10:	k_eff = 0.362052	res = 5.337E-04
[  NORMAL ]  Iteration 11:	k_eff = 0.363766	res = 2.000E-03
[  NORMAL ]  Iteration 12:	k_eff = 0.366383	res = 4.733E-03
[  NORMAL ]  Iteration 13:	k_eff = 0.369846	res = 7.196E-03
[  NORMAL ]  Iteration 14:	k_eff = 0.374028	res = 9.452E-03
[  NORMAL ]  Iteration 15:	k_eff = 0.378957	res = 1.131E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.384508	res = 1.318E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.390660	res = 1.465E-02
[  NORMAL ]  Iteration 18:	k_eff = 0.397354	res = 1.600E-02
[  NORMAL ]  Iteration 19:	k_eff = 0.404542	res = 1.713E-02
[  NORMAL ]  Iteration 20:	k_eff = 0.412186	res = 1.809E-02
[  NORMAL ]  Iteration 21:	k_eff = 0.420246	res = 1.889E-02
[  NORMAL ]  Iteration 22:	k_eff = 0.428671	res = 1.956E-02
[  NORMAL ]  Iteration 23:	k_eff = 0.437437	res = 2.005E-02
[  NORMAL ]  Iteration 24:	k_eff = 0.446503	res = 2.045E-02
[  NORMAL ]  Iteration 25:	k_eff = 0.455839	res = 2.073E-02
[  NORMAL ]  Iteration 26:	k_eff = 0.465413	res = 2.091E-02
[  NORMAL ]  Iteration 27:	k_eff = 0.475198	res = 2.100E-02
[  NORMAL ]  Iteration 28:	k_eff = 0.485167	res = 2.103E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.495295	res = 2.098E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.505558	res = 2.087E-02
[  NORMAL ]  Iteration 31:	k_eff = 0.515935	res = 2.072E-02
[  NORMAL ]  Iteration 32:	k_eff = 0.526405	res = 2.053E-02
[  NORMAL ]  Iteration 33:	k_eff = 0.536950	res = 2.029E-02
[  NORMAL ]  Iteration 34:	k_eff = 0.547551	res = 2.003E-02
[  NORMAL ]  Iteration 35:	k_eff = 0.558191	res = 1.974E-02
[  NORMAL ]  Iteration 36:	k_eff = 0.568856	res = 1.943E-02
[  NORMAL ]  Iteration 37:	k_eff = 0.579532	res = 1.911E-02
[  NORMAL ]  Iteration 38:	k_eff = 0.590203	res = 1.877E-02
[  NORMAL ]  Iteration 39:	k_eff = 0.600860	res = 1.841E-02
[  NORMAL ]  Iteration 40:	k_eff = 0.611489	res = 1.806E-02
[  NORMAL ]  Iteration 41:	k_eff = 0.622080	res = 1.769E-02
[  NORMAL ]  Iteration 42:	k_eff = 0.632623	res = 1.732E-02
[  NORMAL ]  Iteration 43:	k_eff = 0.643111	res = 1.695E-02
[  NORMAL ]  Iteration 44:	k_eff = 0.653533	res = 1.658E-02
[  NORMAL ]  Iteration 45:	k_eff = 0.663882	res = 1.621E-02
[  NORMAL ]  Iteration 46:	k_eff = 0.674152	res = 1.584E-02
[  NORMAL ]  Iteration 47:	k_eff = 0.684335	res = 1.547E-02
[  NORMAL ]  Iteration 48:	k_eff = 0.694427	res = 1.511E-02
[  NORMAL ]  Iteration 49:	k_eff = 0.704421	res = 1.475E-02
[  NORMAL ]  Iteration 50:	k_eff = 0.714313	res = 1.439E-02
[  NORMAL ]  Iteration 51:	k_eff = 0.724099	res = 1.404E-02
[  NORMAL ]  Iteration 52:	k_eff = 0.733774	res = 1.370E-02
[  NORMAL ]  Iteration 53:	k_eff = 0.743335	res = 1.336E-02
[  NORMAL ]  Iteration 54:	k_eff = 0.752778	res = 1.303E-02
[  NORMAL ]  Iteration 55:	k_eff = 0.762103	res = 1.270E-02
[  NORMAL ]  Iteration 56:	k_eff = 0.771304	res = 1.239E-02
[  NORMAL ]  Iteration 57:	k_eff = 0.780382	res = 1.207E-02
[  NORMAL ]  Iteration 58:	k_eff = 0.789332	res = 1.177E-02
[  NORMAL ]  Iteration 59:	k_eff = 0.798155	res = 1.147E-02
[  NORMAL ]  Iteration 60:	k_eff = 0.806849	res = 1.118E-02
[  NORMAL ]  Iteration 61:	k_eff = 0.815413	res = 1.089E-02
[  NORMAL ]  Iteration 62:	k_eff = 0.823846	res = 1.061E-02
[  NORMAL ]  Iteration 63:	k_eff = 0.832147	res = 1.034E-02
[  NORMAL ]  Iteration 64:	k_eff = 0.840316	res = 1.008E-02
[  NORMAL ]  Iteration 65:	k_eff = 0.848354	res = 9.817E-03
[  NORMAL ]  Iteration 66:	k_eff = 0.856259	res = 9.565E-03
[  NORMAL ]  Iteration 67:	k_eff = 0.864033	res = 9.318E-03
[  NORMAL ]  Iteration 68:	k_eff = 0.871674	res = 9.078E-03
[  NORMAL ]  Iteration 69:	k_eff = 0.879184	res = 8.844E-03
[  NORMAL ]  Iteration 70:	k_eff = 0.886563	res = 8.616E-03
[  NORMAL ]  Iteration 71:	k_eff = 0.893812	res = 8.393E-03
[  NORMAL ]  Iteration 72:	k_eff = 0.900932	res = 8.177E-03
[  NORMAL ]  Iteration 73:	k_eff = 0.907923	res = 7.965E-03
[  NORMAL ]  Iteration 74:	k_eff = 0.914786	res = 7.760E-03
[  NORMAL ]  Iteration 75:	k_eff = 0.921523	res = 7.560E-03
[  NORMAL ]  Iteration 76:	k_eff = 0.928134	res = 7.365E-03
[  NORMAL ]  Iteration 77:	k_eff = 0.934621	res = 7.174E-03
[  NORMAL ]  Iteration 78:	k_eff = 0.940985	res = 6.989E-03
[  NORMAL ]  Iteration 79:	k_eff = 0.947227	res = 6.809E-03
[  NORMAL ]  Iteration 80:	k_eff = 0.953349	res = 6.634E-03
[  NORMAL ]  Iteration 81:	k_eff = 0.959352	res = 6.463E-03
[  NORMAL ]  Iteration 82:	k_eff = 0.965237	res = 6.297E-03
[  NORMAL ]  Iteration 83:	k_eff = 0.971006	res = 6.134E-03
[  NORMAL ]  Iteration 84:	k_eff = 0.976660	res = 5.977E-03
[  NORMAL ]  Iteration 85:	k_eff = 0.982201	res = 5.823E-03
[  NORMAL ]  Iteration 86:	k_eff = 0.987631	res = 5.674E-03
[  NORMAL ]  Iteration 87:	k_eff = 0.992950	res = 5.528E-03
[  NORMAL ]  Iteration 88:	k_eff = 0.998161	res = 5.386E-03
[  NORMAL ]  Iteration 89:	k_eff = 1.003266	res = 5.248E-03
[  NORMAL ]  Iteration 90:	k_eff = 1.008265	res = 5.114E-03
[  NORMAL ]  Iteration 91:	k_eff = 1.013161	res = 4.983E-03
[  NORMAL ]  Iteration 92:	k_eff = 1.017954	res = 4.856E-03
[  NORMAL ]  Iteration 93:	k_eff = 1.022648	res = 4.731E-03
[  NORMAL ]  Iteration 94:	k_eff = 1.027243	res = 4.611E-03
[  NORMAL ]  Iteration 95:	k_eff = 1.031741	res = 4.493E-03
[  NORMAL ]  Iteration 96:	k_eff = 1.036143	res = 4.379E-03
[  NORMAL ]  Iteration 97:	k_eff = 1.040452	res = 4.267E-03
[  NORMAL ]  Iteration 98:	k_eff = 1.044667	res = 4.159E-03
[  NORMAL ]  Iteration 99:	k_eff = 1.048794	res = 4.052E-03
[  NORMAL ]  Iteration 100:	k_eff = 1.052831	res = 3.950E-03
[  NORMAL ]  Iteration 101:	k_eff = 1.056781	res = 3.849E-03
[  NORMAL ]  Iteration 102:	k_eff = 1.060645	res = 3.752E-03
[  NORMAL ]  Iteration 103:	k_eff = 1.064425	res = 3.656E-03
[  NORMAL ]  Iteration 104:	k_eff = 1.068122	res = 3.564E-03
[  NORMAL ]  Iteration 105:	k_eff = 1.071738	res = 3.474E-03
[  NORMAL ]  Iteration 106:	k_eff = 1.075274	res = 3.386E-03
[  NORMAL ]  Iteration 107:	k_eff = 1.078734	res = 3.300E-03
[  NORMAL ]  Iteration 108:	k_eff = 1.082116	res = 3.217E-03
[  NORMAL ]  Iteration 109:	k_eff = 1.085423	res = 3.136E-03
[  NORMAL ]  Iteration 110:	k_eff = 1.088657	res = 3.056E-03
[  NORMAL ]  Iteration 111:	k_eff = 1.091818	res = 2.980E-03
[  NORMAL ]  Iteration 112:	k_eff = 1.094909	res = 2.904E-03
[  NORMAL ]  Iteration 113:	k_eff = 1.097930	res = 2.831E-03
[  NORMAL ]  Iteration 114:	k_eff = 1.100884	res = 2.760E-03
[  NORMAL ]  Iteration 115:	k_eff = 1.103771	res = 2.690E-03
[  NORMAL ]  Iteration 116:	k_eff = 1.106593	res = 2.623E-03
[  NORMAL ]  Iteration 117:	k_eff = 1.109351	res = 2.557E-03
[  NORMAL ]  Iteration 118:	k_eff = 1.112047	res = 2.492E-03
[  NORMAL ]  Iteration 119:	k_eff = 1.114682	res = 2.430E-03
[  NORMAL ]  Iteration 120:	k_eff = 1.117257	res = 2.369E-03
[  NORMAL ]  Iteration 121:	k_eff = 1.119772	res = 2.310E-03
[  NORMAL ]  Iteration 122:	k_eff = 1.122231	res = 2.251E-03
[  NORMAL ]  Iteration 123:	k_eff = 1.124633	res = 2.196E-03
[  NORMAL ]  Iteration 124:	k_eff = 1.126980	res = 2.140E-03
[  NORMAL ]  Iteration 125:	k_eff = 1.129273	res = 2.087E-03
[  NORMAL ]  Iteration 126:	k_eff = 1.131514	res = 2.035E-03
[  NORMAL ]  Iteration 127:	k_eff = 1.133703	res = 1.984E-03
[  NORMAL ]  Iteration 128:	k_eff = 1.135840	res = 1.934E-03
[  NORMAL ]  Iteration 129:	k_eff = 1.137929	res = 1.886E-03
[  NORMAL ]  Iteration 130:	k_eff = 1.139970	res = 1.839E-03
[  NORMAL ]  Iteration 131:	k_eff = 1.141963	res = 1.793E-03
[  NORMAL ]  Iteration 132:	k_eff = 1.143910	res = 1.749E-03
[  NORMAL ]  Iteration 133:	k_eff = 1.145812	res = 1.705E-03
[  NORMAL ]  Iteration 134:	k_eff = 1.147670	res = 1.663E-03
[  NORMAL ]  Iteration 135:	k_eff = 1.149484	res = 1.622E-03
[  NORMAL ]  Iteration 136:	k_eff = 1.151256	res = 1.581E-03
[  NORMAL ]  Iteration 137:	k_eff = 1.152986	res = 1.541E-03
[  NORMAL ]  Iteration 138:	k_eff = 1.154676	res = 1.503E-03
[  NORMAL ]  Iteration 139:	k_eff = 1.156326	res = 1.466E-03
[  NORMAL ]  Iteration 140:	k_eff = 1.157938	res = 1.429E-03
[  NORMAL ]  Iteration 141:	k_eff = 1.159512	res = 1.394E-03
[  NORMAL ]  Iteration 142:	k_eff = 1.161049	res = 1.359E-03
[  NORMAL ]  Iteration 143:	k_eff = 1.162549	res = 1.326E-03
[  NORMAL ]  Iteration 144:	k_eff = 1.164015	res = 1.292E-03
[  NORMAL ]  Iteration 145:	k_eff = 1.165446	res = 1.261E-03
[  NORMAL ]  Iteration 146:	k_eff = 1.166843	res = 1.229E-03
[  NORMAL ]  Iteration 147:	k_eff = 1.168208	res = 1.199E-03
[  NORMAL ]  Iteration 148:	k_eff = 1.169540	res = 1.169E-03
[  NORMAL ]  Iteration 149:	k_eff = 1.170840	res = 1.140E-03
[  NORMAL ]  Iteration 150:	k_eff = 1.172110	res = 1.112E-03
[  NORMAL ]  Iteration 151:	k_eff = 1.173350	res = 1.085E-03
[  NORMAL ]  Iteration 152:	k_eff = 1.174560	res = 1.058E-03
[  NORMAL ]  Iteration 153:	k_eff = 1.175742	res = 1.031E-03
[  NORMAL ]  Iteration 154:	k_eff = 1.176895	res = 1.006E-03
[  NORMAL ]  Iteration 155:	k_eff = 1.178021	res = 9.812E-04
[  NORMAL ]  Iteration 156:	k_eff = 1.179121	res = 9.567E-04
[  NORMAL ]  Iteration 157:	k_eff = 1.180194	res = 9.336E-04
[  NORMAL ]  Iteration 158:	k_eff = 1.181242	res = 9.100E-04
[  NORMAL ]  Iteration 159:	k_eff = 1.182264	res = 8.875E-04
[  NORMAL ]  Iteration 160:	k_eff = 1.183264	res = 8.656E-04
[  NORMAL ]  Iteration 161:	k_eff = 1.184238	res = 8.452E-04
[  NORMAL ]  Iteration 162:	k_eff = 1.185189	res = 8.233E-04
[  NORMAL ]  Iteration 163:	k_eff = 1.186118	res = 8.029E-04
[  NORMAL ]  Iteration 164:	k_eff = 1.187024	res = 7.839E-04
[  NORMAL ]  Iteration 165:	k_eff = 1.187908	res = 7.641E-04
[  NORMAL ]  Iteration 166:	k_eff = 1.188772	res = 7.449E-04
[  NORMAL ]  Iteration 167:	k_eff = 1.189615	res = 7.271E-04
[  NORMAL ]  Iteration 168:	k_eff = 1.190438	res = 7.093E-04
[  NORMAL ]  Iteration 169:	k_eff = 1.191241	res = 6.915E-04
[  NORMAL ]  Iteration 170:	k_eff = 1.192024	res = 6.742E-04
[  NORMAL ]  Iteration 171:	k_eff = 1.192789	res = 6.576E-04
[  NORMAL ]  Iteration 172:	k_eff = 1.193536	res = 6.419E-04
[  NORMAL ]  Iteration 173:	k_eff = 1.194265	res = 6.263E-04
[  NORMAL ]  Iteration 174:	k_eff = 1.194976	res = 6.104E-04
[  NORMAL ]  Iteration 175:	k_eff = 1.195670	res = 5.955E-04
[  NORMAL ]  Iteration 176:	k_eff = 1.196347	res = 5.806E-04
[  NORMAL ]  Iteration 177:	k_eff = 1.197008	res = 5.665E-04
[  NORMAL ]  Iteration 178:	k_eff = 1.197653	res = 5.525E-04
[  NORMAL ]  Iteration 179:	k_eff = 1.198284	res = 5.388E-04
[  NORMAL ]  Iteration 180:	k_eff = 1.198898	res = 5.263E-04
[  NORMAL ]  Iteration 181:	k_eff = 1.199498	res = 5.126E-04
[  NORMAL ]  Iteration 182:	k_eff = 1.200083	res = 5.000E-04
[  NORMAL ]  Iteration 183:	k_eff = 1.200654	res = 4.880E-04
[  NORMAL ]  Iteration 184:	k_eff = 1.201212	res = 4.757E-04
[  NORMAL ]  Iteration 185:	k_eff = 1.201756	res = 4.647E-04
[  NORMAL ]  Iteration 186:	k_eff = 1.202286	res = 4.528E-04
[  NORMAL ]  Iteration 187:	k_eff = 1.202804	res = 4.414E-04
[  NORMAL ]  Iteration 188:	k_eff = 1.203310	res = 4.310E-04
[  NORMAL ]  Iteration 189:	k_eff = 1.203803	res = 4.203E-04
[  NORMAL ]  Iteration 190:	k_eff = 1.204285	res = 4.100E-04
[  NORMAL ]  Iteration 191:	k_eff = 1.204755	res = 4.003E-04
[  NORMAL ]  Iteration 192:	k_eff = 1.205214	res = 3.902E-04
[  NORMAL ]  Iteration 193:	k_eff = 1.205661	res = 3.810E-04
[  NORMAL ]  Iteration 194:	k_eff = 1.206097	res = 3.711E-04
[  NORMAL ]  Iteration 195:	k_eff = 1.206523	res = 3.617E-04
[  NORMAL ]  Iteration 196:	k_eff = 1.206939	res = 3.528E-04
[  NORMAL ]  Iteration 197:	k_eff = 1.207345	res = 3.447E-04
[  NORMAL ]  Iteration 198:	k_eff = 1.207740	res = 3.363E-04
[  NORMAL ]  Iteration 199:	k_eff = 1.208127	res = 3.281E-04
[  NORMAL ]  Iteration 200:	k_eff = 1.208503	res = 3.198E-04
[  NORMAL ]  Iteration 201:	k_eff = 1.208871	res = 3.119E-04
[  NORMAL ]  Iteration 202:	k_eff = 1.209230	res = 3.041E-04
[  NORMAL ]  Iteration 203:	k_eff = 1.209580	res = 2.968E-04
[  NORMAL ]  Iteration 204:	k_eff = 1.209921	res = 2.897E-04
[  NORMAL ]  Iteration 205:	k_eff = 1.210255	res = 2.824E-04
[  NORMAL ]  Iteration 206:	k_eff = 1.210580	res = 2.759E-04
[  NORMAL ]  Iteration 207:	k_eff = 1.210898	res = 2.688E-04
[  NORMAL ]  Iteration 208:	k_eff = 1.211208	res = 2.622E-04
[  NORMAL ]  Iteration 209:	k_eff = 1.211510	res = 2.563E-04
[  NORMAL ]  Iteration 210:	k_eff = 1.211804	res = 2.495E-04
[  NORMAL ]  Iteration 211:	k_eff = 1.212092	res = 2.428E-04
[  NORMAL ]  Iteration 212:	k_eff = 1.212373	res = 2.374E-04
[  NORMAL ]  Iteration 213:	k_eff = 1.212647	res = 2.318E-04
[  NORMAL ]  Iteration 214:	k_eff = 1.212914	res = 2.258E-04
[  NORMAL ]  Iteration 215:	k_eff = 1.213175	res = 2.200E-04
[  NORMAL ]  Iteration 216:	k_eff = 1.213429	res = 2.153E-04
[  NORMAL ]  Iteration 217:	k_eff = 1.213678	res = 2.097E-04
[  NORMAL ]  Iteration 218:	k_eff = 1.213920	res = 2.049E-04
[  NORMAL ]  Iteration 219:	k_eff = 1.214156	res = 1.996E-04
[  NORMAL ]  Iteration 220:	k_eff = 1.214387	res = 1.945E-04
[  NORMAL ]  Iteration 221:	k_eff = 1.214612	res = 1.904E-04
[  NORMAL ]  Iteration 222:	k_eff = 1.214832	res = 1.853E-04
[  NORMAL ]  Iteration 223:	k_eff = 1.215046	res = 1.806E-04
[  NORMAL ]  Iteration 224:	k_eff = 1.215255	res = 1.762E-04
[  NORMAL ]  Iteration 225:	k_eff = 1.215459	res = 1.722E-04
[  NORMAL ]  Iteration 226:	k_eff = 1.215658	res = 1.676E-04
[  NORMAL ]  Iteration 227:	k_eff = 1.215852	res = 1.637E-04
[  NORMAL ]  Iteration 228:	k_eff = 1.216041	res = 1.597E-04
[  NORMAL ]  Iteration 229:	k_eff = 1.216226	res = 1.557E-04
[  NORMAL ]  Iteration 230:	k_eff = 1.216407	res = 1.520E-04
[  NORMAL ]  Iteration 231:	k_eff = 1.216582	res = 1.483E-04
[  NORMAL ]  Iteration 232:	k_eff = 1.216754	res = 1.444E-04
[  NORMAL ]  Iteration 233:	k_eff = 1.216921	res = 1.409E-04
[  NORMAL ]  Iteration 234:	k_eff = 1.217084	res = 1.376E-04
[  NORMAL ]  Iteration 235:	k_eff = 1.217244	res = 1.344E-04
[  NORMAL ]  Iteration 236:	k_eff = 1.217399	res = 1.309E-04
[  NORMAL ]  Iteration 237:	k_eff = 1.217551	res = 1.274E-04
[  NORMAL ]  Iteration 238:	k_eff = 1.217699	res = 1.247E-04
[  NORMAL ]  Iteration 239:	k_eff = 1.217843	res = 1.216E-04
[  NORMAL ]  Iteration 240:	k_eff = 1.217983	res = 1.184E-04
[  NORMAL ]  Iteration 241:	k_eff = 1.218121	res = 1.154E-04
[  NORMAL ]  Iteration 242:	k_eff = 1.218255	res = 1.131E-04
[  NORMAL ]  Iteration 243:	k_eff = 1.218386	res = 1.101E-04
[  NORMAL ]  Iteration 244:	k_eff = 1.218514	res = 1.075E-04
[  NORMAL ]  Iteration 245:	k_eff = 1.218639	res = 1.048E-04
[  NORMAL ]  Iteration 246:	k_eff = 1.218760	res = 1.024E-04
[  NORMAL ]  Iteration 247:	k_eff = 1.218879	res = 9.989E-05
[  NORMAL ]  Iteration 248:	k_eff = 1.218994	res = 9.739E-05
[  NORMAL ]  Iteration 249:	k_eff = 1.219107	res = 9.487E-05
[  NORMAL ]  Iteration 250:	k_eff = 1.219217	res = 9.257E-05
[  NORMAL ]  Iteration 251:	k_eff = 1.219324	res = 9.035E-05
[  NORMAL ]  Iteration 252:	k_eff = 1.219429	res = 8.803E-05
[  NORMAL ]  Iteration 253:	k_eff = 1.219531	res = 8.599E-05
[  NORMAL ]  Iteration 254:	k_eff = 1.219631	res = 8.364E-05
[  NORMAL ]  Iteration 255:	k_eff = 1.219728	res = 8.175E-05
[  NORMAL ]  Iteration 256:	k_eff = 1.219823	res = 7.971E-05
[  NORMAL ]  Iteration 257:	k_eff = 1.219916	res = 7.776E-05
[  NORMAL ]  Iteration 258:	k_eff = 1.220006	res = 7.582E-05
[  NORMAL ]  Iteration 259:	k_eff = 1.220094	res = 7.438E-05
[  NORMAL ]  Iteration 260:	k_eff = 1.220181	res = 7.227E-05
[  NORMAL ]  Iteration 261:	k_eff = 1.220264	res = 7.058E-05
[  NORMAL ]  Iteration 262:	k_eff = 1.220346	res = 6.872E-05
[  NORMAL ]  Iteration 263:	k_eff = 1.220426	res = 6.695E-05
[  NORMAL ]  Iteration 264:	k_eff = 1.220504	res = 6.549E-05
[  NORMAL ]  Iteration 265:	k_eff = 1.220580	res = 6.375E-05
[  NORMAL ]  Iteration 266:	k_eff = 1.220654	res = 6.240E-05
[  NORMAL ]  Iteration 267:	k_eff = 1.220726	res = 6.073E-05
[  NORMAL ]  Iteration 268:	k_eff = 1.220797	res = 5.942E-05
[  NORMAL ]  Iteration 269:	k_eff = 1.220866	res = 5.792E-05
[  NORMAL ]  Iteration 270:	k_eff = 1.220933	res = 5.661E-05
[  NORMAL ]  Iteration 271:	k_eff = 1.220999	res = 5.501E-05
[  NORMAL ]  Iteration 272:	k_eff = 1.221063	res = 5.368E-05
[  NORMAL ]  Iteration 273:	k_eff = 1.221125	res = 5.286E-05
[  NORMAL ]  Iteration 274:	k_eff = 1.221187	res = 5.103E-05
[  NORMAL ]  Iteration 275:	k_eff = 1.221246	res = 5.013E-05
[  NORMAL ]  Iteration 276:	k_eff = 1.221304	res = 4.861E-05
[  NORMAL ]  Iteration 277:	k_eff = 1.221360	res = 4.737E-05
[  NORMAL ]  Iteration 278:	k_eff = 1.221416	res = 4.629E-05
[  NORMAL ]  Iteration 279:	k_eff = 1.221470	res = 4.515E-05
[  NORMAL ]  Iteration 280:	k_eff = 1.221522	res = 4.417E-05
[  NORMAL ]  Iteration 281:	k_eff = 1.221574	res = 4.309E-05
[  NORMAL ]  Iteration 282:	k_eff = 1.221624	res = 4.219E-05
[  NORMAL ]  Iteration 283:	k_eff = 1.221673	res = 4.099E-05
[  NORMAL ]  Iteration 284:	k_eff = 1.221721	res = 3.998E-05
[  NORMAL ]  Iteration 285:	k_eff = 1.221767	res = 3.891E-05
[  NORMAL ]  Iteration 286:	k_eff = 1.221812	res = 3.781E-05
[  NORMAL ]  Iteration 287:	k_eff = 1.221857	res = 3.698E-05
[  NORMAL ]  Iteration 288:	k_eff = 1.221900	res = 3.633E-05
[  NORMAL ]  Iteration 289:	k_eff = 1.221942	res = 3.535E-05
[  NORMAL ]  Iteration 290:	k_eff = 1.221983	res = 3.445E-05
[  NORMAL ]  Iteration 291:	k_eff = 1.222023	res = 3.355E-05
[  NORMAL ]  Iteration 292:	k_eff = 1.222062	res = 3.281E-05
[  NORMAL ]  Iteration 293:	k_eff = 1.222100	res = 3.203E-05
[  NORMAL ]  Iteration 294:	k_eff = 1.222138	res = 3.108E-05
[  NORMAL ]  Iteration 295:	k_eff = 1.222174	res = 3.049E-05
[  NORMAL ]  Iteration 296:	k_eff = 1.222209	res = 2.969E-05
[  NORMAL ]  Iteration 297:	k_eff = 1.222244	res = 2.902E-05
[  NORMAL ]  Iteration 298:	k_eff = 1.222278	res = 2.820E-05
[  NORMAL ]  Iteration 299:	k_eff = 1.222310	res = 2.747E-05
[  NORMAL ]  Iteration 300:	k_eff = 1.222342	res = 2.679E-05
[  NORMAL ]  Iteration 301:	k_eff = 1.222374	res = 2.626E-05
[  NORMAL ]  Iteration 302:	k_eff = 1.222404	res = 2.554E-05
[  NORMAL ]  Iteration 303:	k_eff = 1.222434	res = 2.483E-05
[  NORMAL ]  Iteration 304:	k_eff = 1.222463	res = 2.432E-05
[  NORMAL ]  Iteration 305:	k_eff = 1.222492	res = 2.387E-05
[  NORMAL ]  Iteration 306:	k_eff = 1.222519	res = 2.316E-05
[  NORMAL ]  Iteration 307:	k_eff = 1.222546	res = 2.274E-05
[  NORMAL ]  Iteration 308:	k_eff = 1.222573	res = 2.220E-05
[  NORMAL ]  Iteration 309:	k_eff = 1.222599	res = 2.168E-05
[  NORMAL ]  Iteration 310:	k_eff = 1.222624	res = 2.107E-05
[  NORMAL ]  Iteration 311:	k_eff = 1.222648	res = 2.062E-05
[  NORMAL ]  Iteration 312:	k_eff = 1.222672	res = 2.005E-05
[  NORMAL ]  Iteration 313:	k_eff = 1.222696	res = 1.959E-05
[  NORMAL ]  Iteration 314:	k_eff = 1.222718	res = 1.898E-05
[  NORMAL ]  Iteration 315:	k_eff = 1.222740	res = 1.841E-05
[  NORMAL ]  Iteration 316:	k_eff = 1.222762	res = 1.808E-05
[  NORMAL ]  Iteration 317:	k_eff = 1.222783	res = 1.765E-05
[  NORMAL ]  Iteration 318:	k_eff = 1.222803	res = 1.713E-05
[  NORMAL ]  Iteration 319:	k_eff = 1.222823	res = 1.674E-05
[  NORMAL ]  Iteration 320:	k_eff = 1.222843	res = 1.638E-05
[  NORMAL ]  Iteration 321:	k_eff = 1.222862	res = 1.593E-05
[  NORMAL ]  Iteration 322:	k_eff = 1.222880	res = 1.546E-05
[  NORMAL ]  Iteration 323:	k_eff = 1.222898	res = 1.519E-05
[  NORMAL ]  Iteration 324:	k_eff = 1.222916	res = 1.487E-05
[  NORMAL ]  Iteration 325:	k_eff = 1.222933	res = 1.446E-05
[  NORMAL ]  Iteration 326:	k_eff = 1.222950	res = 1.403E-05
[  NORMAL ]  Iteration 327:	k_eff = 1.222967	res = 1.379E-05
[  NORMAL ]  Iteration 328:	k_eff = 1.222982	res = 1.342E-05
[  NORMAL ]  Iteration 329:	k_eff = 1.222998	res = 1.298E-05
[  NORMAL ]  Iteration 330:	k_eff = 1.223013	res = 1.278E-05
[  NORMAL ]  Iteration 331:	k_eff = 1.223028	res = 1.236E-05
[  NORMAL ]  Iteration 332:	k_eff = 1.223043	res = 1.227E-05
[  NORMAL ]  Iteration 333:	k_eff = 1.223057	res = 1.177E-05
[  NORMAL ]  Iteration 334:	k_eff = 1.223070	res = 1.146E-05
[  NORMAL ]  Iteration 335:	k_eff = 1.223084	res = 1.122E-05
[  NORMAL ]  Iteration 336:	k_eff = 1.223097	res = 1.105E-05
[  NORMAL ]  Iteration 337:	k_eff = 1.223110	res = 1.063E-05
[  NORMAL ]  Iteration 338:	k_eff = 1.223122	res = 1.054E-05
[  NORMAL ]  Iteration 339:	k_eff = 1.223134	res = 1.015E-05
[  NORMAL ]  Iteration 340:	k_eff = 1.223146	res = 1.005E-05
In [29]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined[0]
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.223146
bias [pcm]: -133.8

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.

In [30]:
# 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()))
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
Out[30]:
(1.0000000000000001e-05, 20000000.0)

Another useful type of illustration is scattering matrix sparsity structures. First, we extract Pandas DataFrames for the H-1 and O-16 scattering matrices.

In [31]:
# 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.as_matrix()
o16 = o16.as_matrix()

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

In [32]:
# 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()

MGXS Part III: Libraries

mgxs-part-iii

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

In [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
/home/wbinventor/miniconda3/lib/python3.5/site-packages/matplotlib/__init__.py:1350: UserWarning:  This call to matplotlib.use() has no effect
because the backend has already been chosen;
matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
or matplotlib.backends is imported for the first time.

  warnings.warn(_use_error_msg)

First we need to define materials that will be used in the problem. Before defining a material, we must create nuclides that are used in the material.

In [2]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pins.

In [3]:
# 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.

In [4]:
# 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.

In [5]:
# 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.

In [6]:
# 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.

In [7]:
# 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.

In [8]:
# 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.

In [9]:
# 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.

In [10]:
# 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.

In [11]:
# Create Geometry and set root Universe
geometry = openmc.Geometry()
geometry.root_universe = root_universe
In [12]:
# 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.

In [13]:
# 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': 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.Source(space=uniform_dist)

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

Let us also create a Plots file that we can use to verify that our fuel assembly geometry was created successfully.

In [14]:
# Instantiate a Plot
plot = openmc.Plot(plot_id=1)
plot.filename = 'materials-xy'
plot.origin = [0, 0, 0]
plot.pixels = [250, 250]
plot.width = [-10.71*2, -10.71*2]
plot.color = 'mat'

# Instantiate a Plots object, add Plot, and export to "plots.xml"
plot_file = openmc.Plots([plot])
plot_file.export_to_xml()

With the plots.xml file, we can now generate and view the plot. OpenMC outputs plots in .ppm format, which can be converted into a compressed format like .png with the convert utility.

In [15]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[15]:
0
In [16]:
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png

# Display the materials plot inline
Image(filename='materials-xy.png')
Out[16]:

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.

In [17]:
# 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 fuel assembly geometry.

In [18]:
# Initialize an 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 "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.

In [19]:
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['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.

In [20]:
# 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.

In [21]:
# 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.

In [22]:
# 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.

In [23]:
# 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.

In [24]:
# Instantiate a tally Mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
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)
In [25]:
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
In [26]:
# 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 | 647bf77a57a3cc5cce24b39cb192e1b99f52e499
         Date/Time | 2017-02-27 14:21:38
    OpenMP Threads | 4

 ===========================================================================
 ========================>     INITIALIZATION     <=========================
 ===========================================================================

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U235.h5
 Reading U238 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/U238.h5
 Reading O16 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/O16.h5
 Reading H1 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/H1.h5
 Reading B10 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/B10.h5
 Reading Zr90 from
 /home/wbinventor/Documents/NSE-CRPG-Codes/openmc/data/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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.02684    1.02662 +/- 0.00889
       18/1    0.97234    1.01984 +/- 0.01026
       19/1    0.99754    1.01736 +/- 0.00938
       20/1    0.98964    1.01459 +/- 0.00884
       21/1    1.04140    1.01703 +/- 0.00836
       22/1    1.03854    1.01882 +/- 0.00784
       23/1    1.05917    1.02192 +/- 0.00785
       24/1    1.02413    1.02208 +/- 0.00727
       25/1    1.03113    1.02268 +/- 0.00679
       26/1    1.05113    1.02446 +/- 0.00660
       27/1    1.03252    1.02494 +/- 0.00622
       28/1    1.05196    1.02644 +/- 0.00605
       29/1    0.99663    1.02487 +/- 0.00593
       30/1    1.01820    1.02454 +/- 0.00564
       31/1    1.02753    1.02468 +/- 0.00537
       32/1    1.02162    1.02454 +/- 0.00512
       33/1    1.04083    1.02525 +/- 0.00494
       34/1    1.03335    1.02558 +/- 0.00474
       35/1    1.01304    1.02508 +/- 0.00458
       36/1    0.99299    1.02385 +/- 0.00457
       37/1    1.04936    1.02479 +/- 0.00450
       38/1    1.02856    1.02493 +/- 0.00433
       39/1    1.03706    1.02535 +/- 0.00420
       40/1    1.08118    1.02721 +/- 0.00447
       41/1    1.00149    1.02638 +/- 0.00440
       42/1    1.00233    1.02563 +/- 0.00433
       43/1    1.03023    1.02577 +/- 0.00419
       44/1    1.03230    1.02596 +/- 0.00407
       45/1    0.98123    1.02468 +/- 0.00416
       46/1    1.02126    1.02458 +/- 0.00404
       47/1    0.99772    1.02386 +/- 0.00400
       48/1    1.02773    1.02396 +/- 0.00389
       49/1    1.01690    1.02378 +/- 0.00379
       50/1    1.02890    1.02391 +/- 0.00370
 Creating state point statepoint.50.h5...

 ===========================================================================
 ======================>     SIMULATION FINISHED     <======================
 ===========================================================================


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

 Total time for initialization     =  3.4887E-01 seconds
   Reading cross sections          =  2.1990E-01 seconds
 Total time in simulation          =  3.2195E+01 seconds
   Time in transport only          =  3.1778E+01 seconds
   Time in inactive batches        =  1.9903E+00 seconds
   Time in active batches          =  3.0205E+01 seconds
   Time synchronizing fission bank =  5.9614E-03 seconds
     Sampling source sites         =  4.8344E-03 seconds
     SEND/RECV source sites        =  1.0392E-03 seconds
   Time accumulating tallies       =  1.5849E-03 seconds
 Total time for finalization       =  3.9664E-05 seconds
 Total time elapsed                =  3.2560E+01 seconds
 Calculation Rate (inactive)       =  12561.1 neutrons/second
 Calculation Rate (active)         =  3310.69 neutrons/second

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

 k-effective (Collision)     =  1.02621 +/-  0.00393
 k-effective (Track-length)  =  1.02391 +/-  0.00370
 k-effective (Absorption)    =  1.02077 +/-  0.00423
 Combined k-effective        =  1.02331 +/-  0.00353
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[26]:
0

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

In [27]:
# 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.

In [28]:
# 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.

In [29]:
# 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.

In [30]:
df = fuel_mgxs.get_pandas_dataframe()
df
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
Out[30]:
cell group in nuclide mean std. dev.
3 10000 1 U235 8.096764e-03 3.130177e-05
4 10000 1 U238 7.364515e-03 4.510564e-05
5 10000 1 O16 0.000000e+00 0.000000e+00
0 10000 2 U235 3.611153e-01 2.048312e-03
1 10000 2 U238 6.735070e-07 3.780177e-09
2 10000 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.

In [31]:
fuel_mgxs.print_xs()
Multi-Group XS
	Reaction Type  =	nu-fission
	Domain Type    =	cell
	Domain ID      =	10000
	Nuclide        =	U235
	Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:	8.10e-03 +/- 3.87e-01%
            Group 2 [0.0        - 0.625     eV]:	3.61e-01 +/- 5.67e-01%

	Nuclide        =	U238
	Cross Sections [cm^-1]:
            Group 1 [0.625      - 20000000.0eV]:	7.36e-03 +/- 6.12e-01%
            Group 2 [0.0        - 0.625     eV]:	6.74e-07 +/- 5.61e-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/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1506: 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:

In [32]:
# Store the cross section data in an "mgxs/mgxs.h5" HDF5 binary file
mgxs_lib.build_hdf5_store(filename='mgxs.h5', directory='mgxs')
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

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 module. This is illustrated as follows.

In [33]:
# Store a Library and its MGXS objects in a pickled binary file "mgxs/mgxs.pkl"
mgxs_lib.dump_to_file(filename='mgxs', directory='mgxs')
In [34]:
# 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.

In [35]:
# 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)
In [36]:
# 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()
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
Out[36]:
cell group in nuclide mean std. dev.
0 10000 1 U235 0.074393 0.000308
1 10000 1 U238 0.005982 0.000036
2 10000 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.

In [37]:
# 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.

In [38]:
# Load the library into the OpenMOC geometry
materials = load_openmc_mgxs_lib(mgxs_lib, openmoc_geometry)
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/wbinventor/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

We are now ready to run OpenMOC to verify our cross-sections from OpenMC.

In [39]:
# 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 ]  Importing ray tracing data from file...
[  NORMAL ]  Computing the eigenvalue...
[  NORMAL ]  Iteration 0:	k_eff = 0.823793	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.780554	res = 1.938E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.739678	res = 6.539E-02
[  NORMAL ]  Iteration 3:	k_eff = 0.711003	res = 5.285E-02
[  NORMAL ]  Iteration 4:	k_eff = 0.689738	res = 3.931E-02
[  NORMAL ]  Iteration 5:	k_eff = 0.675038	res = 3.014E-02
[  NORMAL ]  Iteration 6:	k_eff = 0.665753	res = 2.147E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.661013	res = 1.389E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.660052	res = 7.293E-03
[  NORMAL ]  Iteration 9:	k_eff = 0.662216	res = 2.057E-03
[  NORMAL ]  Iteration 10:	k_eff = 0.666942	res = 3.576E-03
[  NORMAL ]  Iteration 11:	k_eff = 0.673743	res = 7.278E-03
[  NORMAL ]  Iteration 12:	k_eff = 0.682202	res = 1.030E-02
[  NORMAL ]  Iteration 13:	k_eff = 0.691961	res = 1.264E-02
[  NORMAL ]  Iteration 14:	k_eff = 0.702715	res = 1.438E-02
[  NORMAL ]  Iteration 15:	k_eff = 0.714203	res = 1.561E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.726205	res = 1.641E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.738532	res = 1.686E-02
[  NORMAL ]  Iteration 18:	k_eff = 0.751030	res = 1.703E-02
[  NORMAL ]  Iteration 19:	k_eff = 0.763567	res = 1.697E-02
[  NORMAL ]  Iteration 20:	k_eff = 0.776034	res = 1.674E-02
[  NORMAL ]  Iteration 21:	k_eff = 0.788344	res = 1.637E-02
[  NORMAL ]  Iteration 22:	k_eff = 0.800423	res = 1.591E-02
[  NORMAL ]  Iteration 23:	k_eff = 0.812215	res = 1.536E-02
[  NORMAL ]  Iteration 24:	k_eff = 0.823673	res = 1.477E-02
[  NORMAL ]  Iteration 25:	k_eff = 0.834764	res = 1.415E-02
[  NORMAL ]  Iteration 26:	k_eff = 0.845462	res = 1.350E-02
[  NORMAL ]  Iteration 27:	k_eff = 0.855748	res = 1.285E-02
[  NORMAL ]  Iteration 28:	k_eff = 0.865611	res = 1.220E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.875045	res = 1.156E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.884048	res = 1.093E-02
[  NORMAL ]  Iteration 31:	k_eff = 0.892623	res = 1.032E-02
[  NORMAL ]  Iteration 32:	k_eff = 0.900775	res = 9.727E-03
[  NORMAL ]  Iteration 33:	k_eff = 0.908511	res = 9.158E-03
[  NORMAL ]  Iteration 34:	k_eff = 0.915841	res = 8.613E-03
[  NORMAL ]  Iteration 35:	k_eff = 0.922778	res = 8.092E-03
[  NORMAL ]  Iteration 36:	k_eff = 0.929332	res = 7.596E-03
[  NORMAL ]  Iteration 37:	k_eff = 0.935519	res = 7.124E-03
[  NORMAL ]  Iteration 38:	k_eff = 0.941351	res = 6.676E-03
[  NORMAL ]  Iteration 39:	k_eff = 0.946843	res = 6.253E-03
[  NORMAL ]  Iteration 40:	k_eff = 0.952011	res = 5.852E-03
[  NORMAL ]  Iteration 41:	k_eff = 0.956869	res = 5.474E-03
[  NORMAL ]  Iteration 42:	k_eff = 0.961431	res = 5.118E-03
[  NORMAL ]  Iteration 43:	k_eff = 0.965712	res = 4.783E-03
[  NORMAL ]  Iteration 44:	k_eff = 0.969727	res = 4.467E-03
[  NORMAL ]  Iteration 45:	k_eff = 0.973489	res = 4.170E-03
[  NORMAL ]  Iteration 46:	k_eff = 0.977013	res = 3.892E-03
[  NORMAL ]  Iteration 47:	k_eff = 0.980310	res = 3.631E-03
[  NORMAL ]  Iteration 48:	k_eff = 0.983394	res = 3.386E-03
[  NORMAL ]  Iteration 49:	k_eff = 0.986277	res = 3.156E-03
[  NORMAL ]  Iteration 50:	k_eff = 0.988971	res = 2.942E-03
[  NORMAL ]  Iteration 51:	k_eff = 0.991487	res = 2.740E-03
[  NORMAL ]  Iteration 52:	k_eff = 0.993835	res = 2.552E-03
[  NORMAL ]  Iteration 53:	k_eff = 0.996026	res = 2.376E-03
[  NORMAL ]  Iteration 54:	k_eff = 0.998069	res = 2.212E-03
[  NORMAL ]  Iteration 55:	k_eff = 0.999974	res = 2.059E-03
[  NORMAL ]  Iteration 56:	k_eff = 1.001749	res = 1.915E-03
[  NORMAL ]  Iteration 57:	k_eff = 1.003403	res = 1.782E-03
[  NORMAL ]  Iteration 58:	k_eff = 1.004943	res = 1.657E-03
[  NORMAL ]  Iteration 59:	k_eff = 1.006377	res = 1.540E-03
[  NORMAL ]  Iteration 60:	k_eff = 1.007711	res = 1.432E-03
[  NORMAL ]  Iteration 61:	k_eff = 1.008952	res = 1.331E-03
[  NORMAL ]  Iteration 62:	k_eff = 1.010107	res = 1.237E-03
[  NORMAL ]  Iteration 63:	k_eff = 1.011181	res = 1.149E-03
[  NORMAL ]  Iteration 64:	k_eff = 1.012179	res = 1.067E-03
[  NORMAL ]  Iteration 65:	k_eff = 1.013107	res = 9.909E-04
[  NORMAL ]  Iteration 66:	k_eff = 1.013969	res = 9.201E-04
[  NORMAL ]  Iteration 67:	k_eff = 1.014769	res = 8.542E-04
[  NORMAL ]  Iteration 68:	k_eff = 1.015513	res = 7.929E-04
[  NORMAL ]  Iteration 69:	k_eff = 1.016204	res = 7.358E-04
[  NORMAL ]  Iteration 70:	k_eff = 1.016845	res = 6.828E-04
[  NORMAL ]  Iteration 71:	k_eff = 1.017440	res = 6.335E-04
[  NORMAL ]  Iteration 72:	k_eff = 1.017993	res = 5.877E-04
[  NORMAL ]  Iteration 73:	k_eff = 1.018505	res = 5.451E-04
[  NORMAL ]  Iteration 74:	k_eff = 1.018980	res = 5.056E-04
[  NORMAL ]  Iteration 75:	k_eff = 1.019422	res = 4.688E-04
[  NORMAL ]  Iteration 76:	k_eff = 1.019831	res = 4.347E-04
[  NORMAL ]  Iteration 77:	k_eff = 1.020210	res = 4.030E-04
[  NORMAL ]  Iteration 78:	k_eff = 1.020562	res = 3.736E-04
[  NORMAL ]  Iteration 79:	k_eff = 1.020888	res = 3.463E-04
[  NORMAL ]  Iteration 80:	k_eff = 1.021190	res = 3.209E-04
[  NORMAL ]  Iteration 81:	k_eff = 1.021471	res = 2.974E-04
[  NORMAL ]  Iteration 82:	k_eff = 1.021730	res = 2.756E-04
[  NORMAL ]  Iteration 83:	k_eff = 1.021971	res = 2.553E-04
[  NORMAL ]  Iteration 84:	k_eff = 1.022194	res = 2.365E-04
[  NORMAL ]  Iteration 85:	k_eff = 1.022400	res = 2.191E-04
[  NORMAL ]  Iteration 86:	k_eff = 1.022592	res = 2.030E-04
[  NORMAL ]  Iteration 87:	k_eff = 1.022769	res = 1.880E-04
[  NORMAL ]  Iteration 88:	k_eff = 1.022933	res = 1.741E-04
[  NORMAL ]  Iteration 89:	k_eff = 1.023085	res = 1.612E-04
[  NORMAL ]  Iteration 90:	k_eff = 1.023226	res = 1.493E-04
[  NORMAL ]  Iteration 91:	k_eff = 1.023356	res = 1.382E-04
[  NORMAL ]  Iteration 92:	k_eff = 1.023477	res = 1.279E-04
[  NORMAL ]  Iteration 93:	k_eff = 1.023588	res = 1.184E-04
[  NORMAL ]  Iteration 94:	k_eff = 1.023692	res = 1.097E-04
[  NORMAL ]  Iteration 95:	k_eff = 1.023788	res = 1.015E-04
[  NORMAL ]  Iteration 96:	k_eff = 1.023876	res = 9.392E-05
[  NORMAL ]  Iteration 97:	k_eff = 1.023958	res = 8.694E-05
[  NORMAL ]  Iteration 98:	k_eff = 1.024034	res = 8.045E-05
[  NORMAL ]  Iteration 99:	k_eff = 1.024104	res = 7.446E-05
[  NORMAL ]  Iteration 100:	k_eff = 1.024169	res = 6.890E-05
[  NORMAL ]  Iteration 101:	k_eff = 1.024229	res = 6.374E-05
[  NORMAL ]  Iteration 102:	k_eff = 1.024285	res = 5.897E-05
[  NORMAL ]  Iteration 103:	k_eff = 1.024336	res = 5.457E-05
[  NORMAL ]  Iteration 104:	k_eff = 1.024384	res = 5.049E-05
[  NORMAL ]  Iteration 105:	k_eff = 1.024428	res = 4.672E-05
[  NORMAL ]  Iteration 106:	k_eff = 1.024469	res = 4.321E-05
[  NORMAL ]  Iteration 107:	k_eff = 1.024506	res = 3.994E-05
[  NORMAL ]  Iteration 108:	k_eff = 1.024541	res = 3.696E-05
[  NORMAL ]  Iteration 109:	k_eff = 1.024574	res = 3.418E-05
[  NORMAL ]  Iteration 110:	k_eff = 1.024603	res = 3.164E-05
[  NORMAL ]  Iteration 111:	k_eff = 1.024631	res = 2.925E-05
[  NORMAL ]  Iteration 112:	k_eff = 1.024657	res = 2.705E-05
[  NORMAL ]  Iteration 113:	k_eff = 1.024680	res = 2.501E-05
[  NORMAL ]  Iteration 114:	k_eff = 1.024702	res = 2.313E-05
[  NORMAL ]  Iteration 115:	k_eff = 1.024722	res = 2.140E-05
[  NORMAL ]  Iteration 116:	k_eff = 1.024741	res = 1.981E-05
[  NORMAL ]  Iteration 117:	k_eff = 1.024758	res = 1.827E-05
[  NORMAL ]  Iteration 118:	k_eff = 1.024774	res = 1.690E-05
[  NORMAL ]  Iteration 119:	k_eff = 1.024789	res = 1.564E-05
[  NORMAL ]  Iteration 120:	k_eff = 1.024802	res = 1.445E-05
[  NORMAL ]  Iteration 121:	k_eff = 1.024815	res = 1.338E-05
[  NORMAL ]  Iteration 122:	k_eff = 1.024827	res = 1.236E-05
[  NORMAL ]  Iteration 123:	k_eff = 1.024837	res = 1.142E-05
[  NORMAL ]  Iteration 124:	k_eff = 1.024847	res = 1.057E-05

We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.

In [40]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined[0]
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.023307
openmoc keff = 1.024847
bias [pcm]: 154.0

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.

In [41]:
# 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)

Next, we extract OpenMOC's volume-averaged fission rates into a 2D 17x17 NumPy array.

In [42]:
# 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)

Now we can easily use Matplotlib to visualize the fission rates from OpenMC and OpenMOC side-by-side.

In [43]:
# 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')
Out[43]:
<matplotlib.text.Text at 0x7f3a95649710>

Multi-Group (Delayed) Cross Section Generation Part I: Introduction

mdgxs-part-i

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.

In [1]:
from IPython.display import Image
Image(filename='images/mdgxs.png', width=350)
Out[1]:

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

In [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. Before defining a material, we must create nuclides that are used in the material.

In [3]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
pu239 = openmc.Nuclide('Pu239')
zr90 = openmc.Nuclide('Zr90')

With the nuclides we defined, we will now create a material for the homogeneous medium.

In [4]:
# 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.

In [5]:
# Instantiate a Materials collection and export to XML
materials_file = openmc.Materials([inf_medium])
materials_file.default_xs = '71c'
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.

In [6]:
# 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.

In [7]:
# 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.

In [8]:
# Instantiate Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(cell)

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

In [9]:
# Create Geometry and set root Universe
openmc_geometry = openmc.Geometry()
openmc_geometry.root_universe = 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.

In [10]:
# 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.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.

In [11]:
# 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.

In [12]:
# 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.

In [13]:
decay_rate.tallies
Out[13]:
OrderedDict([('delayed-nu-fission', Tally
              	ID             =	10000
              	Name           =	
              	Filters        =	CellFilter, DelayedGroupFilter, EnergyFilter
              	Nuclides       =	U235 Pu239 
              	Scores         =	['delayed-nu-fission']
              	Estimator      =	tracklength), ('decay-rate', Tally
              	ID             =	10001
              	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.

In [14]:
# 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()

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

In [15]:
# 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 | 5e313cf5f1d601074ad95c17ae589bf564972adb
         Date/Time | 2017-02-26 06:05:10
    OpenMP Threads | 8

 ===========================================================================
 ========================>     INITIALIZATION     <=========================
 ===========================================================================

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading H1 from /opt/xsdata/nndc/H1.h5
 Reading O16 from /opt/xsdata/nndc/O16.h5
 Reading U235 from /opt/xsdata/nndc/U235.h5
 Reading U238 from /opt/xsdata/nndc/U238.h5
 Reading Pu239 from /opt/xsdata/nndc/Pu239.h5
 Reading Zr90 from /opt/xsdata/nndc/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for H1
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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...

 ===========================================================================
 ======================>     SIMULATION FINISHED     <======================
 ===========================================================================


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

 Total time for initialization     =  3.8846E-01 seconds
   Reading cross sections          =  3.0221E-01 seconds
 Total time in simulation          =  1.2666E+01 seconds
   Time in transport only          =  1.2196E+01 seconds
   Time in inactive batches        =  5.1652E-01 seconds
   Time in active batches          =  1.2150E+01 seconds
   Time synchronizing fission bank =  5.1914E-03 seconds
     Sampling source sites         =  3.6297E-03 seconds
     SEND/RECV source sites        =  1.5222E-03 seconds
   Time accumulating tallies       =  5.2027E-04 seconds
 Total time for finalization       =  4.8293E-02 seconds
 Total time elapsed                =  1.3117E+01 seconds
 Calculation Rate (inactive)       =  96801.2 neutrons/second
 Calculation Rate (active)         =  16461.5 neutrons/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

Out[15]:
0

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

In [16]:
# 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.

In [17]:
# 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.

In [18]:
delayed_nu_fission.get_condensed_xs(one_group).get_xs()
Out[18]:
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.

In [19]:
df = delayed_nu_fission.get_pandas_dataframe()
df.head(10)
Out[19]:
cell delayedgroup group in nuclide mean std. dev.
198 1 1 1 U235 9.534320e-11 4.789291e-11
199 1 1 1 Pu239 1.606580e-11 8.071486e-12
398 1 2 1 U235 1.224152e-09 6.149552e-10
399 1 2 1 Pu239 2.602562e-10 1.307612e-10
598 1 3 1 U235 9.032969e-10 4.537585e-10
599 1 3 1 Pu239 1.522290e-10 7.648238e-11
798 1 4 1 U235 1.749268e-09 8.787082e-10
799 1 4 1 Pu239 2.400495e-10 1.206032e-10
998 1 5 1 U235 2.724017e-10 1.368376e-10
999 1 5 1 Pu239 4.749191e-11 2.386080e-11
In [20]:
df = decay_rate.get_pandas_dataframe()
df.head(12)
Out[20]:
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.

In [21]:
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.

In [22]:
chi_prompt.build_hdf5_store(filename='mdgxs', append=True)
chi_delayed.build_hdf5_store(filename='mdgxs', append=True)
/home/nelsonag/git/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']

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.

In [23]:
# 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))
Out[23]:
<matplotlib.legend.Legend at 0x7fcb1e0dd588>

Now let's compute the initial concentration of the delayed neutron precursors:

In [24]:
# 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()
Out[24]:
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.

In [25]:
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
Out[25]:
(0, 7)

We can also plot the energy spectrum for fission emission of prompt and delayed neutrons.

In [26]:
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)
Out[26]:
(1000.0, 20000000.0)

Multi-Group (Delayed) Cross Section Generation Part II: Advanced Features

mdgxs-part-ii

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

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

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

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

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

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

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

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

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

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

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

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

In [12]:
# Plot our geometry
plot = openmc.Plot.from_geometry(geometry)
plot.pixels = (250, 250)
plot.color_by = 'material'
openmc.plot_inline(plot)

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.

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

In [14]:
# Instantiate a tally mesh                                                                                                                                                                  
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
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.MeshFilter(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()

Now, we can run OpenMC to generate the cross sections.

In [15]:
# 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 | f7edad68f0654d775ed363bfdcbe4aa5d3cfba23
         Date/Time | 2017-04-03 21:15:56

 Reading settings XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading cross sections XML file...
 Reading U235 from /home/romano/openmc/scripts/nndc_hdf5/U235.h5
 Reading U238 from /home/romano/openmc/scripts/nndc_hdf5/U238.h5
 Reading O16 from /home/romano/openmc/scripts/nndc_hdf5/O16.h5
 Reading H1 from /home/romano/openmc/scripts/nndc_hdf5/H1.h5
 Reading B10 from /home/romano/openmc/scripts/nndc_hdf5/B10.h5
 Reading Zr90 from /home/romano/openmc/scripts/nndc_hdf5/Zr90.h5
 Maximum neutron transport energy: 2.00000E+07 eV for U235
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 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.02684    1.02662 +/- 0.00889
       18/1    0.97234    1.01984 +/- 0.01026
       19/1    0.99754    1.01736 +/- 0.00938
       20/1    0.98964    1.01459 +/- 0.00884
       21/1    1.04140    1.01703 +/- 0.00836
       22/1    1.03854    1.01882 +/- 0.00784
       23/1    1.05917    1.02192 +/- 0.00785
       24/1    1.02413    1.02208 +/- 0.00727
       25/1    1.03113    1.02268 +/- 0.00679
       26/1    1.05113    1.02446 +/- 0.00660
       27/1    1.03252    1.02494 +/- 0.00622
       28/1    1.05196    1.02644 +/- 0.00605
       29/1    0.99663    1.02487 +/- 0.00593
       30/1    1.01820    1.02454 +/- 0.00564
       31/1    1.02753    1.02468 +/- 0.00537
       32/1    1.02162    1.02454 +/- 0.00512
       33/1    1.04083    1.02525 +/- 0.00494
       34/1    1.03335    1.02558 +/- 0.00474
       35/1    1.01304    1.02508 +/- 0.00458
       36/1    0.99299    1.02385 +/- 0.00457
       37/1    1.04936    1.02479 +/- 0.00450
       38/1    1.02856    1.02493 +/- 0.00433
       39/1    1.03706    1.02535 +/- 0.00420
       40/1    1.08118    1.02721 +/- 0.00447
       41/1    1.00149    1.02638 +/- 0.00440
       42/1    1.00233    1.02563 +/- 0.00433
       43/1    1.03023    1.02577 +/- 0.00419
       44/1    1.03230    1.02596 +/- 0.00407
       45/1    0.98123    1.02468 +/- 0.00416
       46/1    1.02126    1.02458 +/- 0.00404
       47/1    0.99772    1.02386 +/- 0.00400
       48/1    1.02773    1.02396 +/- 0.00389
       49/1    1.01690    1.02378 +/- 0.00379
       50/1    1.02890    1.02391 +/- 0.00370
 Creating state point statepoint.50.h5...

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

 Total time for initialization     =  3.7616E-01 seconds
   Reading cross sections          =  3.2363E-01 seconds
 Total time in simulation          =  5.8159E+01 seconds
   Time in transport only          =  5.7959E+01 seconds
   Time in inactive batches        =  3.9349E+00 seconds
   Time in active batches          =  5.4224E+01 seconds
   Time synchronizing fission bank =  3.6903E-03 seconds
     Sampling source sites         =  2.4990E-03 seconds
     SEND/RECV source sites        =  1.1328E-03 seconds
   Time accumulating tallies       =  1.7598E-01 seconds
 Total time for finalization       =  3.5126E-03 seconds
 Total time elapsed                =  5.8551E+01 seconds
 Calculation Rate (inactive)       =  6353.42 neutrons/second
 Calculation Rate (active)         =  1844.20 neutrons/second

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

 k-effective (Collision)     =  1.02621 +/-  0.00393
 k-effective (Track-length)  =  1.02391 +/-  0.00370
 k-effective (Absorption)    =  1.02077 +/-  0.00423
 Combined k-effective        =  1.02331 +/-  0.00353
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[15]:
0

Tally Data Processing

Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint object.

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

In [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) $$
In [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)
/home/romano/openmc/openmc/tallies.py:1875: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/romano/openmc/openmc/tallies.py:1876: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/romano/openmc/openmc/tallies.py:1877: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']
/home/romano/openmc/openmc/tallies.py:1869: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/romano/openmc/openmc/tallies.py:1870: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
Out[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.011301 0.003184
1 1 1 1 2 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000854 0.000078
2 1 1 1 3 total (((delayed-nu-fission / nu-fission) * (delayed... 0.007267 0.000713
3 1 1 1 4 total (((delayed-nu-fission / nu-fission) * (delayed... 0.088903 0.005501
4 1 1 1 5 total (((delayed-nu-fission / nu-fission) * (delayed... 0.044257 0.002890
5 1 1 1 6 total (((delayed-nu-fission / nu-fission) * (delayed... 0.001897 0.000282
6 1 2 1 1 total (((delayed-nu-fission / nu-fission) * (delayed... 0.007362 0.001968
7 1 2 1 2 total (((delayed-nu-fission / nu-fission) * (delayed... 0.000872 0.000064
8 1 2 1 3 total (((delayed-nu-fission / nu-fission) * (delayed... 0.007015 0.000702
9 1 2 1 4 total (((delayed-nu-fission / nu-fission) * (delayed... 0.101115 0.007008

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.

In [19]:
current_tally.get_pandas_dataframe().head(10)
Out[19]:
mesh 1 surface nuclide score mean std. dev.
x y z
0 1 1 1 x-min out total current 0.00000 0.000000
1 1 1 1 x-max out total current 0.00000 0.000000
2 1 1 1 y-min out total current 0.03153 0.000566
3 1 1 1 y-max out total current 0.03153 0.000593
4 1 1 1 z-min out total current 0.00000 0.000000
5 1 1 1 z-max out total current 0.00000 0.000000
6 1 1 1 x-min in total current 0.03265 0.000753
7 1 1 1 x-max in total current 0.03216 0.000698
8 1 1 1 y-min in total current 0.00000 0.000000
9 1 1 1 y-max 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.

In [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')
/home/romano/openmc/openmc/tallies.py:1875: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/romano/openmc/openmc/tallies.py:1876: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/romano/openmc/openmc/tallies.py:1877: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']
Out[20]:
<matplotlib.text.Text at 0x7fecd32c1da0>

Multi-Group Mode

Multi-Group Mode Part I: Introduction

mg-mode-part-i

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

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

In [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).

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

In [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 openmc.Nuclides or openmc.Elements were created 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 openmc.Macroscopic objects to in-place of openmc.Nuclide or openmc.Element objects.

openmc.Macroscopic, unlike openmc.Nuclide and openmc.Element objects, do not need to be provided enough information to calculate number densities, as no number densities are needed.

When assigning openmc.Macroscopic objects to openmc.Material objects, 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 an openmc.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.

In [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

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

In [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

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

In [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

In [10]:
root_universe.plot(origin=(0., 0., 0.), width=(3 * 21.42, 3 * 21.42), pixels=(500, 500),
                   color_by='material')

OK, it looks pretty good, let's go ahead and write the file

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

In [12]:
tallies_file = openmc.Tallies()

# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
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!

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

In [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

Out[14]:
0

Results Visualization

Now that we have run the simulation, let's look at the fission rate and flux tallies that we tallied.

In [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)

# 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()

There we have it! We have just successfully run the C5G7 benchmark model!

Multi-Group Mode Part II: MGXS Library Generation With OpenMC

mg-mode-part-ii

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

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

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

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

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

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

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

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

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

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

In [10]:
root_universe.plot(origin=(0., 0., 0.), width=(21.42, 21.42), pixels=(500, 500), color_by='material')

Looks good!

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

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

In [12]:
# OpenMC simulation parameters
batches = 600
inactive = 50
particles = 2000

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

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

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

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

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

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

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

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

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

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

In [22]:
# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
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()

Time to run the calculation and get our results!

In [23]:
# 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 | 4b01fd311461f1350989cb84ec18fe2cbaa8fa9f
         Date/Time | 2017-03-10 17:30:51
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  1.16584 +/-  0.00111
 k-effective (Track-length)  =  1.16532 +/-  0.00131
 k-effective (Absorption)    =  1.16513 +/-  0.00100
 Combined k-effective        =  1.16538 +/-  0.00086
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[23]:
0

To make sure the results we need are available after running the multi-group calculation, we will now rename the statepoint and summary files.

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

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

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

In [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()
/home/nelsonag/git/openmc/openmc/tallies.py:1834: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

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.

In [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')
fuel_mg.add_macroscopic('fuel')

# cladding
zircaloy_mg = openmc.Material(name='Clad')
zircaloy_mg.add_macroscopic('zircaloy')

# moderator
water_mg = openmc.Material(name='Water')
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()

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.

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

In [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.add_tally(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.

In [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()

At this point, the problem is set up and we can run the multi-group calculation.

In [32]:
# Run the Multi-Group OpenMC Simulation
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 | 4b01fd311461f1350989cb84ec18fe2cbaa8fa9f
         Date/Time | 2017-03-10 17:31:49
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  1.16235 +/-  0.00111
 k-effective (Track-length)  =  1.16345 +/-  0.00134
 k-effective (Absorption)    =  1.16397 +/-  0.00058
 Combined k-effective        =  1.16388 +/-  0.00058
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[32]:
0

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.

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

In [34]:
ce_keff = sp.k_combined

Lets compare the two eigenvalues, including their bias

In [35]:
bias = 1.0E5 * (ce_keff[0] - mg_keff[0])

print('Continuous-Energy keff = {0:1.6f}'.format(ce_keff[0]))
print('Multi-Group keff = {0:1.6f}'.format(mg_keff[0]))
print('bias [pcm]: {0:1.1f}'.format(bias))
Continuous-Energy keff = 1.165379
Multi-Group keff = 1.163885
bias [pcm]: 149.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.

In [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)

We can now do the same for the Continuous-Energy results.

In [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)

Now we can easily use Matplotlib to visualize the two fission rates side-by-side.

In [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')
Out[38]:
<matplotlib.text.Text at 0x7f353b227780>

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.

In [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

In [40]:
# Run the Multi-Group OpenMC Simulation
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 | 4b01fd311461f1350989cb84ec18fe2cbaa8fa9f
         Date/Time | 2017-03-10 17:32:18
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  1.16104 +/-  0.00109
 k-effective (Track-length)  =  1.16004 +/-  0.00125
 k-effective (Absorption)    =  1.16297 +/-  0.00061
 Combined k-effective        =  1.16273 +/-  0.00061
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[40]:
0

And then get the eigenvalue differences from the Continuous-Energy and P3 MG solution

In [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[0] - mg_p0_keff[0])

print('P3 bias [pcm]: {0:1.1f}'.format(bias))
print('P0 bias [pcm]: {0:1.1f}'.format(bias_p0))
P3 bias [pcm]: 149.4
P0 bias [pcm]: 265.3

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.

In [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

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

In [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-2017 Massachusetts Institute of Technology
           License | http://openmc.readthedocs.io/en/latest/license.html
           Version | 0.8.0
          Git SHA1 | 4b01fd311461f1350989cb84ec18fe2cbaa8fa9f
         Date/Time | 2017-03-10 17:32:48
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  1.16348 +/-  0.00117
 k-effective (Track-length)  =  1.16263 +/-  0.00133
 k-effective (Absorption)    =  1.16485 +/-  0.00063
 Combined k-effective        =  1.16459 +/-  0.00061
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[44]:
0

For a final step we can again obtain the eigenvalue differences from this case and compare with the same from the P3 MG solution

In [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[0] - mg_mixed_keff[0])

print('P3 bias [pcm]: {0:1.1f}'.format(bias))
print('Mixed Scattering bias [pcm]: {0:1.1f}'.format(bias_mixed))
P3 bias [pcm]: 149.4
Mixed Scattering bias [pcm]: 79.0

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.

Multi-Group Mode Part III: Advanced Feature Showcase

mg-mode-part-iii

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

In [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 create all the elemental data we would need for this case.

In [2]:
# Instantiate some elements
elements = {}
for elem in ['H', 'O', 'U', 'Zr', 'Gd', 'B', 'C', 'Fe']:
    elements[elem] = openmc.Element(elem)

With the elements we defined, we will now create the materials 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.
In [3]:
materials = {}

# Fuel
materials['Fuel'] = openmc.Material(name='Fuel')
materials['Fuel'].set_density('g/cm3', 10.32)
materials['Fuel'].add_element(elements['O'], 2)
materials['Fuel'].add_element(elements['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(elements['O'], 2)
materials['Gad'].add_element(elements['U'], 1, enrichment=3.)
materials['Gad'].add_element(elements['Gd'], .02)

# Zircaloy
materials['Zirc2'] = openmc.Material(name='Zirc2')
materials['Zirc2'].set_density('g/cm3', 6.55)
materials['Zirc2'].add_element(elements['Zr'], 1)

# Boiling Water
materials['Water'] = openmc.Material(name='Water')
materials['Water'].set_density('g/cm3', 0.6)
materials['Water'].add_element(elements['H'], 2)
materials['Water'].add_element(elements['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(elements['B'], 4)
materials['B4C'].add_element(elements['C'], 1)

# Steel 
materials['Steel'] = openmc.Material(name='Steel')
materials['Steel'].set_density('g/cm3', 7.75)
materials['Steel'].add_element(elements['Fe'], 1)

We can now create a Materials object that can be exported to an actual XML file.

In [4]:
# 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
In [5]:
# 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(x0=0., boundary_type='reflective')
surfaces['Global x+'] = openmc.XPlane(x0=length, boundary_type='reflective')
surfaces['Global y-'] = openmc.YPlane(y0=0., boundary_type='reflective')
surfaces['Global y+'] = openmc.YPlane(y0=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(x0=pin_pitch)
surfaces['Assembly x+'] = openmc.XPlane(x0=length - pin_pitch)
surfaces['Assembly y-'] = openmc.YPlane(y0=pin_pitch)
surfaces['Assembly y+'] = openmc.YPlane(y0=length - pin_pitch)

# Set surfaces for the control blades
surfaces['Top Blade y-'] = openmc.YPlane(y0=length - rod_thick)
surfaces['Top Blade x-'] = openmc.XPlane(x0=pin_pitch)
surfaces['Top Blade x+'] = openmc.XPlane(x0=rod_span)
surfaces['Left Blade x+'] = openmc.XPlane(x0=rod_thick)
surfaces['Left Blade y-'] = openmc.YPlane(y0=length - rod_span)
surfaces['Left Blade y+'] = openmc.YPlane(y0=9. * pin_pitch)

With the surfaces defined, we can now construct regions with these surfaces before we use those to create cells

In [6]:
# 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).

In [7]:
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.

In [8]:
# 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

In [9]:
# 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.

In [10]:
# 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.

In [11]:
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)})

Looks pretty good to us!

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

In [12]:
# 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.

In [13]:
# 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.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.

In [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 our the problem geometry. This library will use the default setting of isotropically-weighting the multi-group cross sections.

In [15]:
# 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.

In [16]:
# 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.

In [17]:
# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
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.

In [18]:
# 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.

In [19]:
# 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.

In [20]:
# 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.

In [21]:
# Construct all tallies needed for the multi-group cross section library
iso_mgxs_lib.build_library()
angle_mgxs_lib.build_library()
/home/nelsonag/git/openmc/openmc/mgxs/mgxs.py:3801: 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.

In [22]:
# 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.

In [23]:
# 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()

Time to run the calculation and get our results!

In [24]:
# 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 09:18:01
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  0.83694 +/-  0.00098
 k-effective (Track-length)  =  0.83663 +/-  0.00116
 k-effective (Absorption)    =  0.83775 +/-  0.00102
 Combined k-effective        =  0.83724 +/-  0.00083
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[24]:
0

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.

In [25]:
# 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.

In [26]:
# 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.

In [27]:
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.

In [28]:
# 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.

In [29]:
# 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/nelsonag/git/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

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.

In [30]:
# 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

In [31]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()

# Add our fission rate mesh tally
tallies_file.add_tally(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.

In [32]:
geometry_file.root_universe.plot(origin=(length / 2., length / 2., 0.),
                       pixels=(300, 300), width=(length, length),
                       color_by='material')

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.

In [33]:
# Execute the Isotropic MG OpenMC Run
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 09:18:56
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  0.82605 +/-  0.00103
 k-effective (Track-length)  =  0.82596 +/-  0.00103
 k-effective (Absorption)    =  0.82503 +/-  0.00075
 Combined k-effective        =  0.82528 +/-  0.00067
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[33]:
0

Before we go the angle-dependent case, let's save the StatePoint and Summary files so they don't get over-written

In [34]:
# 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.

In [35]:
# 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/nelsonag/git/openmc/openmc/tallies.py:1835: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1836: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1837: RuntimeWarning: invalid value encountered in true_divide
  new_tally._mean = data['self']['mean'] / data['other']['mean']

At this point, the problem is set up and we can run the multi-group calculation.

In [36]:
# Execute the angle-dependent OpenMC Run
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 09:19:06
    OpenMP Threads | 8


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


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

 k-effective (Collision)     =  0.83752 +/-  0.00102
 k-effective (Track-length)  =  0.83708 +/-  0.00104
 k-effective (Absorption)    =  0.83678 +/-  0.00077
 Combined k-effective        =  0.83684 +/-  0.00068
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[36]:
0

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.

In [37]:
# 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

In [38]:
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[0] - iso_mg_keff[0])
angle_bias = 1.0E5 * (ce_keff[0] - angle_mg_keff[0])

Let's compare the eigenvalues in units of pcm

In [39]:
print('Isotropic to CE Bias [pcm]: {0:1.1f}'.format(iso_bias))
print('Angle to CE Bias [pcm]: {0:1.1f}'.format(angle_bias))
Isotropic to CE Bias [pcm]: 1195.9
Angle to CE Bias [pcm]: 40.4

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.

In [40]:
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])
    
    # 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')

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

In [41]:
# 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)
Out[41]:
<matplotlib.colorbar.Colorbar at 0x7f264c3a56a0>

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!

Release Notes for OpenMC 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

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:

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, the random samples a reaction for that nuclide 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.
  • All allocatable arrays are deallocated.

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)\[ \begin{align}\begin{aligned}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\end{aligned}\end{align} \]

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)\[\begin{split}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\end{split}\]

Expanding equation (17) and rearranging terms, we obtain

(18)\[\begin{split}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\end{split}\]

Defining the terms

(19)\[ \begin{align}\begin{aligned}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)\end{aligned}\end{align} \]

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)\[ \begin{align}\begin{aligned}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\end{aligned}\end{align} \]

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)\[ \begin{align}\begin{aligned}\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\end{aligned}\end{align} \]

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)\[\begin{split}x^2 + y^2 + z^2 - 10^2 < 0 \\ x - (-3) > 0 \\ y - 2 < 0\end{split}\]

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.

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)\[ \begin{align}\begin{aligned}\begin{split}u' = u - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial x} \\\end{split}\\\begin{split}v' = v - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial y} \\\end{split}\\w' = w - \frac{2 ( \mathbf{\Omega} \cdot \nabla f )}{|| \nabla f ||^2} \frac{\partial f}{\partial z}\end{aligned}\end{align} \]

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)\[\begin{split}\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 )\end{split}\]

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)\[ \begin{align}\begin{aligned}\begin{split}v' = v - \frac{2 ( \bar{y}v + \bar{z}w ) \bar{y}}{R^2} \\\end{split}\\w' = w - \frac{2 ( \bar{y}v + \bar{z}w ) \bar{z}}{R^2}\end{aligned}\end{align} \]
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)\[\begin{split}\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 )\end{split}\]

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)\[ \begin{align}\begin{aligned}\begin{split}u' = u - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{x} }{R^2} \\\end{split}\\\begin{split}v' = v - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{y} }{R^2} \\\end{split}\\w' = w - \frac{2 ( \bar{x}u + \bar{y}v + \bar{z}w ) \bar{z} }{R^2}\end{aligned}\end{align} \]
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)\[\begin{split}\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 )\end{split}\]

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)\[\begin{split}|| \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\end{split}\]

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)\[ \begin{align}\begin{aligned}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}}\end{aligned}\end{align} \]
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)\[\begin{split}\nabla f = \left ( \begin{array}{c} 2Ax + Dy + Fz + G \\ 2By + Dx + Ez + H \\ 2Cz + Ey + Fx + J \end{array} \right ).\end{split}\]

Cross Section Representations

Continuous-Energy Data

The data governing the interaction of neutrons with various nuclei for continous-energy problems are represented using 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. The use of a standard cross section format allows for a direct comparison of OpenMC with other codes since the same cross section libraries can be used.

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 an experimental 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[i 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.

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 the Multi-Group Mode Part I: Introduction 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 (2) has the same general form as eqref{eq:lcg}, so the idea is to determine the new multiplicative and additive constants in \(O(\log_2 N)\) operations.

References

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.

No secondary particles from disappearance reactions such as photons or alpha-particles are produced or tracked. To truly capture the affects of gamma heating in a problem, it would be necessary to explicitly track photons originating from \((n,\gamma)\) and other reactions.

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

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'=0}^{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 forthe 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 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)\[\begin{split}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} )\end{split}\]

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)\[\begin{split}R = R_{\ell,j} \\ A = A_{\ell,j}.\end{split}\]

If linear-linear interpolation is specified, the parameters are

(43)\[\begin{split}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} )\end{split}\]

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)\[ \begin{align}\begin{aligned}\begin{split}u' = \mu u + \frac{\sqrt{1 - \mu^2} ( uw \cos\phi - v \sin\phi )}{\sqrt{1 - w^2}} \\\end{split}\\\begin{split}v' = \mu v + \frac{\sqrt{1 - \mu^2} ( vw \cos\phi + u \sin\phi )}{\sqrt{1 - w^2}} \\\end{split}\\w' = \mu w - \sqrt{1 - \mu^2} \sqrt{1 - w^2} \cos\phi.\end{aligned}\end{align} \]

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{align}\begin{aligned}\begin{split}p(v_T, \mu) &= f_1(v_T, \mu) f_2(v_T) \\\end{split}\\\begin{split}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} \\\end{split}\\f_2(v_T) &= (v_n + v_T) \beta^3 v_T^2 \exp \left ( -\beta^2 v_T^2 \right ).\end{aligned}\end{align} \]

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)\[\begin{split}x = \beta v_T \\ y = \beta v_n.\end{split}\]

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 are described fully in Walsh et al.

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 elastic and inelastic scattering, the cross sections are stored as linearly interpolable functions on a specified energy grid. For coherent elastic data, the cross section can be expressed as

(78)\[\sigma(E) = \frac{\sigma_c}{E} \sum_{E_i < E} f_i e^{-4WE_i}\]

where \(\sigma_c\) is the effective bound coherent scattering cross section, \(W\) is the effective Debye-Waller coefficient, \(E_i\) are the energies of the Bragg edges, and \(f_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).

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

(79)\[\frac{f_i e^{-4WE_i}}{\sum_j f_j e^{-4WE_j}}.\]

After a Bragg edge has been sampled, the cosine of the angle of scattering is given analytically by

(80)\[\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, the probability distribution for the cosine of the angle of scattering is represent 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 final cosine is

(81)\[\mu = \mu_{i,j} + f (\mu_{i+1,j} - \mu_{i,j})\]

where the interpolation factor is defined as

(82)\[f = \frac{E - E_i}{E_{i+1} - E_i}.\]
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 (82). Then, an outgoing energy bin is sampled from a uniform distribution and then interpolated between values corresponding to neighboring incoming energies:

(83)\[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:

(84)\[\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 (84).

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

(85)\[\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 (82). If logarithmic interpolation is specified, the cross sections are calculated as

(86)\[\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

(87)\[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.

(88)\[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

(89)\[\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.

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

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 9: 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 10: 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 11: 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 MPICH2 implementation. For short messages, MPICH2 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 MPICH2. 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)\[\begin{split}\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.\end{split}\]

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)\[\begin{split}\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.\end{split}\]

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)\[\begin{split}\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.\end{split}\]

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 CMFD Specification – cmfd.xml.

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, http://crpg.mit.edu/pub/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 only neutrons) 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 arbitrarily 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. conda-forge is a community-led conda channel of installable packages. For instructions on installing conda, please consult their documentation.

Once you have conda installed on your system, add the conda-forge channel to your configuration with:

conda config --add channels conda-forge

Once the conda-forge channel has been enabled, OpenMC can then be installed with:

conda install openmc

It is possible to list all of the versions of OpenMC available on your platform with:

conda search openmc --channel conda-forge

Installing on Ubuntu with PPA

For users with Ubuntu 15.04 or later, a binary package for OpenMC is available through a Personal Package Archive (PPA) and can be installed through the APT package manager. First, add the following PPA to the repository sources:

sudo apt-add-repository ppa:paulromano/staging

Next, resynchronize the package index files:

sudo apt update

Now OpenMC should be recognized within the repository and can be installed:

sudo apt install openmc

Binary packages from this PPA may exist for earlier versions of Ubuntu, but they are no longer supported.

Building from Source

Prerequisites

Required

  • A Fortran compiler such as gfortran

    In order to compile OpenMC, you will need to have a Fortran compiler installed on your machine. Since a number of Fortran 2003/2008 features are used in the code, it is recommended that you use the latest version of whatever compiler you choose. For gfortran, it is necessary to use version 4.8.0 or above.

    If you are using Debian or a Debian derivative such as Ubuntu, you can install the gfortran compiler using the following command:

    sudo apt install gfortran
    
  • A C/C++ compiler such as gcc

    OpenMC includes two libraries written in C and C++, respectively. These libraries 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 binary 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 you are using HDF5 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:

    FC=/opt/mpich/3.1/bin/mpif90 CC=/opt/mpich/3.1/bin/mpicc \
    ./configure --prefix=/opt/hdf5/1.8.12 --enable-fortran \
                --enable-fortran2003 --enable-parallel
    

    You may omit --enable-parallel if you want to compile HDF5 in serial.

    Important

    OpenMC uses various parts of the HDF5 Fortran 2003 API; as such you must include --enable-fortran2003 or else OpenMC will not be able to compile.

    On Debian derivatives, HDF5 and/or parallel HDF5 can be installed through the APT package manager:

    sudo apt install libhdf5-dev hdf5-helpers
    

    Note that the exact package names may vary depending on your particular distribution and version.

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
    
  • 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 https://github.com/mit-crpg/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 gfortran and Intel Fortran, this compiles with -O3.
openmp
Enables shared-memory parallelism using the OpenMP API. The Fortran compiler being used must support OpenMP. (Default: on)
coverage
Compile and link code instrumented for coverage analysis. This is typically used in conjunction with gcov.
maxcoord
Maximum number of nested coordinate levels in geometry. Defaults to 10.

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 FC and CC environment variables to the path to the MPI Fortran and C wrappers, respectively. For example, in a bash shell:

export FC=mpif90
export CC=mpicc
cmake /path/to/openmc

Note that in many shells, environment variables can be set for a single command, i.e.

FC=mpif90 CC=mpicc cmake /path/to/openmc
Selecting HDF5 Installation

CMakeLists.txt searches for the h5fc or h5pfc HDF5 Fortran 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 h5fc / h5pfc 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.

Compiling for the Intel Xeon Phi

For the second generation Knights Landing architecture, nothing special is required to compile OpenMC. You may wish to experiment with compiler flags that control generation of vector instructions to see what configuration gives optimal performance for your target problem.

For the first generation Knights Corner architecture, it is necessary to cross-compile OpenMC. If you are using the Intel Fortran compiler, it is necessary to specify that all objects be compiled with the -mmic flag as follows:

mkdir build && cd build
FC=ifort CC=icc FFLAGS=-mmic cmake -Dopenmp=on ..
make

Note that unless an HDF5 build for the Intel Xeon Phi (Knights Corner) is already on your target machine, you will need to cross-compile HDF5 for the Xeon Phi. An example script to build zlib and HDF5 provides several necessary workarounds.

Testing Build

If you have ENDF/B-VII.1 cross sections from NNDC you can test your build. Make sure the OPENMC_CROSS_SECTIONS environmental variable is set to the cross_sections.xml file in the data/nndc directory. There are two ways to run tests. The first is to use the Makefile present in the source directory and run the following:

make test

If you want more options for testing you can use ctest command. For example, if we wanted to run only the plot tests with 4 processors, we run:

cd build
ctest -j 4 -R plot

If you want to run the full test suite with different build options please refer to our OpenMC Test Suite documentation.

Python Prerequisites

OpenMC’s Python API works with either Python 2.7 or Python 3.2+. 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

six
The Python API works with both Python 2.7+ and 3.2+. To do so, the six compatibility library is used.
NumPy
NumPy is used extensively within the Python API for its powerful N-dimensional array.
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.

Optional

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 Pandas Dataframes example notebook.
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 optionally used for decay data in the openmc.data module.
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.
silomesh
The silomesh package is needed to convert voxel and track files to SILO format.
lxml
lxml is used for the openmc-validate-xml script.

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 Format) to store all nuclear data. 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 openmc-ace-to-hdf5 script (or using the Python API). Several sources provide openly available ACE data as described below and can be easily converted using the provided scripts. The TALYS-based evaluated nuclear data library, TENDL, is also available in ACE format. 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 class (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 Materials.cross_sections (equivalently, the <cross_sections> Element in materials.xml).
OPENMC_MULTIPOLE_LIBRARY
Indicates the path to a directory containing windowed multipole data if the user has not specified Materials.multipole_library (equivalently, the <multipole_library> Element in materials.xml)
OPENMC_MG_CROSS_SECTIONS
Indicates the path to the an HDF5 file that contains multi-group cross sections if the user has not specified 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 ENDF/B-VII.1 Cross Sections from NNDC

The NNDC provides ACE data from the ENDF/B-VII.1 neutron and thermal scattering sublibraries at room temperature processed using NJOY. To use this data with OpenMC, the openmc-get-nndc-data script can be used to automatically download and extract the ACE data, fix any deficiencies, and create an HDF5 library:

openmc-get-nndc-data

At this point, you should set the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the file nndc_hdf5/cross_sections.xml. This cross section set is used by the test suite.

Using JEFF Cross Sections from OECD/NEA

The NEA provides processed ACE data from the JEFF library. To use this data with OpenMC, the openmc-get-jeff-data script can be used to automatically download and extract the ACE data, fix any deficiencies, and create an HDF5 library.

openmc-get-jeff-data

At this point, you should set the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the file jeff-3.2-hdf5/cross_sections.xml.

Using Cross Sections from MCNP

OpenMC provides two scripts (openmc-convert-mcnp70-data and openmc-convert-mcnp71-data) that will automatically convert ENDF/B-VII.0 and ENDF/B-VII.1 ACE data that is provided with MCNP5 or MCNP6. To convert the ENDF/B-VII.0 ACE files (endf70[a-k] and endf70sab) into the native HDF5 format, run the following:

openmc-convert-mcnp70-data /path/to/mcnpdata/

where /path/to/mcnpdata is the directory containing the endf70[a-k] files.

To convert the ENDF/B-VII.1 ACE files (the endf71x and ENDF71SaB libraries), use the following script:

openmc-convert-mcnp71-data /path/to/mcnpdata

where /path/to/mcnpdata is the directory containing the endf71x and ENDF71SaB directories.

Using Other Cross Sections

If you have a library of ACE format cross sections other than those listed above that you need to convert to OpenMC’s HDF5 format, the openmc-ace-to-hdf5 script can be used. 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).

Manually Creating a Library from ACE files

The scripts described above use the openmc.data module in the Python API to convert ACE data and create a 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 Nuclear Data 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.

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. To add the 0 K elastic scattering cross section to 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.

Windowed Multipole Data

OpenMC is capable of using windowed multipole data for on-the-fly Doppler broadening. While such data is not yet available for all nuclides, an experimental multipole library is available that contains data for 70 nuclides. To obtain this library, you can run openmc-get-multipole-data which will download and extract it into a wmp directory. Once the library has been downloaded, set the OPENMC_MULTIPOLE_LIBRARY environment variable (or the Materials.multipole_library attribute) to the wmp directory.

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 Multi-Group Mode Part I: Introduction.

Basics of Using OpenMC

Creating 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 simulation. 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.
CMFD Specification – cmfd.xml
This file specifies execution parameters for coarse mesh finite difference (CMFD) acceleration.
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()

One 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)

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 are expanded to the naturally-occurring isotopes.

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 (either through the Materials.cross_sections attribute or 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 GND 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 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 perpendicular 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 of 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'>

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.get_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.get_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 only be applied to pairs of axis-aligned planar surfaces.

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.

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.get_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()

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

Number of Particles

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

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.

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.

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]

For a full list of all classes related to statistical distributions, see openmc.stats – Statistics.

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.Mesh class. After instantiating a Mesh, you need to specify the lower-left coordinates of the mesh (Mesh.lower_left), the number of mesh cells in each direction (Mesh.dimension) and either the upper-right coordinates of the mesh (Mesh.upper_right) or the width of each mesh cell (Mesh.width). Once you have a mesh, simply assign it to the Settings.entropy_mesh attribute.

entropy_mesh = openmc.Mesh()
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.Mesh()
m.lower_left, m.upper_right = geom.bounding_box
m.dimension = (8, 8, 8)

settings.entropy_mesh = m

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 called filters and the scoring functions are simply called scores.

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’), a reaction rate (‘total’, ‘fission’, etc.), or even scattering moments (e.g., ‘scatter-P3’). 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.
flux-YN Spherical harmonic expansion of the direction of motion \(\left(\Omega\right)\) of the total flux. This score will tally all of the harmonic moments of order 0 to N. N must be between 0 and 10.
Reaction scores: units are reactions per source particle.
Score Description
absorption Total absorption rate. This accounts for all reactions which do not produce secondary neutrons as well as fission.
elastic Elastic scattering reaction rate.
fission Total fission reaction rate.
scatter Total scattering rate. Can also be identified with the “scatter-0” response type.
scatter-N Tally the Nth scattering moment, where N is the Legendre expansion order of the change in particle angle \(\left(\mu\right)\). N must be between 0 and 10. As an example, tallying the 2nd scattering moment would be specified as <scores>scatter-2</scores>.
scatter-PN Tally all of the scattering moments from order 0 to N, where N is the Legendre expansion order of the change in particle angle \(\left(\mu\right)\). That is, “scatter-P1” is equivalent to requesting tallies of “scatter-0” and “scatter-1”. Like for “scatter-N”, N must be between 0 and 10. As an example, tallying up to the 2nd scattering moment would be specified as <scores> scatter-P2 </scores>.
scatter-YN “scatter-YN” is similar to “scatter-PN” except an additional expansion is performed for the incoming particle direction \(\left(\Omega\right)\) using the real spherical harmonics. This is useful for performing angular flux moment weighting of the scattering moments. Like “scatter-PN”, “scatter-YN” will tally all of the moments from order 0 to N; N again must be between 0 and 10.
total Total reaction rate.
total-YN The total reaction rate expanded via spherical harmonics about the direction of motion of the neutron, \(\Omega\). This score will tally all of the harmonic moments of order 0 to N. N must be between 0 and 10.
(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,nHe-3) (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.
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, nu-scatter-N, nu-scatter-PN, nu-scatter-YN These scores are similar in functionality to their scatter* equivalents 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.
Miscellaneous scores: units are indicated for each.
Score Description
current Partial currents on the boundaries of each cell in a mesh. Units are particles per source particle. Note that this score can only be used if a mesh filter has been specified. Furthermore, it may not be used in conjunction with any other score.
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.
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.

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-silovtk script can convert the HDF5 voxel file to VTK or SILO 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.

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. 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
-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-convert-mcnp70-data

This script converts ENDF/B-VII.0 ACE data from the MCNP5/6 distribution into an HDF5 library that can be used by OpenMC. This assumes that you have a directory containing files named endf70a, endf70b, ..., endf70k, and endf70sab. The path to the directory containing these files should be given as a positional argument. The following optional arguments are available:

-d DESTINATION, --destination DESTINATION
 Directory to create new library in (Default: mcnp_endfb70)

openmc-convert-mcnp71-data

This script converts ENDF/B-VII.1 ACE data from the MCNP6 distribution into an HDF5 library that can be used by OpenMC. This assumes that you have a directory containing subdirectories ‘endf71x’ and ‘ENDF71SaB’. The path to the directory containing these subdirectories should be given as a positional argument. The following optional arguments are available:

-d DESTINATION, --destination DESTINATION
 Directory to create new library in (Default: mcnp_endfb71)
-f FER, --fission_energy_release FER
 HDF5 file containing fission energy release data

openmc-get-jeff-data

This script downloads JEFF 3.2 ACE data from OECD/NEA and converts it to a multi-temperature HDF5 library for use with OpenMC. It has the following optional arguments:

-b, --batch Suppress standard in
-d DESTINATION, --destination DESTINATION
 Directory to create new library in (default: jeff-3.2-hdf5)

Warning

This script will download approximately 9 GB of data. Extracting and processing the data may require as much as 40 GB of additional free disk space.

openmc-get-multipole-data

This script downloads and extracts windowed multipole data based on ENDF/B-VII.1. It has the following optional arguments:

-b, --batch Suppress standard in

openmc-get-nndc-data

This script downloads ENDF/B-VII.1 ACE data from NNDC and converts it to an HDF5 library for use with OpenMC. This data is used for OpenMC’s regression test suite. This script has the following optional arguments:

-b, --batch Suppress standard in

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

When OpenMC generates voxel plots, they are in an HDF5 format that is not terribly useful by itself. The openmc-voxel-to-silovtk script converts a voxel HDF5 file to VTK or SILO file. For VTK, you need to have the VTK Python bindings installed. For SILO, you need to have silomesh installed. To convert a voxel file, simply provide the path to the file:

openmc-voxel-to-silovtk voxel_1.h5

The openmc-voxel-to-silovtk script also takes the following optional command-line arguments:

-o, --output Path to output VTK or SILO file
-s, --silo Flag to convert to SILO instead of VTK

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 IPython notebook demonstrates how to extract data from a statepoint using the Python API.

Plotting in 2D

The IPython notebook example also demonstrates how to plot a 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 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 IPython 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. To use OpenMP, you need to pass the -Dopenmp=on flag when running CMake:

cmake -Dopenmp=on /path/to/openmc/root
make

The only requirement is that the Fortran 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.

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.

undefined reference to `_vtab$...

If you see this message when trying to compile, the most likely cause is that you are using a compiler that does not support type-bound procedures from Fortran 2003. This affects any version of gfortran prior to 4.6. Downloading and installing the latest gfortran compiler should resolve this problem.

gfortran: unrecognized option ‘-cpp’

You are probably using a version of the gfortran compiler that is too old. Download and install the latest version of gfortran.

f951: error: unrecognized command line option “-fbacktrace”

You are probably using a version of the gfortran compiler that is too old. Download and install the latest version of gfortran.

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, send an email to the OpenMC User’s Group mailing list.

ERROR: No cross_sections.xml file was specified in settings.xml or in the 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 CROSS_SECTIONS environment variable. It is recommended to add a line in your .profile or .bash_profile setting the 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 -s, -particle, or --particle command-line options 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 and explains the structure of the OpenMC source code and how to do various development tasks such as debugging.

Data Structures

The purpose of this section is to give you an overview of the major data structures in OpenMC and how they are logically related. A majority of variables in OpenMC are derived types (similar to a struct in C). These derived types are defined in the various header modules, e.g. src/geometry_header.F90. Most important variables are found in the global module. Have a look through that module to get a feel for what variables you’ll often come across when looking at OpenMC code.

Particle

Perhaps the variable that you will see most often is simply called p and is of type(Particle). This variable stores information about a particle’s physical characteristics (coordinates, direction, energy), what cell and material it’s currently in, how many collisions it has undergone, etc. In practice, only one particle is followed at a time so there is no array of type(Particle). The Particle type is defined in the particle_header module.

You will notice that the direction and angle of the particle is stored in a linked list of type(LocalCoord). In geometries with multiple Universes, the coordinates in each universe are stored in this linked list. If universes or lattices are not used in a geometry, only one LocalCoord is present in the linked list.

The LocalCoord type has a component called cell which gives the index in the cells array in the global module. The cells array is of type(Cell) and stored information about each region defined by the user.

Cell

The Cell type is defined in the geometry_header module along with other geometry-related derived types. Each cell in the problem is described in terms of its bounding surfaces, which are listed on the surfaces component. The absolute value of each item in the surfaces component contains the index of the corresponding surface in the surfaces array defined in the global module. The sign on each item in the surfaces component indicates whether the cell exists on the positive or negative side of the surface (see Geometry).

Each cell can either be filled with another universe/lattice or with a material. If it is filled with a material, the material component gives the index of the material in the materials array defined in the global module.

Surface

The Surface type is defined in the geometry_header module. A surface is defined by a type (sphere, cylinder, etc.) and a list of coefficients for that surface type. The simplest example would be a plane perpendicular to the xy, yz, or xz plane which needs only one parameter. The type component indicates the type through integer parameters such as SURF_SPHERE or SURF_CYL_Y (these are defined in the constants module). The coeffs component gives the necessary coefficients to parameterize the surface type (see <surface> Element).

Material

The Material type is defined in the material_header module. Each material contains a number of nuclides at a given atom density. Each item in the nuclide component corresponds to the index in the global nuclides array (as usual, found in the global module). The atom_density component is the same length as the nuclides component and lists the corresponding atom density in atom/barn-cm for each nuclide in the nuclides component.

If the material contains nuclides for which binding effects are important in low-energy scattering, a \(S(\alpha,\beta)\) can be associated with that material through the sab_table component. Again, this component contains the index in the sab_tables array from the global module.

Nuclide

The Nuclide derived type stores cross section and interaction data for a nucleus and is defined in the ace_header module. The energy component is an array that gives the discrete energies at which microscopic cross sections are tabulated. The actual microscopic cross sections are stored in a separate derived type, Reaction. An arrays of Reactions is present in the reactions component. There are a few summary microscopic cross sections stored in other components, such as total, elastic, fission, and nu_fission.

If a Nuclide is fissionable, the prompt and delayed neutron yield and energy distributions are also stored on the Nuclide type. Many nuclides also have unresolved resonance probability table data. If present, this data is stored in the component urr_data of derived type UrrData. A complete description of the probability table method is given in Unresolved Resonance Region Probability Tables.

The list of nuclides present in a problem is stored in the nuclides array defined in the global module.

SAlphaBeta

The SAlphaBeta derived type stores \(S(\alpha,\beta)\) data to account for molecular binding effects when treating thermal scattering. Each SAlphaBeta table is associated with a specific nuclide as identified in the zaid component. A complete description of the \(S(\alpha,\beta)\) treatment can be found in S() Tables.

XsListing

The XsListing derived type stores information on the location of an ACE cross section table based on the data in cross_sections.xml and is defined in the ace_header module. For each <ace_table> you see in cross_sections.xml, there is a XsListing with its information. When the user input is read, the array xs_listings in the global module that is of derived type XsListing is used to locate the ACE data to parse.

NuclideMicroXS

The NuclideMicroXS derived type, defined in the ace_header module, acts as a ‘cache’ for microscopic cross sections. As a particle is traveling through different materials, cross sections can be reused if the energy of the particle hasn’t changed. The components total, elastic, absorption, fission, and nu_fission represent those microscopic cross sections at the current energy of the particle for a given nuclide. An array micro_xs in the global module that is the same length as the nuclides array stores these cached cross sections for each nuclide in the problem.

MaterialMacroXS

In addition to the NuclideMicroXS type, there is also a MaterialMacroXS derived type, defined in the ace_header module that stored cached macroscopic cross sections for the current material. These macroscopic cross sections are used for both physics and tallying purposes. The variable material_xs in the global module is of type MaterialMacroXS.

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.

Fortran

General Rules

Conform to the Fortran 2008 standard.

Make sure code can be compiled with most common compilers, especially gfortran and the Intel Fortran compiler. This supercedes the previous rule — if a Fortran 2003/2008 feature is not implemented in a common compiler, do not use it.

Do not use special extensions that can be only be used from certain compilers.

In general, write your code in lower-case. Having code in all caps does not enhance code readability or otherwise.

Always include comments to describe what your code is doing. Do not be afraid of using copious amounts of comments.

Use <, >, <=, >=, ==, and /= rather than .lt., .gt., .le., .ge., .eq., and .ne.

Try to keep code within 80 columns when possible.

Don’t use print * or write(*,*). If writing to a file, use a specific unit. Writing to standard output or standard error should be handled by the write_message subroutine or functionality in the error module.

Procedures

Above each procedure, include a comment block giving a brief description of what the procedure does.

Nonpointer dummy arguments to procedures should be explicitly specified as intent(in), intent(out), or intent(inout).

Include a comment describing what each argument to a procedure is.

Variables

Never, under any circumstances, should implicit variables be used! Always include implicit none and define all your variables.

Variable names should be all lower-case and descriptive, i.e. not a random assortment of letters that doesn’t give any information to someone seeing it for the first time. Variables consisting of multiple words should be separated by underscores, not hyphens or in camel case.

Constant (parameter) variables should be in ALL CAPITAL LETTERS and defined in in the constants.F90 module.

32-bit reals (real(4)) should never be used. Always use 64-bit reals (real(8)).

For arbitrary length character variables, use the pre-defined lengths MAX_LINE_LEN, MAX_WORD_LEN, and MAX_FILE_LEN if possible.

Do not use old-style character/array length (e.g. character*80, real*8).

Integer values being used to indicate a certain state should be defined as named constants (see the constants.F90 module for many examples).

Always use a double colon :: when declaring a variable.

Yes:

if (boundary_condition == BC_VACUUM) then

No:

if (boundary_condition == -10) then

Avoid creating arrays with a pre-defined maximum length. Use dynamic memory allocation instead. Use allocatable variables instead of pointer variables when possible.

Shared/Module Variables

Always put shared variables in modules. Access module variables through a use statement. Always use the only specifier on the use statement except for variables from the global, constants, and various header modules.

Never use equivalence statements, common blocks, or data statements.

Derived Types and Classes

Derived types and classes should have CamelCase names with words not separated by underscores or hyphens.

Indentation

Never use tab characters. Indentation should always be applied using spaces. Emacs users should include the following line in their .emacs file:

(setq-default indent-tabs-mode nil)

vim users should include the following line in their .vimrc file:

set expandtab

Use 2 spaces per indentation level. This applies to all constructs such as program, subroutine, function, if, associate, etc. Emacs users should set the variables f90-if-indent, f90-do-indent, f90-continuation-indent, f90-type-indent, f90-associate-indent, and f90-program indent to 2.

Continuation lines should be indented by at least 5 spaces. They may be indented more in order to make the content match the context. For example, either of these are valid continuation indentations:

local_xyz(1) = xyz(1) - (this % lower_left(1) + &
     (i_xyz(1) - HALF)*this % pitch(1))
call which_data(scatt_type, get_scatt, get_nuscatt, get_chi_t, get_chi_p, &
                get_chi_d, scatt_order)
Whitespace in Expressions

Use a single space between arguments to procedures.

Avoid extraneous whitespace in the following situations:

  • In procedure calls:

    Yes: call somesub(x, y(2), z)
    No:  call somesub( x, y( 2 ), z )
    
  • In logical expressions, use one space around operators but nowhere else:

    Yes: if (variable == 2) then
    No:  if ( variable==2 ) then
    

The structure component designator % should be surrounded by one space on each side.

Do not leave trailing whitespace at the end of a line.

Python

Style for Python code should follow PEP8.

Docstrings for functions and methods should follow numpydoc style.

Python code should work with both Python 2.7+ and Python 3.0+.

Use of third-party Python packages should be limited to numpy, scipy, and h5py. Use of other third-party packages must be implemented as optional dependencies rather than required dependencies.

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.

Trivial changes to the code may be committed directly to the develop branch by a trusted developer. However, most new features 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 trusted developer. If the pull request is satisfactory, it is then merged into develop. Note that a trusted developer 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 the regression suite with all configurations (This is checked by Travis CI).
  • If appropriate, test cases are added to regression suite.
  • 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 to 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 mit-crpg/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 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 mit-crpg/openmc as the target.

    _images/pullrequest.png

    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 trusted developer 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 mit-crpg/openmc.

OpenMC Test Suite

The purpose of this test suite is to ensure that OpenMC compiles using various combinations of compiler flags and options, and that all user input options can be used successfully without breaking the code. The test suite is comprised of regression tests where different types of input files are configured and the full OpenMC code is executed. Results from simulations are compared with expected results. The test suite is comprised of many build configurations (e.g. debug, mpi, hdf5) and the actual tests which reside in sub-directories in the tests directory. We recommend to developers to test their branches before submitting a formal pull request using gfortran and Intel compilers if available.

The test suite is designed to integrate with cmake using ctest. It is configured to run with cross sections from NNDC augmented with 0 K elastic scattering data for select nuclides as well as multipole data. To download the proper data, run the following commands:

wget -O nndc_hdf5.tar.xz $(cat <openmc_root>/.travis.yml | grep anl.box | awk '{print $2}')
tar xJvf nndc_hdf5.tar.xz
export OPENMC_CROSS_SECTIONS=$(pwd)/nndc_hdf5/cross_sections.xml

git clone --branch=master git://github.com/smharper/windowed_multipole_library.git wmp_lib
tar xzvf wmp_lib/multipole_lib.tar.gz
export OPENMC_MULTIPOLE_LIBRARY=$(pwd)/multipole_lib

The test suite can be run on an already existing build using:

cd build
make test

or

cd build
ctest

There are numerous ctest command line options that can be set to have more control over which tests are executed.

Before running the test suite python script, the following environmental variables should be set if the default paths are incorrect:

  • FC - The command for a Fortran compiler (e.g. gfotran, ifort).

    • Default - gfortran
  • CC - The command for a C compiler (e.g. gcc, icc).

    • Default - gcc
  • CXX - The command for a C++ compiler (e.g. g++, icpc).

    • Default - g++
  • MPI_DIR - The path to the MPI directory.

    • Default - /opt/mpich/3.2-gnu
  • HDF5_DIR - The path to the HDF5 directory.

    • Default - /opt/hdf5/1.8.16-gnu
  • PHDF5_DIR - The path to the parallel HDF5 directory.

    • Default - /opt/phdf5/1.8.16-gnu

To run the full test suite, the following command can be executed in the tests directory:

python run_tests.py

A subset of build configurations and/or tests can be run. To see how to use the script run:

python run_tests.py --help

As an example, say we want to run all tests with debug flags only on tests that have cone and plot in their name. Also, we would like to run this on 4 processors. We can run:

python run_tests.py -j 4 -C debug -R "cone|plot"

Note that standard regular expression syntax is used for selecting build configurations and tests. To print out a list of build configurations, we can run:

python run_tests.py -p
Adding tests to test suite

To add a new test to the test suite, create a sub-directory in the tests directory that conforms to the regular expression test_. To configure a test you need to add the following files to your new test directory, test_name for example:

  • OpenMC input XML files
  • test_name.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/HDF5, 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 directory will automatically be added to the CTest framework.

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 mit-crpg/openmc repository, simply follow the steps above with an extra step of pulling a branch from your private repository into a public fork.

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 Fortran 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 Fortran codebase. Make appropriate changes in the input_xml module to read your new user input. You should use procedures and types defined by the xml_interface module.

  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:

sudo pip install sphinx

Additionally, you will also need a Sphinx extension for numbering figures. The Numfig package can be installed directly with pip:

sudo pip install sphinx-numfig

Building Documentation as a Webpage

To build the documentation as a webpage (what appears at http://mit-crpg.github.io/openmc), 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 as well as Inkscape, which is used to convert .svg files to .pdf files. Inkscape can be installed in a Debian-derivative with:

sudo apt-get install inkscape

One the pre-requisites are installed, simply go to the docs directory and run:

make latexpdf

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 Example Notebooks. 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.
  • Ability to create materials based on natural elements or uranium enrichment
  • Automated multi-group cross section generation (openmc.mgxs)
  • 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_trisos())
  • A fully-featured nuclear data interface (openmc.data)

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.VolumeCalculation Stochastic volume calculation specifications and results.
openmc.Settings Settings used for an OpenMC simulation.
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.

Two helper function are also available to create rectangular and hexagonal prisms defined by the intersection of four and six surface half-spaces, respectively.

openmc.get_hexagonal_prism Create a hexagon region from six surface planes.
openmc.get_rectangular_prism Get an infinite rectangular prism from four planar surfaces.
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.CellbornFilter Bins tally events based on which Cell the neutron was born in.
openmc.SurfaceFilter Bins particle currents on Mesh surfaces.
openmc.MeshFilter Bins tally event locations onto 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.Mesh A structured Cartesian mesh in one, two, or three dimensions
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.
Coarse Mesh Finite Difference Acceleration
openmc.CMFDMesh A structured Cartesian mesh used for Coarse Mesh Finite Difference (CMFD) acceleration.
openmc.CMFD Parameters that control the use of coarse-mesh finite difference acceleration in OpenMC.
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.

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.

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.
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.Box Uniform distribution of coordinates in a rectangular cuboid.
openmc.stats.Point Delta function in three dimensions.

openmc.mgxs – Multi-Group Cross Section Generation

Energy Groups
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.AbsorptionXS An absorption multi-group cross section.
openmc.mgxs.CaptureXS A capture multi-group cross section.
openmc.mgxs.Chi The fission spectrum.
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.
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.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.model – Model Building

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_trisos Generate a random, non-overlapping configuration of TRISO particles within a container.
Model Container
Classes
openmc.model.Model Model container.

openmc.data – Nuclear Data Interface

Core Classes
openmc.data.IncidentNeutron Continuous-energy neutron interaction data.
openmc.data.Reaction A nuclear reaction
openmc.data.Product Secondary particle emitted in a nuclear reaction
openmc.data.Tabulated1D A one-dimensional tabulated function.
openmc.data.FissionEnergyRelease Energy relased by fission reactions.
openmc.data.ThermalScattering A ThermalScattering object contains thermal scattering data as represented by an S(alpha, beta) table.
openmc.data.CoherentElastic Coherent elastic scattering data from a crystalline material
openmc.data.FissionEnergyRelease Energy relased by fission reactions.
openmc.data.DataLibrary Collection of cross section data libraries.
openmc.data.Decay Radioactive decay data.
openmc.data.FissionProductYields Independent and cumulative fission product yields.
openmc.data.WindowedMultipole Resonant cross sections represented in the windowed multipole format.
Core Functions
openmc.data.atomic_mass Return atomic mass of isotope in atomic mass units.
openmc.data.linearize Return a tabulated representation of a function of one variable.
openmc.data.thin Check for (x,y) points that can be removed.
openmc.data.write_compact_458_library Read ENDF files, strip the MF=1 MT=458 data and write to small HDF5.
Angle-Energy Distributions
openmc.data.AngleEnergy Distribution in angle and energy of a secondary particle.
openmc.data.KalbachMann Kalbach-Mann distribution
openmc.data.CorrelatedAngleEnergy Correlated angle-energy distribution
openmc.data.UncorrelatedAngleEnergy Uncorrelated angle-energy distribution
openmc.data.NBodyPhaseSpace N-body phase space distribution
openmc.data.LaboratoryAngleEnergy Laboratory angle-energy distribution
openmc.data.AngleDistribution Angle distribution as a function of incoming energy
openmc.data.EnergyDistribution Abstract superclass for all energy distributions.
openmc.data.ArbitraryTabulated Arbitrary tabulated function given in ENDF MF=5, LF=1 represented as
openmc.data.GeneralEvaporation General evaporation spectrum given in ENDF MF=5, LF=5 represented as
openmc.data.MaxwellEnergy Simple Maxwellian fission spectrum represented as
openmc.data.Evaporation Evaporation spectrum represented as
openmc.data.WattEnergy Energy-dependent Watt spectrum represented as
openmc.data.MadlandNix Energy-dependent fission neutron spectrum (Madland and Nix) given in
openmc.data.DiscretePhoton Discrete photon energy distribution
openmc.data.LevelInelastic Level inelastic scattering
openmc.data.ContinuousTabular Continuous tabular distribution
Resonance Data
openmc.data.Resonances Resolved and unresolved resonance data
openmc.data.ResonanceRange Resolved resonance range
openmc.data.SingleLevelBreitWigner Single-level Breit-Wigner resolved resonance formalism data.
openmc.data.MultiLevelBreitWigner Multi-level Breit-Wigner resolved resonance formalism data.
openmc.data.ReichMoore Reich-Moore resolved resonance formalism data.
openmc.data.RMatrixLimited R-matrix limited resolved resonance formalism data.
openmc.data.ParticlePair
openmc.data.SpinGroup Resonance spin group
openmc.data.Unresolved Unresolved resonance parameters as identified by LRU=2 in MF=2.
ACE Format
Classes
openmc.data.ace.Library A Library objects represents an ACE-formatted file which may contain multiple tables with data.
openmc.data.ace.Table ACE cross section table
Functions
openmc.data.ace.ascii_to_binary Convert an ACE file in ASCII format (type 1) to binary format (type 2).
ENDF Format
Classes
openmc.data.endf.Evaluation ENDF material evaluation with multiple files/sections
Functions
openmc.data.endf.float_endf Convert string of floating point number in ENDF to float.
openmc.data.endf.get_cont_record Return data from a CONT record in an ENDF-6 file.
openmc.data.endf.get_evaluations Return a list of all evaluations within an ENDF file.
openmc.data.endf.get_head_record Return data from a HEAD record in an ENDF-6 file.
openmc.data.endf.get_tab1_record Return data from a TAB1 record in an ENDF-6 file.
openmc.data.endf.get_tab2_record
openmc.data.endf.get_text_record Return data from a TEXT record in an ENDF-6 file.
NJOY Interface
openmc.data.njoy.run Run NJOY with given commands
openmc.data.njoy.make_pendf Generate ACE file from an ENDF file
openmc.data.njoy.make_ace Generate incident neutron ACE file from an ENDF file
openmc.data.njoy.make_ace_thermal Generate thermal scattering ACE file from ENDF files

openmc.examples – Example Models

Simple Models
openmc.examples.slab_mg Create a one-group, 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.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.

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. If windowed-multipole data is avalable, this temperature will be used to Doppler broaden some cross sections in the resolved resonance region. 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

\[\begin{split}\left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\theta \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 ]\end{split}\]

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

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.

<multipole_library> Element

The <multipole_library> element indicates the directory containing a windowed multipole library. If a windowed multipole library is available, OpenMC can use it for on-the-fly Doppler-broadening of resolved resonance range cross sections. If this element is absent from the settings.xml file, the OPENMC_MULTIPOLE_LIBRARY environment variable will be used.

Note

The <temperature_multipole> element must also be set to “true” for windowed multipole functionality.

<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: “”

temperature:

An element with no attributes which is used to set the default 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.

An optional attribute/sub-element for each nuclide is scattering. This attribute may be set to “data” to use the scattering laws specified by the cross section library (default). Alternatively, when set to “iso-in-lab”, the scattering laws are used to sample the outgoing energy but an isotropic-in-lab distribution is used to sample the outgoing angle at each scattering interaction. The scattering attribute 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: None

Note

The scattering attribute/sub-element is not used in the multi-group <energy_mode> Element.

sab:

Associates an S(a,b) table with the material. This element has one attribute/sub-element called name. The name attribute is the name of the S(a,b) table that should be associated with the material.

Default: None

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

The energy under which particles will be killed.

Default: 0.0

<energy_grid> Element

The <energy_grid> element determines the treatment of the energy grid during a simulation. The valid options are “nuclide”, “logarithm”, and “material-union”. Setting this element to “nuclide” will cause OpenMC to use a nuclide’s energy grid when determining what points to interpolate between for determining cross sections (i.e. non-unionized energy grid). Setting this element to “logarithm” causes OpenMC to use a logarithmic mapping technique described in LA-UR-14-24530. Setting this element to “material-union” will cause OpenMC to create energy grids that are unionized material-by-material and use these grids when determining the energy-cross section pairs to interpolate cross section values between.

Default: logarithm

Note

This element is not used in the multi-group <energy_mode> Element.

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

The <entropy> element describes a mesh that is used for calculating Shannon entropy. This mesh should cover all possible fissionable materials in the problem. It has the following attributes/sub-elements:

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

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

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

<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 neutrons per batch for a fixed source simulation.

Default: None
<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: “ares” (accelerated resonance elastic scattering), “dbrc” (Doppler broadening rejection correction), or “wcm” (weight correction method). Descriptions of each of these methods are documented here.

Default: “ares”

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_cmfd> Element

The <run_cmfd> element indicates whether or not CMFD acceleration should be turned on or off. This element has no attributes or sub-elements and can be set to either “false” or “true”.

Default: false
<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

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

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”, and “cartesian”. 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. An “cartesian” spatial distribution specifies independent distributions of x-, y-, and z-coordinates.

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.

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 an “cartesian” distribution, 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).

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

<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_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
<threads> Element

The <threads> element indicates the number of OpenMP threads to be used for a simulation. It has no attributes and accepts a positive integer value.

Default: None (Determined by environment variable OMP_NUM_THREADS)
<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.

<uniform_fs> Element

The <uniform_fs> element describes 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). This mesh should cover all possible fissionable materials in the problem. It has the following attributes/sub-elements:

dimension:

The number of mesh cells in the x, y, and z directions, respectively.

Default: None

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

<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

<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
<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 three valid elements in the tallies.xml file are <tally>, <mesh>, 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: “”

filter:

Specify a filter that modifies 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. This element and its attributes/sub-elements are described below.

Note

You may specify zero, one, or multiple filters to apply to the tally. To specify multiple filters, you must use multiple <filter> elements.

The filter element has the following attributes/sub-elements:

type:The type of the filter. Accepted options are “cell”, “cellborn”, “material”, “universe”, “energy”, “energyout”, “mu”, “polar”, “azimuthal”, “mesh”, “distribcell”, “delayedgroup”, and “energyfunction”.
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)
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 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.

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

<mesh> Element

If a structured 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 structured mesh. The only valid option is “regular”.
dimension:The number of mesh cells in each direction.
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.
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.
width:The width of mesh cells in each direction.

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 3D SILO files using the openmc-voxel-to-silovtk 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 SILO 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

CMFD Specification – cmfd.xml

Coarse mesh finite difference acceleration method has been implemented in OpenMC. Currently, it allows users to accelerate fission source convergence during inactive neutron batches. To run CMFD, the <run_cmfd> element in settings.xml should be set to “true”.

<begin> Element

The <begin> element controls what batch CMFD calculations should begin.

Default: 1
<dhat_reset> Element

The <dhat_reset> element controls whether \(\widehat{D}\) nonlinear CMFD parameters should be reset to zero before solving CMFD eigenproblem. It can be turned on with “true” and off with “false”.

Default: false
<display> Element

The <display> element sets one additional CMFD output column. Options are:

  • “balance” - prints the RMS [%] of the resdiual from the neutron balance equation on CMFD tallies.

  • “dominance” - prints the estimated dominance ratio from the CMFD iterations. This will only work for power iteration eigensolver.

  • “entropy” - prints the entropy of the CMFD predicted fission source. Can only be used if OpenMC entropy is active as well.

  • “source” - prints the RMS [%] between the OpenMC fission source and CMFD fission source.

    Default: balance

<downscatter> Element

The <downscatter> element controls whether an effective downscatter cross section should be used when using 2-group CMFD. It can be turned on with “true” and off with “false”.

Default: false
<feedback> Element

The <feedback> element controls whether or not the CMFD diffusion result is used to adjust the weight of fission source neutrons on the next OpenMC batch. It can be turned on with “true” and off with “false”.

Default: false
<gauss_seidel_tolerance> Element

The <gauss_seidel_tolerance> element specifies two parameters. The first is the absolute inner tolerance for Gauss-Seidel iterations when performing CMFD and the second is the relative inner tolerance for Gauss-Seidel iterations for CMFD calculations.

Default: 1.e-10 1.e-5
<ktol> Element

The <ktol> element specifies the tolerance on the eigenvalue when performing CMFD power iteration.

Default: 1.e-8
<mesh> Element

The CMFD mesh is a structured Cartesian mesh. This element has the following attributes/sub-elements:

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.

upper_right:

The upper-right corner of the structrued mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh.

dimension:

The number of mesh cells in each direction.

width:

The width of mesh cells in each direction.

energy:

Energy bins [in eV], listed in ascending order (e.g. 0.0 0.625 20.0e6) for CMFD tallies and acceleration. If no energy bins are listed, OpenMC automatically assumes a one energy group calculation over the entire energy range.

albedo:

Surface ratio of incoming to outgoing partial currents on global boundary conditions. They are listed in the following order: -x +x -y +y -z +z.

Default: 1.0 1.0 1.0 1.0 1.0 1.0

map:

An optional acceleration map can be specified to overlay on the coarse mesh spatial grid. If this option is used, a 1 is used for a non-accelerated region and a 2 is used for an accelerated region. For a simple 4x4 coarse mesh with a 2x2 fuel lattice surrounded by reflector, the map is:

1 1 1 1

1 2 2 1

1 2 2 1

1 1 1 1

Therefore a 2x2 system of equations is solved rather than a 4x4. This is extremely important to use in reflectors as neutrons will not contribute to any tallies far away from fission source neutron regions. A 2 must be used to identify any fission source region.

Note

Only two of the following three sub-elements are needed: lower_left, upper_right and width. Any combination of two of these will yield the third.

<norm> Element

The <norm> element is used to normalize the CMFD fission source distribution to a particular value. For example, if a fission source is calculated for a 17 x 17 lattice of pins, the fission source may be normalized to the number of fission source regions, in this case 289. This is useful when visualizing this distribution as the average peaking factor will be unity. This parameter will not impact the calculation.

Default: 1.0
<power_monitor> Element

The <power_monitor> element is used to view the convergence of power iteration. This option can be turned on with “true” and turned off with “false”.

Default: false
<run_adjoint> Element

The <run_adjoint> element can be turned on with “true” to have an adjoint calculation be performed on the last batch when CMFD is active.

Default: false
<shift> Element

The <shift> element specifies an optional Wielandt shift parameter for accelerating power iterations. It is by default very large so the impact of the shift is effectively zero.

Default: 1e6
<spectral> Element

The <spectral> element specifies an optional spectral radius that can be set to accelerate the convergence of Gauss-Seidel iterations during CMFD power iteration solve.

Default: 0.0
<stol> Element

The <stol> element specifies the tolerance on the fission source when performing CMFD power iteration.

Default: 1.e-8
<tally_reset> Element

The <tally_reset> element contains a list of batch numbers in which CMFD tallies should be reset.

Default: None
<write_matrices> Element

The <write_matrices> element is used to write the sparse matrices created when solving CMFD equations. This option can be turned on with “true” and off with “false”.

Default: false

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 cross section file is located, whether it contains incident neutron or thermal scattering 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, either ‘neutron’ or ‘thermal’.

Nuclear Data File Format

Incident Neutron Data

/

Attributes:
  • 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[]) – Energy points 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

/<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 (polynomial) – Energy released in the form of fragments as a function of incident neutron energy.
  • prompt_neutrons (polynomial or tabulated) – Energy released in the form of prompt neutrons as a function of incident neutron energy.
  • delayed_neutrons (polynomial) – Energy released in the form of delayed neutrons as a function of incident neutron energy.
  • prompt_photons (polynomial) – Energy released in the form of prompt photons as a function of incident neutron energy.
  • delayed_photons (polynomial) – Energy released in the form of delayed photons as a function of incident neutron energy.
  • betas (polynomial) – Energy released in the form of betas as a function of incident neutron energy.
  • neutrinos (polynomial) – Energy released in the form of neutrinos as a function of incident neutron energy.
  • q_prompt (polynomial or tabulated) – The prompt fission Q-value (fragments + prompt neutrons + prompt photons - incident energy)
  • q_recoverable (polynomial or tabulated) – The recoverable fission Q-value (Q_prompt + delayed neutrons + delayed photons + betas)
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
  • nuclides (char[][]) – Names of nuclides for which the thermal scattering data applies to
  • secondary_mode (char[]) – Indicates how the inelastic outgoing angle-energy distributions are represented (‘equal’, ‘skewed’, or ‘continuous’).

/<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 (tabulated) – Thermal inelastic scattering cross section for temperature TTT (in Kelvin)
  • mu_out (double[][]) – Distribution of outgoing energies and angles for coherent elastic scattering for temperature TTT (in Kelvin)

/<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 (tabulated) – Thermal inelastic scattering cross section for temperature TTT (in Kelvin)
  • energy_out (double[][]) – Distribution of outgoing energies for each incoming energy for temperature TTT (in Kelvin). Only present if secondary mode is not continuous.
  • mu_out (double[][][]) – Distribution of scattering cosines for each pair of incoming and outgoing energies. for temperature TTT (in Kelvin). Only present if secondary mode is not continuous.

If the secondary mode is continuous, the outgoing energy-angle distribution is given as a correlated angle-energy distribution.

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[]) – ‘bragg’
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
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

/version (char[])
The format version of the file. The current version is “v0.2”
/nuclide/
  • 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 (reaction type, coefficient index, window index).

  • 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: total, competitive if present, absorption, fission. Complex numbers are stored by forming a type with “\(r\)” and “\(i\)” identifiers, similar to how h5py does it.

  • end_E (double)

    Highest energy the windowed multipole part of the library is valid for.

  • energy_points (double[])

    Energy grid for the pointwise library in the reaction group.

  • fissionable (int)

    1 if this nuclide has fission data. 0 if it does not.

  • fit_order (int)

    The order of the curve fit.

  • formalism (int)

    The formalism of the underlying data. Uses the ENDF-6 format formalism numbers.

    Table of supported formalisms.
    Formalism Formalism number
    MLBW 2
    Reich-Moore 3
  • l_value (int[])

    The index for a corresponding pole. Equivalent to the \(l\) quantum number of the resonance the pole comes from \(+1\).

  • length (int)

    Total count of poles in data.

  • max_w (int)

    Maximum number of poles in a window.

  • MT_count (int)

    Number of pointwise tables in the library.

  • MT_list (int[])

    A list of available MT identifiers. See ENDF-6 for meaning.

  • n_grid (int)

    Total length of the pointwise data.

  • num_l (int)

    Number of possible \(l\) quantum states for this nuclide.

  • pseudo_K0RS (double[])

    \(l\) dependent value of

    \[\sqrt{\frac{2 m_n}{\hbar}}\frac{AWR}{AWR + 1} r_{s,l}\]

    Where \(m_n\) is mass of neutron, \(AWR\) is the atomic weight ratio of the target to the neutron, and \(r_{s,l}\) is the scattering radius for a given \(l\).

  • spacing (double)
    \[\frac{\sqrt{E_{max}}- \sqrt{E_{min}}}{n_w}\]

    Where \(E_{max}\) is the maximum energy the windows go up to. This is not equivalent to the maximum energy for which the windowed multipole data is valid for. It is slightly higher to ensure an integer number of windows. \(E_{min}\) is the minimum energy and equivalent to start_E, and \(n_w\) is the number of windows, given by windows.

  • sqrtAWR (double)

    Square root of the atomic weight ratio.

  • start_E (double)

    Lowest energy the windowed multipole part of the library is valid for.

  • w_start (int[])

    The pole to start from for each window.

  • w_end (int[])

    The pole to end at for each window.

  • windows (int)

    Number of windows.

/nuclide/reactions/MT<i>
  • MT_sigma (double[]) – Cross section value for this reaction.
  • Q_value (double) – Energy released in this reaction, in eV.
  • threshold (int) – The first non-zero entry in MT_sigma.

Fission Energy Release File Format

This file is a compact HDF5 representation of the ENDF MT=1, MF=458 data (see ENDF-102 for details). It gives the information needed to compute the energy carried away from fission reactions by each reaction product (e.g. fragment nuclei, neutrons) which depends on the incident neutron energy. OpenMC is distributed with one of these files under data/fission_Q_data_endfb71.h5. More files of this format can be created from ENDF files with the openmc.data.write_compact_458_library function. They can be read with the openmc.data.FissionEnergyRelease.from_compact_hdf5 class method.

Attributes:
  • comment (char[]) – An optional text comment
  • component order (char[][]) – An array of strings specifying the order each reaction product occurs in the data arrays. The components use the 2-3 letter abbreviations specified in ENDF-102 e.g. EFR for fission fragments and ENP for prompt neutrons.
/<nuclide name>/
Nuclides are named by concatenating their atomic symbol and mass number. For example, ‘U235’ or ‘Pu239’. Metastable nuclides are appended with an ‘_m’ and their metastable number. For example, ‘Am242_m1’
Datasets:
  • data (double[][][]) – The energy release coefficients. The

    first axis indexes the component type. The second axis specifies values or uncertainties. The third axis indexes the polynomial order. If the data uses the Sher-Beck format, then the last axis will have a length of one and ENDF-102 should be consulted for energy dependence. Otherwise, the data uses the Madland format which is a polynomial of incident energy.

    For example, if ‘EFR’ is given first in the component order attribute and the data uses the Madland format, then the energy released in the form of fission fragments at an incident energy \(E\) is given by

    \[\text{data}[0, 0, 0] + \text{data}[0, 0, 1] \cdot E + \text{data}[0, 0, 2] \cdot E^2 + \ldots\]

    And its uncertainty is

    \[\text{data}[0, 1, 0] + \text{data}[0, 1, 1] \cdot E + \text{data}[0, 1, 2] \cdot E^2 + \ldots\]

Output Files

State Point File Format

The current version of the statepoint file format is 16.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.
  • cmfd_on (int) – Flag indicating whether CMFD is on (1) or off (0).
  • 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 wgt, xyz, uvw, E, g, and delayed_group, which represent the weight, position, direction, energy, energy group, and delayed_group of the source particle, respectively. Only present when run_mode is ‘eigenvalue’.

/cmfd/

Datasets:
  • indices (int[4]) – Indices for cmfd mesh (i,j,k,g)
  • k_cmfd (double[]) – CMFD eigenvalues
  • cmfd_src (double[][][][]) – CMFD fission source
  • cmfd_entropy (double[]) – CMFD estimate of Shannon entropy
  • cmfd_balance (double[]) – RMS of the residual neutron balance equation on CMFD mesh
  • cmfd_dominance (double[]) – CMFD estimate of dominance ratio
  • cmfd_srccmp (double[]) – RMS comparison of difference between OpenMC and CMFD fission source

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

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

Datasets:
  • n_realizations (int) – Number of realizations.
  • n_filters (int) – Number of filters used.
  • 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. In general, this can be greater than the number of user-specified scores since each score might have multiple scoring bins, e.g., scatter-PN.
  • score_bins (char[][]) – Values of specified scores.
  • n_user_scores (int) – Number of scores without accounting for those added by expansions, e.g. scatter-PN.
  • moment_orders (char[][]) – Tallying moment orders for Legendre and spherical harmonic tally expansions (e.g., ‘P2’, ‘Y1,2’, etc.).
  • 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.

/tallies/tally <uid>/filter <j>/

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.

/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.
  • CMFD (double) – Time spent evaluating CMFD.
  • CMFD building matrices (double) – Time spent buliding CMFD matrices.
  • CMFD solving matrices (double) – Time spent solving CMFD matrices.
  • total (double) – Total time spent in the program.

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.

/filetype (char[])

String indicating the type of file.

/source_bank (Compound type)

Source bank information for each particle. The compound type has fields wgt, xyz, uvw, E, and delayed_group, which represent the weight, position, direction, energy, energy group, and delayed_group of the source particle, respectively.

Summary File Format

The current version of the summary file format is 5.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.

/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’.
  • nuclide_densities (double[]) – Atom density of each nuclide.
  • sab_names (char[][]) – Names of S(\(\alpha,\beta\)) tables assigned to the material.

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

/tallies/tally <uid>/

Datasets:
  • name (char[]) – Name of the tally.

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

/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

  • 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

  • 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

  • Amanda L. Lund, Paul K. Romano, and Andrew R. Siegel, “Accelerating Source Convergence in Monte Carlo Criticality Calculations Using a Particle Ramp-Up Technique,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • Antonios G. Mylonakis, M. Varvayanni, D.G.E. Grigoriadis, and N. Catsaros, “Developing and investigating a pure Monte-Carlo module for transient neutron transport analysis,” Ann. Nucl. Energy, 104, 103-112 (2017).
  • Timothy P. Burke, Brian C. Kiedrowski, William R. Martin, and Forrest B. Brown, “GPU Acceleration of Kernel Density Estimators in Monte Carlo Neutron Transport Simulations,” Trans. Am. Nucl. Soc., 115, 531-534 (2016).
  • Timothy P. Burke, Brian C. Kiedrowski, and William R. Martin, “Cylindrical Kernel Density Estimators for Monte Carlo Neutron Transport Reactor Physics Problems,” Trans. Am. Nucl. Soc., 115, 563-566 (2016).
  • Yunzhao Li, Qingming He, Liangzhi Cao, Hongchun Wu, and Tiejun Zu, “Resonance Elastic Scattering and Interference Effects Treatments in Subgroup Method,” Nucl. Eng. Tech., 48, 339-350 (2016).
  • William Boyd, Sterling Harper, and Paul K. Romano, “Equipping OpenMC for the big data era,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
  • Michal Kostal, Vojtech Rypar, Jan Milcak, Vlastimil Juricek, Evzen Losa, Benoit Forget, and Sterling Harper, “Study of graphite reactivity worth on well-defined cores assembled on LR-0 reactor,” Ann. Nucl. Energy, 87, 601-611 (2016).
  • Qicang Shen, William Boyd, Benoit Forget, and Kord Smith, “Tally precision triggers for the OpenMC Monte Carlo code,” Trans. Am. Nucl. Soc., 112, 637-640 (2015).
  • Kyungkwan Noh and Deokjung Lee, “Whole Core Analysis using OpenMC Monte Carlo Code,” Trans. Kor. Nucl. Soc. Autumn Meeting, Gyeongju, Korea, Oct. 24-25, 2013.
  • Timothy P. Burke, Brian C. Kiedrowski, and William R. Martin, “Flux and Reaction Rate Kernel Density Estimators in OpenMC,” Trans. Am. Nucl. Soc., 109, 683-686 (2013).

Multi-group Cross Section Generation

  • Hong Shuang, Yang Yongwei, Zhang Lu, and Gao Yucui, “Fabrication and validation of multigroup cross section library based on the OpenMC code,” Nucl. Techniques 40 (4), 040504 (2017). (in Mandarin)
  • Nicholas E. Stauff, Changho Lee, Paul K. Romano, and Taek K. Kim, “Verification of Mixed Stochastic/Deterministic Approach for Fast and Thermal Reactor Analysis,” Proc. ICAPP, Fukui and Kyoto, Japan, Apr. 24-28, 2017.
  • Zhauyuan Liu, Kord Smith, and Benoit Forget, “Progress of Cumulative Migration Method for Computing Diffusion Coefficients with OpenMC,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • Geoffrey Gunow, Samuel Shaner, William Boyd, Benoit Forget, and Kord Smith, “Accuracy and Performance of 3D MOC for Full-Core PWR Problems,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • Tianliang Hu, Liangzhi Cao, Hongchun Wu, and Kun Zhuang, “A coupled neutronics and thermal-hydraulic modeling approach to the steady-state and dynamic behavior of MSRs,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • William R. D. Boyd, “Reactor Agnostic Multi-Group Cross Section Generation for Fine-Mesh Deterministic Neutron Transport Simulations,” Ph.D. Thesis, Massachusetts Institute of Technology (2017).
  • Zhaoyuan Liu, Kord Smith, and Benoit Forget, “A Cumulative Migration Method for Computing Rigorous Transport Cross Sections and Diffusion Coefficients for LWR Lattices with Monte Carlo,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
  • Adam G. Nelson and William R. Martin, “Improved Monte Carlo tallying of multi-group scattering moments using the NDPP code,” Trans. Am. Nucl. Soc., 113, 645-648 (2015)
  • Adam G. Nelson and William R. Martin, “Improved Monte Carlo tallying of multi-group scattering moment matrices,” Trans. Am. Nucl. Soc., 110, 217-220 (2014).
  • Adam G. Nelson and William R. Martin, “Improved Convergence of Monte Carlo Generated Multi-Group Scattering Moments,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).

Doppler Broadening

Nuclear Data

  • Jonathan A. Walsh, Benoit Forget, Kord S. Smith, and Forrest B. Brown, “Uncertainty in Fast Reactor-Relevant Critical Benchmark Simulations Due to Unresolved Resonance Structure,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
  • Vivian Y. Tran, Jonathan A. Walsh, and Benoit Forget, “Treatments for Neutron Resonance Elastic Scattering Using the Multipole Formalism in Monte Carlo Codes,” Trans. Am. Nucl. Soc., 115, 1133-1137 (2016).
  • Paul K. Romano and Sterling M. Harper, “Nuclear data processing capabilities in OpenMC”, Proc. Nuclear Data, Sep. 11-16, 2016.
  • Jonathan A. Walsh, Benoit Froget, Kord S. Smith, and Forrest B. Brown, “Neutron Cross Section Processing Methods for Improved Integral Benchmarking of Unresolved Resonance Region Evaluations,” Eur. Phys. J. Web Conf. 111, 06001 (2016).
  • Jonathan A. Walsh, Paul K. Romano, Benoit Forget, and Kord S. Smith, “Optimizations of the energy grid search algorithm in continuous-energy Monte Carlo particle transport codes”, Comput. Phys. Commun., 196, 134-142 (2015).
  • Jonathan A. Walsh, Benoit Forget, Kord S. Smith, Brian C. Kiedrowski, and Forrest B. Brown, “Direct, on-the-fly calculation of unresolved resonance region cross sections in Monte Carlo simulations,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Amanda L. Lund, Andrew R. Siegel, Benoit Forget, Colin Josey, and Paul K. Romano, “Using fractional cascading to accelerate cross section lookups in Monte Carlo particle transport calculations,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Ronald O. Rahaman, Andrew R. Siegel, and Paul K. Romano, “Monte Carlo performance analysis for varying cross section parameter regimes,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Jonathan A. Walsh, Benoit Forget, and Kord S. Smith, “Accelerated sampling of the free gas resonance elastic scattering kernel,” Ann. Nucl. Energy, 69, 116–124 (2014).

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

License Agreement

Copyright © 2011-2017 Massachusetts Institute of Technology

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.

Development Team

The following people have contributed to development of the OpenMC Monte Carlo code: