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 ACE format cross sections, also used in 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. Documentation for the latest developmental version of the develop branch can be found on Read the Docs.

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

Release Notes for OpenMC 0.8.0

This release of OpenMC includes a few new major features including the capability to perform neutron transport with multi-group cross section data as well as experimental support for the windowed multipole method being developed at MIT. Source sampling options have also been expanded significantly, with the option to supply arbitrary tabular and discrete distributions for energy, angle, and spatial coordinates.

The Python API has been significantly restructured in this release compared to version 0.7.1. Any scripts written based on the version 0.7.1 API will likely need to be rewritten. Some of the most visible changes include the following:

  • SettingsFile is now Settings, MaterialsFile is now Materials, and TalliesFile is now Tallies.
  • The GeometryFile class no longer exists and is replaced by the Geometry class which now has an export_to_xml() method.
  • Source distributions are defined using the Source class and assigned to the Settings.source property.
  • The Executor class no longer exists and is replaced by openmc.run() and openmc.plot_geometry() functions.

The Python API documentation has also been significantly expanded.

System Requirements

There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions and Mac OS X. Numerous users have reported working builds on Microsoft Windows, but your mileage may vary. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides and tallies in the problem).

New Features

  • Multi-group mode
  • Vast improvements to the Python API
  • Experimental windowed multipole capability
  • Periodic boundary conditions
  • Expanded source sampling options
  • Distributed materials
  • Subcritical multiplication support
  • Improved method for reproducible URR table sampling
  • Refactor of continuous-energy reaction data
  • Improved documentation and new Jupyter notebooks

Bug Fixes

  • 70daa7: Make sure MT=3 cross section is not used
  • 40b05f: Ensure source bank is resampled for fixed source runs
  • 9586ed: Fix two hexagonal lattice bugs
  • a855e8: Make sure graphite models don’t error out on max events
  • 7294a1: Fix incorrect check on cmfd.xml
  • 12f246: Ensure number of realizations is written to statepoint
  • 0227f4: Fix bug when sampling multiple energy distributions
  • 51deaa: Prevent segfault when user specifies ‘18’ on tally scores
  • fed74b: Prevent duplicate tally scores
  • 8467ae: Better threshold for allowable lost particles
  • 493c6f: Fix type of return argument for h5pget_driver_f

Contributors

This release contains new contributions from the following people:

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.

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:

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

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

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

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

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

(19)\[\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 (19), we get

(20)\[\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 (19) as a series of equations:

(21)\[ \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 (21) 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 (21) tell us that \(v\) and \(w\) do not change and the first tell us that

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

(23)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{2(Au + Bv + Cw)}{A^2 + B^2 + C^2}\]

Substituting equation (23) into equation (21) gives us the form of the solution. For example, the x-component of the reflected direction will be

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

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

(26)\[|| \nabla f ||^2 = 4 \bar{y}^2 + 4 \bar{z}^2 = 4 R^2\]

This implies that

(27)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{\bar{y}v + \bar{z}w}{R^2}\]

Substituting equations (27) and (25) into equation (21) 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

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

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

(30)\[|| \nabla f ||^2 = 4 \bar{x}^2 + 4 \bar{y}^2 + 4 \bar{z}^2 = 4 R^2\]

This implies that

(31)\[\frac{2 (\mathbf{\Omega} \cdot \nabla f)}{|| \nabla f ||^2} = \frac{\bar{x}u + \bar{y}v + \bar{z}w}{R^2}\]

Substituting equations (31) and (29) into equation (21) gives us the form of the solution:

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

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

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

(35)\[\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 (35) and (33) into equation (21) gives us the form of the solution:

(36)\[ \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} \]

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 logarithmic mapping technique [Brown] 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.

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 MGXS Part IV: Multi-Group Mode Cross-Section Library 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.

References

[Brown]Forrest B. Brown, “New Hash-based Energy Lookup Algorithm for Monte Carlo codes,” LA-UR-14-24530, Los Alamos National Laboratory (2014).
[Hwang]R. N. Hwang, “A Rigorous Pole Representation of Multilevel Cross Sections and Its Practical Application,” Nucl. Sci. Eng., 96, 192-209 (1987).
[Josey]Colin Josey, Pablo Ducru, Benoit Forget, and Kord Smith, “Windowed Multipole for Cross Section Doppler Broadening,” J. Comp. Phys, 307, 715-727 (2016). http://dx.doi.org/10.1016/j.jcp.2015.08.013

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 [LEcuyer]).

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

[LEcuyer]P. L’Ecuyer, “Tables of Linear Congruential Generators of Different Sizes and Good Lattice Structures,” Math. Comput., 68, 249 (1999).

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 Secondary Angle 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 Secondary Angle Distributions. Then an outgoing energy is sampled using the algorithms in Sampling Secondary Energy and Correlated Angle/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. An ACE table 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 ACE 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 Secondary Angle Distributions and Sampling Secondary Energy and Correlated Angle/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 Angles and 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 any reactions with secondary neutrons, it is necessary to sample secondary angle and energy distributions. This includes elastic and inelastic scattering, fission, and \((n,xn)\) reactions. In some cases, the angle and energy distributions may be specified separately, and in other cases, they may be specified as a correlated angle-energy distribution. In the following sections, we will outline the methods used to sample secondary distributions as well as how they are used to modify the state of a particle.

Sampling Secondary Angle 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,
  • An equiprobable distribution with 32 bins, or
  • A tabular distribution.
Isotropic Angular Distribution

In the first case, no data needs to be stored on the ACE table, 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)\).

Equiprobable Angle Bin Distribution

For a 32 equiprobable bin distribution, we select a random number \(\xi\) to sample a cosine bin \(i\) such that

(16)\[i = 1 + \lfloor 32\xi \rfloor.\]

The same random number can then also be used to interpolate between neighboring \(\mu\) values to get the final scattering cosine:

(17)\[\mu = \mu_i + (32\xi - i) (\mu_{i+1} - \mu_i)\]

where \(\mu_i\) is the \(i\)-th scattering cosine.

Tabular Angular Distribution

As the MCNP Manual points out, using an equiprobable bin distribution works well for high-probability regions of the scattering cosine probability, but for low-probability regions it is not very accurate. Thus, a more accurate method is to represent the scattering cosine with a tabular 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:

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

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

(20)\[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}\),

(21)\[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 (21) we have that

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

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

(24)\[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 (24) and inserting it into equation (21), we obtain

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

(26)\[\eta = \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}} (\mu' - \mu_{\ell,j}) + p_{\ell,j}.\]

Equation (25) then becomes

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

(28)\[m = \frac{p_{\ell,j+1} - p_{\ell,j}}{\mu_{\ell,j+1} - \mu_{\ell,j}}.\]

Integrating equation (27), we have

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

(30)\[\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 Secondary Energy and Correlated Angle/Energy Distributions

For a reaction with secondary neutrons, it is necessary to determine the outgoing energy of the neutrons. For any reaction other than elastic 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 where ENDF File 6 contains correlated energy-angle distributions. The ACE format specifies its own representations based loosely on the formats given in ENDF-6. In this section, we will describe how the outgoing energy of secondary particles is determined based on each ACE law.

One of the subtleties in the ACE format is the fact that a single reaction can have multiple secondary energy distributions. This is mainly useful for reactions with multiple neutrons 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 energy distributions are present, they are assigned probabilities that can then be used to randomly select one.

Once a secondary energy distribution has been sampled, the procedure for determining the outgoing energy will depend on which ACE law has been specified for the data.

ACE Law 1 - Tabular Equiprobable Energy Bins

In the tabular equiprobable bin representation, an array of equiprobable outgoing energy bins is given for a number of incident 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 [Doyas]. 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 (18). Then, we interpolate between the minimum and maximum energies of the outgoing energy distributions corresponding to \(E_i\) and \(E_{i+1}\):

(31)\[\begin{split}E_{min} = E_{i,1} + f ( E_{i+1,1} - E_i ) \\ E_{max} = E_{i,M} + f ( E_{i+1,M} - E_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. Now, we randomly sample an equiprobable outgoing energy bin \(j\) and interpolate between successive values on the outgoing energy distribution:

(32)\[\hat{E} = E_{\ell,j} + \xi_2 (E_{\ell,j+1} - E_{\ell,j})\]

where \(\xi_2\) is a random number sampled uniformly on \([0,1)\). Since this outgoing energy may violate reaction kinematics, we then scale it to the minimum and maximum energies we calculated earlier to get the final outgoing energy:

(33)\[E' = E_{min} + \frac{\hat{E} - E_{\ell,1}}{E_{\ell,M} - E_{\ell,1}} (E_{max} - E_{min})\]
ACE Law 3 - 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:

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

ACE Law 4 - Continuous Tabular Distribution

This representation is very similar to ACE Law 1 - Tabular Equiprobable Energy Bins except that instead of equiprobable outgoing energy bins, the outgoing energy distribution for each incoming energy is represented with a probability distribution function. 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 proceed first as we did for ACE Law 1, determining the bounding energies of the particle’s incoming energy such that \(E_i < E < E_{i+1}\) and calculating an interpolation factor \(f\) with equation (18). 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. Then, we sample an outgoing energy bin \(j\) using the cumulative distribution function:

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

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

(37)\[\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 interpolated between the neighboring outgoing energy distributions to get the final outgoing energy:

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

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

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

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

(41)\[0 \le E' \le E - U\]

where \(U\) is called the restriction energy and is specified on the ACE table. If the outgoing energy is rejected, it is resampled using equation (40).

ACE Law 9 - 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

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

(43)\[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 (41). This algorithm has a much higher rejection efficiency than the standard technique, i.e. rule C45 in the Monte Carlo Sampler.

ACE Law 11 - Energy-Dependent Watt Spectrum

The probability distribution for a Watt fission spectrum can be written in the form

(44)\[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 ACE Law 7 - Maxwell Fission Spectrum to get an energy \(W\). Then, the outgoing energy is calculated as

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

This algorithm can be found in Forrest Brown’s lectures on Monte Carlo methods and is an unpublished sampling scheme based on the original Watt spectrum derivation [Watt].

ACE Law 44 - Kalbach-Mann Correlated Scattering

This law is very similar to ACE Law 4 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 ACE Law 4.

The calculation of the outgoing energy of the neutron proceeds exactly the same as in the algorithm described in ACE Law 4 - 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 (38), 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

(46)\[\begin{split}R = R_{\ell,j} \\ A = A_{\ell,j}.\end{split}\]

If linear-linear interpolation is specified, the parameters are

(47)\[\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 (37). With the parameters determined, the probability distribution function for the cosine of the scattering angle is

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

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

(50)\[\mu = \frac{1}{A} \ln \left ( \xi_4 e^A + (1 - \xi_4) e^{-A} \right ).\]
ACE Law 61 - Correlated Energy and Angle Distribution

This law is very similar to ACE Law 44 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 ACE Law 4 - 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 (38), 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.

ACE Law 66 - 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 of the \(i\)-th particle in the center-of-mass system:

(51)\[p_i(E') dE' = C_n \sqrt{E'} (E_i^{max} - E')^{(3n/2) - 4} dE'\]

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

(52)\[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 ACE Law 7 - 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

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

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

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

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

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

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

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

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:

(60)\[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 (59). 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 (60) to obtain a normalization factor:

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

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

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

(64)\[d\mathbf{v}_T = v_T^2 dv_T d\mu d\phi\]

Let us define the Maxwellian distribution in speed as

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

(66)\[\beta = \sqrt{\frac{m}{2kT}}.\]

Substituting equation (66) into equation (65), we obtain

(67)\[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 (62) by using the result from equation (65), our new probability distribution function is

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

(69)\[2 v_n v_T \mu = v_n^2 + v_T^2 - v_r^2.\]

Thus, we can infer that

(70)\[|| \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 (70) into (68), we obtain

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

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

(73)\[q(x) dx = \frac{f_2(x) dx}{\int f_2(x) dx}\]

and accepting it with probability

(74)\[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 (73). To determine \(q(v_T)\), we need to integrate \(f_2\) in equation (72). Doing so we find that

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

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

(77)\[\begin{split}x = \beta v_T \\ y = \beta v_n.\end{split}\]

Substituting equation (77) into equation (76) along with \(dx = \beta dv_T\) and doing some crafty rearranging of terms yields

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

(79)\[\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 (77). 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 (74). 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

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

Energy-Dependent Cross Section Model

As was noted earlier, assuming that the elastic scattering cross section is constant in (60) 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:

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

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

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

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

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

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

where the interpolation factor is defined as

(86)\[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 (86). Then, an outgoing energy bin is sampled from a uniform distribution and then interpolated between values corresponding to neighboring incoming energies:

(87)\[E = E_{i,j} + f (E_{i+1,j} - E_{i,j})\]

where \(E_{i,j}\) is the j-th outgoing energy corresponding to the i-th incoming energy. For each combination of incoming and outgoing energies, there is a series equiprobable outgoing cosines. An outgoing cosine bin is sampled uniformly and then the final cosine is interpolated on the incoming energy grid:

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

where \(\mu_{i,j,k}\) is the k-th outgoing cosine corresponding to the j-th outgoing energy and the i-th incoming energy.

Skewed Equi-Probable Outgoing Energies

If the thermal data was processed with \(iwt=0\) in NJOY, then the outgoing energy spectra is represented in the ACE data according to the following: the first and last outgoing energies have a relative probability of 1, the second and second-to-last energies have a relative probability of 4, and all other energies have a relative probability of 10. The procedure to determine the outgoing energy and angle is similar to the method discussed above, except that the sampled probability distribution is now skewed accordingly.

Continuous Outgoing Energies

If the thermal data was processed with \(iwt=2\) in NJOY, then the outgoing energy spectra is represented by a continuous outgoing energy spectra in tabular form with linear-linear interpolation. The sampling of the outgoing energy portion of this format is very similar to ACE Law 61, 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 (38), we then need to decide which angular distribution data to use. Like the linear-linear interpolation case in Law 61, the angular distribution closest to the sampled value of the cumulative distribution function for the outgoing energy is utilized. The actual algorithm utilized to sample the outgoing angle is shown in equation (88).

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 [Levitt]. For most thermal reactors, the use of probability tables will not significantly affect problem results. However, for some fast reactors and other problems with an appreciable flux spectrum in the unresolved resonance range, not using probability tables may lead to incorrect results.

Probability tables in the ACE format are generated from the UNRESR module in NJOY following the method of Levitt. A similar method employed for the RACER and MC21 Monte Carlo codes is described in a paper by Sutton and Brown. For the discussion here, we will focus only on use of the probability table table as it appears in the ACE format.

Each probability table for a nuclide contains the following information at a number of incoming energies within the unresolved resonance range:

  • Cumulative probabilities for cross section bands;
  • Total cross section (or factor) in each band;
  • Elastic scattering cross section (or factor) in each band;
  • Fission cross section (or factor) in each band;
  • \((n,\gamma)\) cross section (or factor) in each band; and
  • Neutron heating number (or factor) in each band.

It should be noted that unresolved resonance probability tables affect only integrated cross sections and no extra data need be given for secondary angle/energy distributions. Secondary distributions for elastic and inelastic scattering would be specified whether or not probability tables were present.

The procedure for determining cross sections in the unresolved range using probability tables is as follows. First, the bounding incoming energies are determined, i.e. find \(i\) such that \(E_i < E < E_{i+1}\). We then sample a cross section band \(j\) using the cumulative probabilities for table \(i\). This allows us to then calculate the elastic, fission, and capture cross sections from the probability tables interpolating between neighboring incoming energies. If interpolation is specified, then the cross sections are calculated as

(89)\[\sigma = \sigma_{i,j} + f (\sigma_{i+1,j} - \sigma{i,j})\]

where \(\sigma_{i,j}\) is the j-th band cross section corresponding to the i-th incoming neutron energy and \(f\) is the interpolation factor defined in the same manner as (86). If logarithmic interpolation is specified, the cross sections are calculated as

(90)\[\sigma = \exp \left ( \log \sigma_{i,j} + f \log \frac{\sigma_{i+1,j}}{\sigma_{i,j}} \right )\]

where the interpolation factor is now defined as

(91)\[f = \frac{\log \frac{E}{E_i}}{\log \frac{E_{i+1}}{E_i}}.\]

A flag is also present in the probability table that specifies whether an inelastic cross section should be calculated. If so, this is done from a normal reaction cross section (either MT=51 or a special MT). Finally, if the cross sections defined are above are specified to be factors and not true cross sections, they are multiplied by the underlying smooth cross section in the unresolved range to get the actual cross sections. Lastly, the total cross section is calculated as the sum of the elastic, fission, capture, and inelastic cross sections.

Variance Reduction Techniques

Survival Biasing

In problems with highly absorbing materials, a large fraction of neutrons may be killed through absorption reactions, thus leading to tallies with very few scoring events. To remedy this situation, an algorithm known as survival biasing or implicit absorption (or sometimes implicit capture, even though this is a misnomer) is commonly used.

In survival biasing, absorption reactions are prohibited from occurring and instead, at every collision, the weight of neutron is reduced by probability of absorption occurring, i.e.

(92)\[w' = w \left ( 1 - \frac{\sigma_a (E)}{\sigma_t (E)} \right )\]

where \(w'\) is the weight of the neutron after adjustment and \(w\) is the weight of the neutron before adjustment. A few other things need to be handled differently if survival biasing is turned on. Although fission reactions never actually occur with survival biasing, we still need to create fission sites to serve as source sites for the next generation in the method of successive generations. The algorithm for sampling fission sites is the same as that described in Fission. The only difference is in equation (14). We now need to produce

(93)\[\nu = \frac{w}{k} \frac{\nu_t \sigma_f(E)}{\sigma_t (E)}\]

fission sites, where \(w\) is the weight of the neutron before being adjusted. One should note this is just the expected number of neutrons produced per collision rather than the expected number of neutrons produced given that fission has already occurred.

Additionally, since survival biasing can reduce the weight of the neutron to very low values, it is always used in conjunction with a weight cutoff and Russian rouletting. Two user adjustable parameters \(w_c\) and \(w_s\) are given which are the weight below which neutrons should undergo Russian roulette and the weight should they survive Russian roulette. The algorithm for Russian rouletting is as follows. After a collision if \(w < w_c\), then the neutron is killed with probability \(1 - w/w_s\). If it survives, the weight is set equal to \(w_s\). One can confirm that the average weight following Russian roulette is simply \(w\), so the game can be considered “fair”. By default, the cutoff weight in OpenMC is \(w_c = 0.25\) and the survival weight is \(w_s = 1.0\). These parameters vary from one Monte Carlo code to another.

References

[Doyas]Richard J. Doyas and Sterrett T. Perkins, “Interpolation of Tabular Secondary Neutron and Photon Energy Distributions,” Nucl. Sci. Eng., 50, 390-392 (1972).
[Gelbard]Ely M. Gelbard, “Epithermal Scattering in VIM,” FRA-TM-123, Argonne National Laboratory (1979).
[Levitt]Leo B. Levitt, “The Probability Table Method for Treating Unresolved Neutron Resonances in Monte Carlo Calculations,” Nucl. Sci. Eng., 49, pp. 450-457 (1972).
[SIGMA1]Dermett E. Cullen and Charles R. Weisbin, “Exact Doppler Broadening of Tabulated Cross Sections,” Nucl. Sci. Eng., 60, pp. 199-229 (1976).
[Squires]G. L. Squires, Introduction to the Theory of Thermal Neutron Scattering, Cambridge University Press (1978).
[Watt]B. E. Watt, “Energy Spectrum of Neutrons from Thermal Fission of U235,” Phys. Rev., 87 (6), 1037-1041 (1952).
[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 approximation from [George]:

(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

[George]E. E. Olusegun George and Meenakshi Sivaram, “A modification of the Fisher-Cornish approximation for the student t percentiles,” Communication in Statistics - Simulation and Computation, 16 (4), pp. 1123-1132 (1987).

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 neutrons moving around randomly in a nuclear reactor (or other fissile system). This is what’s known as Monte Carlo simulation. Neutrons are important in nuclear reactors because they are the particles that induce fission in uranium and other nuclides. Knowing the behavior of neutrons allows you 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 nuclear reactor 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 in a reactor 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 should be split up into regions of homogeneous material.
  • 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. Remember, 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, Mac OS X, 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. It is also helpful to be familiar with Python, as most of the post-processing utilities provided with OpenMC rely on it for data manipulation and results visualization.

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

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.6.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-get install gfortran
    
  • 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-get 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-get install libhdf5-8 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-get install mpich libmpich-dev
    sudo apt-get install openmpi-bin libopenmpi1.6 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.
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 environment variable to the path to the MPI Fortran wrapper. For example, in a bash shell:

export FC=mpif90
cmake /path/to/openmc

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

FC=mpif90 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
Using Cygwin

One option for compiling OpenMC on a Windows operating system is to use Cygwin, a Linux-like environment for Windows. You will need to first install Cygwin. When you are asked to select packages, make sure the following are selected:

  • Devel: gcc-core
  • Devel: gcc-fortran
  • Devel: make
  • Devel: cmake

If you plan on obtaining the source code directly using git, select the following packages:

  • Devel: git
  • Devel: git-completion (Optional)
  • Devel: gitk (Optional)

In order to use the Python scripts provided with OpenMC, you will also need to install Python. This can be done within Cygwin or directly in Windows. To install within Cygwin, select the following packages:

  • Python: python (Version > 2.7 recommended)

Once you have obtained the source code, run the following commands from within the source code root directory:

mkdir build && cd build
cmake ..
make

This will build an executable named openmc.

Using MinGW

An alternate option for installing OpenMC on Windows is using MinGW, which stands for Minimalist GNU for Windows. An executable for installing the MinGW distribution is available on SourceForge. When installing MinGW, make sure the following components are selected:

  • MinGW Compiler Suite: Fortran Compiler
  • MSYS Basic System

Once MinGW is installed, copy the OpenMC source distribution to your MinGW home directory (usually C:\MinGW\msys\1.0\home\YourUsername). Once you have the source code in place, run the following commands from within the MinGW shell in the root directory of the OpenMC distribution:

make

This will build an executable named openmc.

Compiling for the Intel Xeon Phi

In order to build OpenMC for the Intel Xeon Phi 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 FFLAGS=-mmic cmake -Dopenmp=on ..
make

Note that unless an HDF5 build for the Intel Xeon Phi 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.

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 ACE format cross sections; in this case you can use nuclear data that was processed with NJOY, such as that distributed with MCNP or Serpent. Several sources provide free processed ACE data as described below. The TALYS-based evaluated nuclear data library, TENDL, is also openly available in ACE format.

In multi-group mode, OpenMC utilizes an XML-based library format which can be used to describe nuclide- or material-specific quantities.

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 four temperatures processed using NJOY. To use this data with OpenMC, a script is provided with OpenMC that will automatically download, extract, and set up a confiuration file:

cd openmc/data
python get_nndc_data.py

At this point, you should set the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the file openmc/data/nndc/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 nuclear library upon request. A DVD of the data can be requested here. To use this data with OpenMC, the following steps must be taken:

  1. Copy and unzip the data on the DVD to a directory on your computer.

  2. In the root directory, a file named xsdir, or some variant thereof, should be present. This file contains a listing of all the cross sections and is used by MCNP. This file should be converted to a cross_sections.xml file for use with OpenMC. A utility is provided in the OpenMC distribution for this purpose:

    openmc/scripts/openmc-xsdir-to-xml xsdir31 cross_sections.xml
    
  3. In the converted cross_sections.xml file, change the contents of the <directory> element to the absolute path of the directory containing the actual ACE files.

  4. Additionally, you may need to change any occurrences of upper-case “ACE” within the cross_sections.xml file to lower-case.

  5. Either set the <cross_sections> Element in a settings.xml file or the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the cross_sections.xml file.

Using Cross Sections from MCNP

To use cross sections distributed with MCNP, change the <directory> element in the cross_sections.xml file in the root directory of the OpenMC distribution to the location of the MCNP cross sections. Then, either set the <cross_sections> Element in a settings.xml file or the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the cross_sections.xml file.

Using Cross Sections from Serpent

To use cross sections distributed with Serpent, change the <directory> element in the cross_sections_serpent.xml file in the root directory of the OpenMC distribution to the location of the Serpent cross sections. Then, either set the <cross_sections> Element in a settings.xml file or the OPENMC_CROSS_SECTIONS environment variable to the absolute path of the cross_sections_serpent.xml file.

Using 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 the user has 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.

Running OpenMC

Once you have a model built (see Writing XML Input Files), 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.

Command-Line Flags

OpenMC accepts the following command line flags:

-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

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.

Writing XML Input Files

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.

Overview of Files

To assemble a complete model for OpenMC, one needs to create separate XML files for the geometry, materials, and settings. Additionally, there are three optional input files. The first is a tallies XML file that specifies physical quantities to be tallied. The second is a plots XML file that specifies regions of geometry which should be plotted. The third is a CMFD XML file that specifies coarse mesh acceleration geometry and execution parameters. OpenMC expects that these files are called:

  • geometry.xml
  • materials.xml
  • settings.xml
  • tallies.xml
  • plots.xml
  • cmfd.xml

Validating XML Files

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. Default: current working directory
  • -r, --relaxng-path - Location of OpenMC RelaxNG files. Default: None

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.

As an example, if OpenMC is installed in the directory /opt/openmc/ and the current working directory is where OpenMC XML input files are located, they can be validated using the following command:

/opt/openmc/bin/openmc-validate-xml

Settings Specification – settings.xml

All simulation parameters and miscellaneous options are specified in the settings.xml file.

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

<cutoff> Element

The <cutoff> element indicates 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. 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

<eigenvalue> Element

The <eigenvalue> element indicates that a \(k\)-eigenvalue calculation should be performed. It has the following attributes/sub-elements:

batches:

The total number of batches, where each batch corresponds to multiple fission source iterations. Batching is done to eliminate correlation between realizations of random variables.

Default: None

generations_per_batch:
 

The number of total fission source iterations per batch.

Default: 1

inactive:

The number of inactive batches. In general, the starting cycles in a criticality calculation can not be used to contribute to tallies since the fission source distribution and eigenvalue are generally not converged immediately.

Default: None

particles:

The number of neutrons to simulate per fission source iteration.

Default: None

keff_trigger:

This tag 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.

<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

<fixed_source> Element

The <fixed_source> element indicates that a fixed source calculation should be performed. It has the following attributes/sub-elements:

batches:

The total number of batches. For fixed source calculations, each batch represents a realization of random variables for tallies.

Default: None

particles:

The number of particles to simulate per batch.

Default: None

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

<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 <use_windowed_multipole> element must also be set to “true” for windowed multipole functionality.

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

<natural_elements> Element

The <natural_elements> element indicates to OpenMC what nuclides are available in the cross section library when expanding an <element> into separate isotopes (see <material> Element). The accepted values are:

  • ENDF/B-VII.0
  • ENDF/B-VII.1
  • JEFF-3.1.1
  • JEFF-3.1.2
  • JEFF-3.2
  • JENDL-3.2
  • JENDL-3.3
  • JENDL-4.0

Note that the value is case-insensitive, so “ENDF/B-VII.1” is equivalent to “endf/b-vii.1”.

Default: ENDF/B-VII.1
<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.

cross_sections:

Writes out an ASCII summary file of the cross sections that were read in.

Default: false

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.

<output_path> Element

The <output_path> element specifies an 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
<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 can contain one or more of the following attributes or sub-elements:

scatterer:

An element with attributes/sub-elements called nuclide, method, xs_label, xs_label_0K, E_min, and E_max. The nuclide attribute is the name, as given by the name attribute within the nuclide sub-element of the material element in materials.xml, of the nuclide to which a resonance scattering treatment is to be applied. The method attribute gives the type of resonance scattering treatment that is to be applied to the nuclide. Acceptable inputs - none of which are case-sensitive - for the method attribute are ARES, CXS, WCM, and DBRC. Descriptions of each of these methods are documented here. The xs_label attribute gives the label for the cross section data of the nuclide at a given temperature. The xs_label_0K gives the label for the 0 K cross section data for the nuclide. The E_min attribute gives the minimum energy above which the method is applied. The E_max attribute gives the maximum energy below which the method is applied. One example would be as follows:

<resonance_scattering>
  <scatterer>
    <nuclide>U-238</nuclide>
    <method>ARES</method>
    <xs_label>92238.72c</xs_label>
    <xs_label_0K>92238.00c</xs_label_0K>
    <E_min>5.0e-6</E_min>
    <E_max>40.0e-6</E_max>
 </scatterer>
 <scatterer>
    <nuclide>Pu-239</nuclide>
    <method>dbrc</method>
    <xs_label>94239.72c</xs_label>
    <xs_label_0K>94239.00c</xs_label_0K>
    <E_min>0.01e-6</E_min>
    <E_max>210.0e-6</E_max>
  </scatterer>
</resonance_scattering>

Note

If the resonance_scattering element is not given, the free gas, constant cross section (cxs) 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 cxs method is applied below E_min and the target-at-rest (asymptotic) kernel is used above E_max. An arbitrary number of scatterer elements may be specified, each corresponding to a single nuclide at a single temperature.

Defaults: None (scatterer), ARES (method), 0.01 eV (E_min), 1.0 keV (E_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”.

Defualt: false
<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).

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

interval:

A single integer \(n\) indicating that a state point should be written every \(n\) batches. This option can be given in lieu of listing batches explicitly.

Default: None

<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

interval:

A single integer \(n\) indicating that a state point should be written every \(n\) batches. This option can be given in lieu of listing batches explicitly. It should be noted that if the separate attribute is not set to “true”, this value should produce a list of batches that is a subset of state point batches.

Default: None

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

<use_windowed_multipole> Element

The <use_windowed_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.

Default: False
<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. This element takes the following attributes:

value:

The specified verbosity between 1 and 10.

Default: 5

Geometry Specification – geometry.xml

The geometry in OpenMC is described using constructive solid geometry (CSG), also sometimes referred to as combinatorial geometry. CSG allows a user to create complex objects using Boolean operators on a set of simpler surfaces. In the geometry model, each unique volume is defined by its bounding surfaces. In OpenMC, most quadratic surfaces can be modeled and used as bounding surfaces.

Every geometry.xml must have an XML declaration at the beginning of the file and a root element named geometry. Within the root element the user can define any number of cells, surfaces, and lattices. Let us look at the following example:

<?xml version="1.0"?>
<geometry>
  <!-- This is a comment -->

  <surface>
    <id>1</id>
    <type>sphere</type>
    <coeffs>0.0 0.0 0.0 5.0</coeffs>
    <boundary>vacuum</boundary>
  <surface>

  <cell>
    <id>1</id>
    <universe>0</universe>
    <material>1</material>
    <region>-1</region>
  </cell>
</geometry>

At the beginning of this file is a comment, denoted by a tag starting with <!-- and ending with -->. Comments, as well as any other type of input, may span multiple lines. One convenient feature of the XML input format is that sub-elements of the cell and surface elements can also be equivalently expressed of attributes of the original element, e.g. the geometry file above could be written as:

<?xml version="1.0"?>
<geometry>
  <!-- This is a comment -->

  <surface id="1" type="sphere" coeffs="0.0 0.0 0.0 5.0" boundary="vacuum" />
  <cell id="1" universe="0" material="1" region="-1" />

</geometry>
<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: The temperature of the coldest nuclide in the cell’s material(s)

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

<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: “”

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, xs, and ao or wo. The name attribute is the name of the cross-section for a desired nuclide while the xs attribute is the cross-section identifier. 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="H-1" xs="70c" ao="2.0" />
<nuclide name="O-16" xs="70c" 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.

element:

Specifies that a natural element is present in the material. The natural element is split up into individual isotopes based on IUPAC Isotopic Compositions of the Elements 2009. This element has attributes/sub-elements called name, xs, and ao. The name attribute is the atomic symbol of the element while the xs attribute is the cross-section identifier. Finally, the ao attribute specifies the atom percent of the element within the material, respectively. One example would be as follows:

<element name="Al" ao="8.7115e-03" />
<element name="Mg" ao="1.5498e-04" />
<element name="Mn" ao="2.7426e-05" />
<element name="Cu" ao="1.6993e-04" />

In some cross section libraries, certain naturally occurring isotopes do not have cross sections. The <natural_elements> Element option determines how a natural element is split into isotopes in these cases.

Default: None

An optional attribute/sub-element for each element 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 attributes/sub-elements called name and xs. The name attribute is the name of the S(a,b) table that should be associated with the material, and xs is the cross-section identifier for the table.

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 attributes/sub-elements called name, and xs. The name attribute is the name of the cross-section for a desired nuclide while the xs attribute is the cross-section identifier. One example would be as follows:

<macroscopic name="UO2" xs="71c" />

Note

This element is only used in the multi-group <energy_mode> Element.

Default: None

<default_xs> Element

In some circumstances, the cross-section identifier may be the same for many or all nuclides in a given problem. In this case, rather than specifying the xs=... attribute on every nuclide, a <default_xs> element can be used to set the default cross-section identifier for any nuclide without an identifier explicitly listed. This element has no attributes and accepts a 3-letter string that indicates the default cross-section identifier, e.g. “70c”.

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 restricts contributions to the tally to particles within certain regions of phase space. 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”, “mesh”, “distribcell”, and “delayedgroup”.

bins:

For each filter type, the corresponding bins entry is given as follows:

cell:

A list of 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.

surface:

A list of surfaces for which the tally should be accumulated.

material:

A list of materials for which the tally should be accumulated.

universe:

A list of universes for 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.0 20.0" />

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.0 20.0" />

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

Note

This filter type is not used in the multi-group <energy_mode> Element.

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. The accepted options are listed in the following tables:

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. This score type is not used in the multi-group <energy_mode> Element.
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. This score type is not used in the multi-group <energy_mode> Element.
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 MeV per source particle.

Note

The analog estimator is actually identical to the collision estimator for the flux and inverse-velocity scores.

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”

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

<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 command-line flag -plot. 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:

Keyword for plot coloring. This can only be either cell or mat, 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 utility provided with the OpenMC source, 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)

col_spec:

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 col_spec 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 col_spec element would look like:

<col_spec 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 col_spec color specifications.

Default: None

meshlines:

The meshlines sub-element allows for plotting the boundaries of a tally 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 MeV], listed in ascending order (e.g. 0.0 0.625e-7 20.0) 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

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.

Data Processing and Visualization

This section is intended to explain in detail the recommended 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. Both the provided scripts and the Python API rely on a number third-party Python packages, including:

Most of these are can easily be installed with pip or alternatively obtaining through a package manager.

[1]Required for most post-processing tasks
[2]Required for reading HDF5 output files
[3]Optional dependency for advanced features in Python API
[4](1, 2, 3, 4) Not used directly by the Python API, but are optional dependencies for a number of scripts.

Geometry Visualization

Geometry plotting is carried out by creating a plots.xml, specifying plots, and running OpenMC with the –plot or -p command-line option (See Geometry Plotting Specification – plots.xml).

Plotting in 2D
_images/atr.png

See below for a simple example of a plots xml file that demonstrates the capabilities of 2D slice plots. Here we assume that there is a geometry.xml file containing 7 cells.

<?xml version="1.0" encoding="UTF-8"?>
<plots>

  <plot id="1" type="slice" color="cell" basis="xy">
      <filename> myplot </filename>
      <origin> 0 0 </origin>
      <width> 10 10 </width>
      <pixels> 2000 2000 </pixels>
      <background> 0 0 0 </background>
      <col_spec id="1" rgb="198 226 255"/>
      <col_spec id="2" rgb="255 218 185"/>
      <col_spec id="3" rgb="255 255 255"/>
      <col_spec id="4" rgb="101 101 101"/>
      <col_spec id="7" rgb="123 123 231"/>
      <mask background="255 255 255">
        <components> 1 3 4 5 6 </components>
      </mask>
  </plot>

</plots>

In this example, OpenMC will produce a plot named myplot.ppm when run in plotting mode. The picture will be on the xy-plane, depicting the rectangle between points (-5,-5) and (5,5) with 2000 pixels along each dimension. 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. In this example, pixels are 10/2000=0.005 cm wide, so points will be at (-4.9975,-4.9975), (-4.9950,-4.9975), (-4.9925,-4.9975), etc. This is pointed out to demonstrate that this plot may miss any features smaller than 0.005 cm, since they could exist between pixel centers. More pixels can be used to resolve finer features, but could result in larger files.

The background, col_spec, and mask elements define how to set pixel colors based on the cell ids at each pixel center. In this example, RGB colors are specified for cells 1,2,3,4, and 7, a random color will be assigned to cells 5 and 6, and a black background color (rgb="0 0 0") will be applied to locations where no cell is defined. However, the mask element here says that only cells 1,3,4,5, and 6 should be displayed, with other cells taking a white color (rgb="255 255 255"), which overrides the col_spec for cell 2 and the random color assigned to cell 7.

After running OpenMC to obtain PPM files, images should be saved to another format before using them elsewhere. This cuts down the size of the file by orders of magnitude. Most image viewers and editors that can view PPM images can also save to other formats (e.g. Gimp, IrfanView, etc.). However, more likely the user will want to convert to another format on the command line. This is easily accomplished with the convert command available on most Linux distributions as part of the ImageMagick package. (On Ubuntu: sudo apt-get install imagemagick). Images are then converted like:

convert myplot.ppm myplot.png
Plotting in 3D
_images/3dgeomplot.png

See below for a simple example of a plots xml file that demonstrates the capabilities of 3D voxel plots.

<?xml version="1.0" encoding="UTF-8"?>
<plots>

  <plot id="1" type="voxel" color="mat">
      <filename> myplot </filename>
      <origin> 0 0 0 </origin>
      <width> 10 10 10 </width>
      <pixels> 500 500 500 </pixels>
  </plot>

</plots>

Voxel plots are built the same way 2D slice plots are, by determining the cell or material id of a particle at the center of each voxel. In this example, the space covered is the cube between the points (-5,-5,-5) and (5,5,5), with voxel centers 10/500 = 0.02 cm apart. The HDF5 voxel files that are produced do not specify any color - instead containing only material or cell ids (material id in this example) - and thus the background, col_spec, and mask elements are not used. If no cell is found at a voxel center, an id of -1 is stored.

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 utility openmc-voxel-to-silovtk accomplishes this for SILO:

openmc-voxel-to-silovtk myplot.voxel -o output.silo

and VTK file formats:

openmc-voxel-to-silovtk myplot.voxel --vtk -o output.vti

To use this utility you need either

or

  • VTK with python bindings. On debian derivatives, these are easily obtained with sudo apt-get install python-vtk

For the HDF5 file structure, see Voxel Plot File Format.

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.

_images/3dba.png

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. For instance, the 3D pin lattice figure at the beginning of this section was generated with a 500x500x1 voxel mesh, which allows for resolution of the cylinders without wasting too many voxels on the axial dimension.

Tally Visualization

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. Note, however, that there is also a script distributed with OpenMC, openmc-plot-mesh-tally, that provides an interactive GUI to explore and plot mesh tallies for any scores and filter bins.

_images/plotmeshtally.png
Plotting in 3D
_images/3dcore.png

As with 3D plots of the geometry, meshtally data needs to be put into a standard format for viewing. The utility openmc-statepoint-3d is provided to accomplish this for both VTK and SILO. By default openmc-statepoint-3d processes a statepoint into a 3D file with all mesh tallies and filter/score combinations,

openmc-statepoint-3d <statepoint_file> -o output.silo
openmc-statepoint-3d <statepoint_file> --vtk -o output.vtm

but it also provides several command-line options to selectively process only certain data arrays in order to keep file sizes down.

openmc-statepoint-3d <statepoint_file> --tallies 2,4 --scores 4.1,4.3 -o output.silo
openmc-statepoint-3d <statepoint_file> --filters 2.energyin.1 --vtk -o output.vtm

All available options for specifying a subset of tallies, scores, and filters can be listed with the --list or -l command line options.

Note

Note that while SILO files can contain multiple meshes in one file, VTK needs to use a multi-block dataset, which stores each mesh piece in a different file in a subfolder. All meshes can be loaded at once with the main VTM file, or each VTI file in the subfolder can be loaded individually.

Alternatively, the user can write their own Python script to manipulate the data appropriately before insertion into a SILO or VTK file. For instance, if the data has been extracted as was done in the 2D plotting example script above, a SILO file can be created with:

import silomesh as sm
sm.init_silo("fluxtally.silo")
sm.init_mesh('tally_mesh', *mesh.dimension, *mesh.lower_left, *mesh.upper_right)
sm.init_var('flux_tally_thermal')
for x in range(1,nx+1):
  for y in range(1,ny+1):
      for z in range(1,nz+1):
        sm.set_value(float(thermal[(x,y,z)]),x,y,z)
sm.finalize_var()
sm.init_var('flux_tally_fast')
for x in range(1,nx+1):
  for y in range(1,ny+1):
      for z in range(1,nz+1):
          sm.set_value(float(fast[(x,y,z)]),x,y,z)
sm.finalize_var()
sm.finalize_mesh()
sm.finalize_silo()

and the equivalent VTK file with:

import vtk

grid = vtk.vtkImageData()
grid.SetDimensions(nx+1,ny+1,nz+1)
grid.SetOrigin(*mesh.lower_left)
grid.SetSpacing(*mesh.width)

# vtk cell arrays have x on the inners, so we need to reorder the data
idata = {}
for x in range(nx):
  for y in range(ny):
    for z in range(nz):
      i = z*nx*ny + y*nx + x
      idata[i] = (x,y,z)

vtkfastdata = vtk.vtkDoubleArray()
vtkfastdata.SetName("fast")
for i in range(nx*ny*nz):
  vtkfastdata.InsertNextValue(fast[idata[i]])

vtkthermaldata = vtk.vtkDoubleArray()
vtkthermaldata.SetName("thermal")
for i in range(nx*ny*nz):
  vtkthermaldata.InsertNextValue(thermal[idata[i]])

grid.GetCellData().AddArray(vtkfastdata)
grid.GetCellData().AddArray(vtkthermaldata)

writer = vtk.vtkXMLImageDataWriter()
writer.SetInput(grid)
writer.SetFileName('tally.vti')
writer.Write()
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”, “-track”, or “–track” will cause a track file to be created for every particle transported in the code.

The settings.xml file can dictate that specific particle tracks are output. These particles are specified within a ‘’track’’ element. The ‘’track’’ element should contain triplets of integers specifying the batch, generation, and particle numbers, respectively. For example, to output the tracks for particles 3 and 4 of batch 1 and generation 2 the settings.xml file should contain:

<track>
  1 2 3
  1 2 4
</track>

After running OpenMC, the directory should 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 utility. The usage of openmc-track-to-vtk is of the form “openmc-track-to-vtk [-o OUT] IN” where OUT is the optional output filename and IN is one or more filenames describing track files. The default output name is “track.pvtp”. A common usage of track.py is “openmc-track-to-vtk track*.h5” which will use the data from all binary track files in the directory to write a “track.pvtp” VTK output file. The .pvtp file can then be read and plotted by 3d visualization programs such as ParaView.

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.

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
    

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 under all conditions (MPI, OpenMP, HDF5, etc.). This is checked as part of the test suite.
  • Passes the regression suite.
  • 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. A wise developer would also check whether their changes do indeed pass the regression test suite.

  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. To download these cross sections please do the following:

cd ../data
python get_nndc_data.py
export CROSS_SECTIONS=<path_to_data_folder>/nndc/cross_sections.xml

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 of the Fortran compiler (e.g. gfotran, ifort).

    • Default - gfortran
  • MPI_DIR - The path to the MPI directory.

    • Default - /opt/mpich/3.1.3-gnu
  • HDF5_DIR - The path to the HDF5 directory.

    • Default - /opt/hdf5/1.8.14-gnu
  • PHDF5_DIR - The path to the parallel HDF5 directory.

    • Default - /opt/phdf5/1.8.14-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.
  • results.py - python script that extracts results from statepoint output files. By default it should look for a binary file, but can take an argument to overwrite which statepoint file is processed, whether it is at a different batch or with an HDF5 extension. This script must output a results file that is named results_test.dat. It is recommended that any real numbers reported use 12.6E format.
  • 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.

XML Input Parsing

OpenMC relies on the FoX Fortran XML library for reading and intrepreting the XML input files for geometry, materials, settings, tallies, etc. The use of an XML format makes writing input files considerably more flexible than would otherwise be possible.

With the FoX library, extending the user input files to include new tags is fairly straightforward. The steps for modifying/adding input are as follows:

  1. Add appropriate calls to procedures from the xml_interface module, such as check_for_node, get_node_value, and get_node_array. All input reading is performed in the input_xml module.
  2. 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”.
  3. Add code to check the variable for any possible errors.

A set of RELAX NG schemata exists that enables real-time validation of input files when using the GNU Emacs text editor. You should also modify the RELAX NG schema for the file you changed (e.g. src/relaxng/geometry.rnc) so that those who use Emacs can confirm whether their input is valid before they run. You will need to be familiar with RELAX NG compact syntax.

Working with the FoX Submodule

The FoX library is included as a submodule in OpenMC. This means that for a given commit in OpenMC, there is an associated commit id that links to FoX. The actual FoX source code is maintained at mit-crpg/fox, branch openmc. When cloning the OpenMC repo for the first time, you will notice that the directory src/xml/fox is empty. To fetch the submodule source code, you can manually enter the following from the root directory of OpenMC:

git submodule init
git submodule update

It should be noted that if the submodule is not initialized and updated, cmake will automatically perform these commands if it cannot file the FoX source code.

If you navigate into the FoX source code in OpenMC, src/xml/fox, and check git information, you will notice that you are in a completely different repo. Actually, you are in a clone of mit-crpg/fox. If you have write access to this repo, you can make changes to the FoX source code, commit and push just like any other repo. Just because you make changes to the FoX source code in OpenMC or in a standalone repo, this does not mean that OpenMC will automatically fetch these changes. The way submodules work is that they are just stored as a commit id. To save FoX xml source changes to your OpenMC branch, do the following:

  1. Go into src/xml/fox and check out the appropriate source code state
  2. Navigate back out of fox subdirectory and type:
git status
  1. Make sure you see that git recognized that the state of FoX changed:
# On branch fox_submodule
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   fox (new commits)
  1. Commit and push this change

Editing FoX on Personal Fork

If you don’t have write access to mit-crpg/fox and thus can’t make a branch off of the openmc branch there, you will need to fork mit-crpg/fox to your personal account. You need to then link your branch in your OpenMC repo, to the openmc branch on your own personal FoX fork. To do this, edit the .gitmodules file in the root folder of the repo. It contains the following information:

[submodule "src/xml/fox"]
    path = src/xml/fox
    url = git@github.com:mit-crpg/fox

Change the url remote to your own fork. The commit id should stay constant until you start making modification to FoX yourself. Once you have made changes to your FoX fork and linked the new commit id to your OpenMC branch, you can pull request your changes in by peforming the following steps:

  1. Create a pull request from your fork of FoX to mit-crpg/fox and wait until it is merged into the openmc branch.
  2. In your OpenMC repo, change your .gitmodules file back to point at mit-crpg/fox.
  3. Submit a pull request to mit-crpg/openmc

Warning

If you make changes to your FoX submodule inside of an OpenMC repo and do not commit, do not run git submodule update. This may throw away any changes that were not committed.

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 Jupyter notebooks provided. However, this assumes that you are already familiar with Python and common third-party packages such as NumPy. If you have never programmed in Python before, 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.

Example Jupyter Notebooks

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('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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))
materials_file.default_xs = '71c'

# 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.Filter(type='mesh')
mesh_filter.mesh = 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()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.org/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       df280b60eb1c6d7b7f842e05ede734a4883a0fc8
      Date/Time:      2016-05-05 14:41:55

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 5010.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 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.03614                       
        9/1    1.05193                       
       10/1    1.02886                       
       11/1    1.05011                       
       12/1    1.04597    1.04804 +/- 0.00207
       13/1    1.07035    1.05548 +/- 0.00753
       14/1    1.06150    1.05698 +/- 0.00554
       15/1    1.07094    1.05977 +/- 0.00512
       16/1    1.05131    1.05836 +/- 0.00441
       17/1    1.04733    1.05679 +/- 0.00405
       18/1    1.08130    1.05985 +/- 0.00465
       19/1    1.02559    1.05605 +/- 0.00560
       20/1    1.03399    1.05384 +/- 0.00547
       21/1    1.04617    1.05314 +/- 0.00500
       22/1    1.06981    1.05453 +/- 0.00477
       23/1    1.05270    1.05439 +/- 0.00439
       24/1    1.02487    1.05228 +/- 0.00458
       25/1    1.05905    1.05273 +/- 0.00429
       26/1    1.07658    1.05422 +/- 0.00428
       27/1    1.03455    1.05307 +/- 0.00418
       28/1    1.00971    1.05066 +/- 0.00462
       29/1    1.06111    1.05121 +/- 0.00440
       30/1    1.01777    1.04954 +/- 0.00450
       31/1    1.04718    1.04942 +/- 0.00428
       32/1    1.03340    1.04870 +/- 0.00415
       33/1    1.04570    1.04857 +/- 0.00397
       34/1    1.02728    1.04768 +/- 0.00390
       35/1    1.02852    1.04691 +/- 0.00382
       36/1    1.03242    1.04636 +/- 0.00371
       37/1    1.01479    1.04519 +/- 0.00376
       38/1    1.06045    1.04573 +/- 0.00366
       39/1    1.03810    1.04547 +/- 0.00354
       40/1    1.05281    1.04571 +/- 0.00343
       41/1    1.03941    1.04551 +/- 0.00332
       42/1    1.04049    1.04535 +/- 0.00322
       43/1    1.04586    1.04537 +/- 0.00312
       44/1    1.05437    1.04563 +/- 0.00304
       45/1    1.03445    1.04531 +/- 0.00297
       46/1    1.05104    1.04547 +/- 0.00289
       47/1    1.00773    1.04445 +/- 0.00299
       48/1    1.06879    1.04509 +/- 0.00298
       49/1    1.06625    1.04564 +/- 0.00295
       50/1    1.02641    1.04515 +/- 0.00292
       51/1    1.05701    1.04544 +/- 0.00286
       52/1    1.02868    1.04504 +/- 0.00282
       53/1    1.04592    1.04506 +/- 0.00275
       54/1    1.05757    1.04535 +/- 0.00271
       55/1    1.02329    1.04486 +/- 0.00269
       56/1    1.04116    1.04478 +/- 0.00263
       57/1    1.01990    1.04425 +/- 0.00263
       58/1    1.06202    1.04462 +/- 0.00260
       59/1    1.03550    1.04443 +/- 0.00255
       60/1    1.01383    1.04382 +/- 0.00258
       61/1    1.04111    1.04377 +/- 0.00253
       62/1    1.02061    1.04332 +/- 0.00252
       63/1    1.00456    1.04259 +/- 0.00257
       64/1    1.02277    1.04222 +/- 0.00255
       65/1    1.04544    1.04228 +/- 0.00251
       66/1    1.04487    1.04233 +/- 0.00246
       67/1    1.02699    1.04206 +/- 0.00243
       68/1    1.06160    1.04240 +/- 0.00241
       69/1    1.02989    1.04218 +/- 0.00238
       70/1    1.03107    1.04200 +/- 0.00235
       71/1    1.06571    1.04239 +/- 0.00234
       72/1    1.03444    1.04226 +/- 0.00231
       73/1    1.05059    1.04239 +/- 0.00228
       74/1    1.03352    1.04225 +/- 0.00224
       75/1    1.03707    1.04217 +/- 0.00221
       76/1    1.02994    1.04199 +/- 0.00219
       77/1    1.05416    1.04217 +/- 0.00216
       78/1    1.03794    1.04211 +/- 0.00213
       79/1    1.04652    1.04217 +/- 0.00210
       80/1    1.05715    1.04239 +/- 0.00208
       81/1    1.08146    1.04294 +/- 0.00212
       82/1    1.02159    1.04264 +/- 0.00211
       83/1    1.01968    1.04233 +/- 0.00211
       84/1    1.05577    1.04251 +/- 0.00209
       85/1    1.07808    1.04298 +/- 0.00211
       86/1    1.03943    1.04293 +/- 0.00209
       87/1    1.03431    1.04282 +/- 0.00206
       88/1    1.02414    1.04258 +/- 0.00205
       89/1    1.02316    1.04234 +/- 0.00204
       90/1    1.03342    1.04223 +/- 0.00202
       91/1    1.02781    1.04205 +/- 0.00200
       92/1    1.01293    1.04169 +/- 0.00201
       93/1    1.04347    1.04171 +/- 0.00198
       94/1    1.05357    1.04186 +/- 0.00196
       95/1    1.04740    1.04192 +/- 0.00194
       96/1    1.05215    1.04204 +/- 0.00192
       97/1    1.06667    1.04232 +/- 0.00192
       98/1    1.04926    1.04240 +/- 0.00190
       99/1    1.05386    1.04253 +/- 0.00188
      100/1    1.05088    1.04262 +/- 0.00186
 Creating state point statepoint.100.h5...

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


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

 Total time for initialization     =  4.4900E-01 seconds
   Reading cross sections          =  1.2100E-01 seconds
 Total time in simulation          =  3.4132E+02 seconds
   Time in transport only          =  3.4128E+02 seconds
   Time in inactive batches        =  1.0748E+01 seconds
   Time in active batches          =  3.3057E+02 seconds
   Time synchronizing fission bank =  1.1000E-02 seconds
     Sampling source sites         =  1.1000E-02 seconds
     SEND/RECV source sites        =  0.0000E+00 seconds
   Time accumulating tallies       =  1.9000E-02 seconds
 Total time for finalization       =  1.5600E-01 seconds
 Total time elapsed                =  3.4196E+02 seconds
 Calculation Rate (inactive)       =  4652.03 neutrons/second
 Calculation Rate (active)         =  1361.27 neutrons/second

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

 k-effective (Collision)     =  1.04214 +/-  0.00161
 k-effective (Track-length)  =  1.04262 +/-  0.00186
 k-effective (Absorption)    =  1.04338 +/-  0.00158
 Combined k-effective        =  1.04278 +/-  0.00122
 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        =	
                		mesh	[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.40945685,  0.        ]],

       [[ 0.40939021,  0.        ]],

       [[ 0.410625  ,  0.        ]],

       ..., 
       [[ 0.41130501,  0.        ]],

       [[ 0.41228849,  0.        ]],

       [[ 0.41420317,  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.00454952,  0.        ]],
 
        [[ 0.00454878,  0.        ]],
 
        [[ 0.0045625 ,  0.        ]],
 
        ..., 
        [[ 0.00457006,  0.        ]],
 
        [[ 0.00458098,  0.        ]],
 
        [[ 0.00460226,  0.        ]]]),
 array([[[  1.64748193e-05,   0.00000000e+00]],
 
        [[  1.70922989e-05,   0.00000000e+00]],
 
        [[  1.67622385e-05,   0.00000000e+00]],
 
        ..., 
        [[  1.69274948e-05,   0.00000000e+00]],
 
        [[  1.57842763e-05,   0.00000000e+00]],
 
        [[  2.06590062e-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        =	
                		mesh	[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 0x7f06934ac4e0>

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.1382758052991497, -0.3512177196880901, -0.33148352853135243], [0.4185689312254184, 0.515870835450486, -0.7474472094699589], 0.12980877137486158, 0),
       (1.0, [-0.1382758052991497, -0.3512177196880901, -0.33148352853135243], [-0.7887279871402189, -0.41474066458733805, -0.4537602268152112], 0.9055673129689725, 0),
       (1.0, [-0.037141545763691594, 0.3164763430804079, -0.13705878399440294], [0.3855911650385466, 0.7867859791128493, -0.48196190359368796], 0.996776361155944, 0),
       ...,
       (1.0, [0.10735654583359178, -0.007276312264885898, -0.05173760352485271], [0.5349120379020222, -0.015907381482490993, 0.8447579931091957], 0.8409377635712494, 0),
       (1.0, [-0.048566216245400286, 0.29160532148505824, 0.31942717397594755], [0.6200960701401188, -0.409911862042636, 0.6689193741801173], 3.8787384499997857, 0),
       (1.0, [0.07888442268099938, -0.14443885526015943, 0.3095767587545024], [0.373899151360205, 0.18819594431853104, 0.9081749342247754], 4.776147593645684, 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([ 0.12980877,  0.90556731,  0.99677636, ...,  0.84093776,
        3.87873845,  4.77614759])

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,1)

# 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 (MeV)')
plt.ylabel('Probability/MeV')
1.0
Out[28]:
<matplotlib.text.Text at 0x7f0695818ba8>

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.

Note: that this Notebook was created using the latest Pandas v0.16.1. Everything in the Notebook will wun with older versions of Pandas, but the multi-indexing option in >v0.15.0 makes the tables look prettier.

In [1]:
%matplotlib inline
import glob
from IPython.display import Image
import matplotlib.pyplot as plt
import scipy.stats
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('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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))
materials_file.default_xs = '71c'

# 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 [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=-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 [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)

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 - 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 [8]:
# 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 [9]:
# Create Geometry and set root Universe
geometry = openmc.Geometry()
geometry.root_universe = root_universe
In [10]:
# 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 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 [11]:
# OpenMC simulation parameters
min_batches = 20
max_batches = 200
inactive = 5
particles = 2500

# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = min_batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}
settings_file.trigger_active = True
settings_file.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_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 [12]:
# 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 [13]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[13]:
0
In [14]:
# 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[14]:

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 [15]:
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
tallies_file._tallies = []

Instantiate a fission rate mesh Tally

In [16]:
# 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.Filter()
mesh_filter.mesh = mesh

# Instantiate energy Filter
energy_filter = openmc.Filter()
energy_filter.type = 'energy'
energy_filter.bins = np.array([0, 0.625e-6, 20.])

# 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_file.append(tally)

Instantiate a cell Tally with nuclides

In [17]:
# Instantiate tally Filter
cell_filter = openmc.Filter(type='cell', bins=[fuel_cell.id])

# 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_file.append(tally)

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

In [18]:
# Instantiate tally Filter
distribcell_filter = openmc.Filter(type='distribcell', bins=[moderator_cell.id])

# 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_file.append(tally)
In [19]:
# 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 [20]:
# Remove old HDF5 (summary, statepoint) files
!rm statepoint.*

# Run OpenMC!
openmc.run()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       ae588276014a905ecc6e0967bf08288ecec5b550
      Date/Time:      2016-05-09 23:01:18
      MPI Processes:  1

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 5010.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 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.68274                       
        7/1    0.66339    0.67307 +/- 0.00967
        8/1    0.65835    0.66816 +/- 0.00743
        9/1    0.66697    0.66786 +/- 0.00527
       10/1    0.70498    0.67528 +/- 0.00847
       11/1    0.68596    0.67706 +/- 0.00714
       12/1    0.68481    0.67817 +/- 0.00614
       13/1    0.68369    0.67886 +/- 0.00536
       14/1    0.68785    0.67986 +/- 0.00483
       15/1    0.66145    0.67802 +/- 0.00470
       16/1    0.71831    0.68168 +/- 0.00561
       17/1    0.68428    0.68190 +/- 0.00512
       18/1    0.67527    0.68139 +/- 0.00474
       19/1    0.68166    0.68141 +/- 0.00439
       20/1    0.65475    0.67963 +/- 0.00446
 Triggers unsatisfied, max unc./thresh. is 1.07581 for absorption in tally 10002
 The estimated number of batches is 23
 Creating state point statepoint.020.h5...
       21/1    0.64538    0.67749 +/- 0.00469
       22/1    0.73275    0.68074 +/- 0.00547
       23/1    0.71674    0.68274 +/- 0.00553
 Triggers satisfied for batch 23
 Creating state point statepoint.023.h5...

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


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

 Total time for initialization     =  3.9000E-01 seconds
   Reading cross sections          =  8.6000E-02 seconds
 Total time in simulation          =  1.0830E+01 seconds
   Time in transport only          =  1.0818E+01 seconds
   Time in inactive batches        =  1.3590E+00 seconds
   Time in active batches          =  9.4710E+00 seconds
   Time synchronizing fission bank =  3.0000E-03 seconds
     Sampling source sites         =  2.0000E-03 seconds
     SEND/RECV source sites        =  1.0000E-03 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  0.0000E+00 seconds
 Total time elapsed                =  1.1234E+01 seconds
 Calculation Rate (inactive)       =  9197.94 neutrons/second
 Calculation Rate (active)         =  3959.46 neutrons/second

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

 k-effective (Collision)     =  0.67952 +/-  0.00434
 k-effective (Track-length)  =  0.68274 +/-  0.00553
 k-effective (Absorption)    =  0.68095 +/-  0.00369
 Combined k-effective        =  0.67994 +/-  0.00349
 Leakage Fraction            =  0.34133 +/-  0.00332

Out[20]:
0

Tally Data Processing

In [21]:
# 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 [22]:
# 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        =	
                		mesh	[1]
                		energy	[  0.00000000e+00   6.25000000e-07   2.00000000e+01]
	Nuclides       =	total 
	Scores         =	[u'fission', u'nu-fission']
	Estimator      =	tracklength

Use the new Tally data retrieval API with pure NumPy

In [23]:
# Get the relative error for the thermal fission reaction 
# rates in the four corner pins 
data = tally.get_values(scores=['fission'], filters=['mesh', 'energy'], \
                        filter_bins=[((1,1),(1,17), (17,1), (17,17)), \
                                    ((0., 0.625e-6),)], value='rel_err')
print(data)
[[[ 0.1508711 ]]

 [[ 0.05389822]]

 [[ 0.19633   ]]

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

# Set the Pandas float display settings
import pandas as pd
pd.set_option('display.float_format', '{:.2e}'.format)

# Print the first twenty rows in the dataframe
df.head(20)
Out[24]:
mesh 1 energy low [MeV] energy high [MeV] score mean std. dev.
x y z
0 1 1 1 0.00e+00 6.25e-07 fission 2.34e-04 3.54e-05
1 1 1 1 0.00e+00 6.25e-07 nu-fission 5.71e-04 8.62e-05
2 1 1 1 6.25e-07 2.00e+01 fission 7.03e-05 7.05e-06
3 1 1 1 6.25e-07 2.00e+01 nu-fission 1.87e-04 1.76e-05
4 1 2 1 0.00e+00 6.25e-07 fission 3.67e-04 3.61e-05
5 1 2 1 0.00e+00 6.25e-07 nu-fission 8.94e-04 8.80e-05
6 1 2 1 6.25e-07 2.00e+01 fission 1.04e-04 5.36e-06
7 1 2 1 6.25e-07 2.00e+01 nu-fission 2.76e-04 1.40e-05
8 1 3 1 0.00e+00 6.25e-07 fission 6.04e-04 5.57e-05
9 1 3 1 0.00e+00 6.25e-07 nu-fission 1.47e-03 1.36e-04
10 1 3 1 6.25e-07 2.00e+01 fission 1.41e-04 6.69e-06
11 1 3 1 6.25e-07 2.00e+01 nu-fission 3.72e-04 1.82e-05
12 1 4 1 0.00e+00 6.25e-07 fission 6.45e-04 4.59e-05
13 1 4 1 0.00e+00 6.25e-07 nu-fission 1.57e-03 1.12e-04
14 1 4 1 6.25e-07 2.00e+01 fission 1.82e-04 9.37e-06
15 1 4 1 6.25e-07 2.00e+01 nu-fission 4.76e-04 2.47e-05
16 1 5 1 0.00e+00 6.25e-07 fission 7.28e-04 7.49e-05
17 1 5 1 0.00e+00 6.25e-07 nu-fission 1.77e-03 1.83e-04
18 1 5 1 6.25e-07 2.00e+01 fission 1.81e-04 1.04e-05
19 1 5 1 6.25e-07 2.00e+01 nu-fission 4.72e-04 2.67e-05
In [25]:
# Create a boxplot to view the distribution of
# fission and nu-fission rates in the pins
bp = df.boxplot(column='mean', by='score')
In [26]:
# Extract thermal nu-fission rates from pandas
fiss = df[df['score'] == 'nu-fission']
fiss = fiss[fiss['energy low [MeV]'] == 0.0]

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

plt.imshow(mean, interpolation='nearest')
plt.title('fission rate')
plt.xlabel('x')
plt.ylabel('y')
plt.colorbar()
Out[26]:
<matplotlib.colorbar.Colorbar at 0x7f2694c288d0>

Analyze the cell+nuclides scatter-y2 rate tally

In [27]:
# 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        =	
                		cell	[10000]
	Nuclides       =	U-235 U-238 
	Scores         =	[u'scatter-Y0,0', u'scatter-Y1,-1', u'scatter-Y1,0', u'scatter-Y1,1', u'scatter-Y2,-2', u'scatter-Y2,-1', u'scatter-Y2,0', u'scatter-Y2,1', u'scatter-Y2,2']
	Estimator      =	analog

In [28]:
# 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[28]:
cell nuclide score mean std. dev.
0 10000 U-235 scatter-Y0,0 3.86e-02 1.11e-03
1 10000 U-235 scatter-Y1,-1 2.75e-04 2.96e-04
2 10000 U-235 scatter-Y1,0 -5.55e-05 4.33e-04
3 10000 U-235 scatter-Y1,1 -4.22e-04 3.51e-04
4 10000 U-235 scatter-Y2,-2 5.88e-05 2.04e-04
5 10000 U-235 scatter-Y2,-1 1.00e-04 2.49e-04
6 10000 U-235 scatter-Y2,0 -8.09e-05 1.59e-04
7 10000 U-235 scatter-Y2,1 1.93e-04 2.14e-04
8 10000 U-235 scatter-Y2,2 1.12e-04 1.86e-04
9 10000 U-238 scatter-Y0,0 2.34e+00 1.34e-02
10 10000 U-238 scatter-Y1,-1 2.32e-02 2.97e-03
11 10000 U-238 scatter-Y1,0 7.50e-04 2.55e-03
12 10000 U-238 scatter-Y1,1 -2.73e-02 3.28e-03
13 10000 U-238 scatter-Y2,-2 -2.36e-03 1.21e-03
14 10000 U-238 scatter-Y2,-1 -1.80e-04 1.49e-03
15 10000 U-238 scatter-Y2,0 3.23e-03 2.25e-03
16 10000 U-238 scatter-Y2,1 3.75e-03 1.97e-03
17 10000 U-238 scatter-Y2,2 2.07e-03 1.60e-03

Use the new Tally data retrieval API with pure NumPy

In [29]:
# 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=['U-238', 'U-235'], value='std_dev')
print(data)
[[[ 0.00159927  0.01341406]
  [ 0.00018637  0.00111048]]]

Analyze the distribcell tally

In [30]:
# 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        =	
                		distribcell	[10002]
	Nuclides       =	total 
	Scores         =	[u'absorption', u'scatter']
	Estimator      =	tracklength

Use the new Tally data retrieval API with pure NumPy

In [31]:
# Get the relative error for the scattering reaction rates in
# the first 10 distribcell instances 
data = tally.get_values(scores=['scatter'], filters=['distribcell'],
                        filter_bins=[(i,) for i in range(10)], value='rel_err')
print(data)
[[[ 0.05767856]]]

Print the distribcell tally dataframe

In [32]:
# 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[32]:
level 1 level 2 level 3 distribcell score mean std. dev.
univ cell lat univ cell
id id id x y z id id
558 0 10003 10001 16 7 0 10000 10002 279 absorption 8.19e-05 7.82e-06
559 0 10003 10001 16 7 0 10000 10002 279 scatter 1.33e-02 6.19e-04
560 0 10003 10001 16 8 0 10000 10002 280 absorption 1.00e-04 7.93e-06
561 0 10003 10001 16 8 0 10000 10002 280 scatter 1.40e-02 5.61e-04
562 0 10003 10001 16 9 0 10000 10002 281 absorption 9.52e-05 7.08e-06
563 0 10003 10001 16 9 0 10000 10002 281 scatter 1.51e-02 6.50e-04
564 0 10003 10001 16 10 0 10000 10002 282 absorption 9.85e-05 9.47e-06
565 0 10003 10001 16 10 0 10000 10002 282 scatter 1.53e-02 4.63e-04
566 0 10003 10001 16 11 0 10000 10002 283 absorption 1.08e-04 1.34e-05
567 0 10003 10001 16 11 0 10000 10002 283 scatter 1.65e-02 7.04e-04
568 0 10003 10001 16 12 0 10000 10002 284 absorption 1.13e-04 7.91e-06
569 0 10003 10001 16 12 0 10000 10002 284 scatter 1.67e-02 5.51e-04
570 0 10003 10001 16 13 0 10000 10002 285 absorption 1.23e-04 9.53e-06
571 0 10003 10001 16 13 0 10000 10002 285 scatter 1.88e-02 7.25e-04
572 0 10003 10001 16 14 0 10000 10002 286 absorption 1.44e-04 1.34e-05
573 0 10003 10001 16 14 0 10000 10002 286 scatter 1.90e-02 7.07e-04
574 0 10003 10001 16 15 0 10000 10002 287 absorption 1.26e-04 8.66e-06
575 0 10003 10001 16 15 0 10000 10002 287 scatter 1.97e-02 7.23e-04
576 0 10003 10001 16 16 0 10000 10002 288 absorption 1.25e-04 9.59e-06
577 0 10003 10001 16 16 0 10000 10002 288 scatter 2.01e-02 6.75e-04
In [33]:
# 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[33]:
mean std. dev.
count 2.89e+02 2.89e+02
mean 4.19e-04 2.24e-05
std 2.42e-04 9.14e-06
min 1.90e-05 3.44e-06
25% 2.02e-04 1.56e-05
50% 4.05e-04 2.20e-05
75% 6.07e-04 2.89e-05
max 9.19e-04 4.95e-05

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

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

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 [35]:
# 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: 6.038663783e-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 [36]:
# 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/python2.7/dist-packages/IPython/kernel/__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[36]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2694c82b10>
In [37]:
# 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[37]:
<matplotlib.legend.Legend at 0x7f2694ac1fd0>

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('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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))
materials_file.default_xs = '71c'

# 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 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, -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 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.Filter(type='energy', bins=[0., 0.625e-6, 20.])

# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='flux')
tally.filters = [openmc.Filter(type='cell', bins=[fuel_cell.id, moderator_cell.id])]
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.Filter(type='cell', bins=[fuel_cell.id])]
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.Filter(type='cell', bins=[moderator_cell.id])]
tally.filters.append(energy_filter)
tally.scores = ['absorption', 'total']
tally.nuclides = [o16, h1]
tallies_file.append(tally)
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.Filter(type='energy', bins=[0., 0.625e-6])]
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.Filter(type='energy', bins=[0., 0.625e-6]),
                               openmc.Filter(type='cell', bins=[fuel_cell.id])]
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.Filter(type='energy', bins=[0., 0.625e-6])]
tallies_file.append(therm_fiss_rate)
In [20]:
# Instantiate energy filter to illustrate Tally slicing
energy_filter = openmc.Filter(type='energy', bins=np.logspace(np.log10(1e-8), np.log10(20), 10))

# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='need-to-slice')
tally.filters = [openmc.Filter(type='cell', bins=[fuel_cell.id, moderator_cell.id])]
tally.filters.append(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()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.org/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       df280b60eb1c6d7b7f842e05ede734a4883a0fc8
      Date/Time:      2016-05-05 14:51:45

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 5010.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.03471                       
        2/1    1.03257                       
        3/1    1.00600                       
        4/1    1.04547                       
        5/1    1.02287                       
        6/1    1.05752                       
        7/1    1.04283    1.05017 +/- 0.00734
        8/1    1.05189    1.05074 +/- 0.00428
        9/1    1.01645    1.04217 +/- 0.00909
       10/1    1.04978    1.04369 +/- 0.00721
       11/1    1.03459    1.04218 +/- 0.00608
       12/1    1.04019    1.04189 +/- 0.00514
       13/1    1.05985    1.04414 +/- 0.00499
       14/1    1.02111    1.04158 +/- 0.00509
       15/1    1.04774    1.04219 +/- 0.00459
       16/1    1.00733    1.03902 +/- 0.00523
       17/1    1.02224    1.03763 +/- 0.00497
       18/1    1.03263    1.03724 +/- 0.00459
       19/1    1.01611    1.03573 +/- 0.00451
       20/1    1.04692    1.03648 +/- 0.00426
 Creating state point statepoint.20.h5...

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


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

 Total time for initialization     =  7.2500E-01 seconds
   Reading cross sections          =  4.4400E-01 seconds
 Total time in simulation          =  1.5547E+01 seconds
   Time in transport only          =  1.5527E+01 seconds
   Time in inactive batches        =  2.2880E+00 seconds
   Time in active batches          =  1.3259E+01 seconds
   Time synchronizing fission bank =  1.0000E-03 seconds
     Sampling source sites         =  0.0000E+00 seconds
     SEND/RECV source sites        =  0.0000E+00 seconds
   Time accumulating tallies       =  1.0000E-03 seconds
 Total time for finalization       =  2.0000E-03 seconds
 Total time elapsed                =  1.6291E+01 seconds
 Calculation Rate (inactive)       =  5463.29 neutrons/second
 Calculation Rate (active)         =  2828.27 neutrons/second

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

 k-effective (Collision)     =  1.03296 +/-  0.00669
 k-effective (Track-length)  =  1.03648 +/-  0.00426
 k-effective (Absorption)    =  1.03431 +/-  0.00702
 Combined k-effective        =  1.03621 +/-  0.00456
 Leakage Fraction            =  0.00000 +/-  0.00000

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-infinity as: $$k_\infty = \frac{\langle \nu \Sigma_f \phi \rangle}{\langle \Sigma_a \phi \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]:
# Compute k-infinity using tally arithmetic
fiss_rate = sp.get_tally(name='fiss. rate')
abs_rate = sp.get_tally(name='abs. rate')
keff = fiss_rate / abs_rate
keff.get_pandas_dataframe()
Out[24]:
nuclide score mean std. dev.
0 total (nu-fission / absorption) 1.038387 0.006141

Notice that even though the neutron production rate and absorption rate 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-infinity represented using the four-factor formula $$k_\infty = p \epsilon f \eta.$$ 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\Sigma_a\phi\rangle}$$ 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')
res_esc = therm_abs_rate / abs_rate
res_esc.get_pandas_dataframe()
Out[25]:
energy low [MeV] energy high [MeV] nuclide score mean std. dev.
0 0.0 6.250000e-07 total absorption 0.693337 0.004109

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 [MeV] energy high [MeV] nuclide score mean std. dev.
0 0.0 6.250000e-07 total nu-fission 1.203042 0.0076

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 [MeV] energy high [MeV] cell nuclide score mean std. dev.
0 0.0 6.250000e-07 10000 total absorption 0.748413 0.004723

The final 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 [MeV] energy high [MeV] cell nuclide score mean std. dev.
0 0.0 6.250000e-07 10000 total (nu-fission / absorption) 1.663385 0.011253

Now we can calculate $k_\infty$ using the product of the factors form the four-factor formula.

In [29]:
keff = res_esc * fast_fiss * therm_util * eta
keff.get_pandas_dataframe()
Out[29]:
energy low [MeV] energy high [MeV] cell nuclide score mean std. dev.
0 0.0 6.250000e-07 10000 total (((absorption * nu-fission) * absorption) * (n... 1.038387 0.01316

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 [30]:
# Compute microscopic multi-group cross-sections
flux = sp.get_tally(name='flux')
flux = flux.get_slice(filters=['cell'], 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 [31]:
fuel_xs = fuel_rxn_rates / flux
fuel_xs.get_pandas_dataframe()
Out[31]:
cell energy low [MeV] energy high [MeV] nuclide score mean std. dev.
0 10000 0.000000e+00 6.250000e-07 (U-238 / total) (nu-fission / flux) 6.636968e-07 4.132875e-09
1 10000 0.000000e+00 6.250000e-07 (U-238 / total) (scatter / flux) 2.099856e-01 1.232455e-03
2 10000 0.000000e+00 6.250000e-07 (U-235 / total) (nu-fission / flux) 3.552458e-01 2.252681e-03
3 10000 0.000000e+00 6.250000e-07 (U-235 / total) (scatter / flux) 5.554345e-03 3.265385e-05
4 10000 6.250000e-07 2.000000e+01 (U-238 / total) (nu-fission / flux) 7.126668e-03 5.296883e-05
5 10000 6.250000e-07 2.000000e+01 (U-238 / total) (scatter / flux) 2.277460e-01 1.003558e-03
6 10000 6.250000e-07 2.000000e+01 (U-235 / total) (nu-fission / flux) 8.010911e-03 6.802256e-05
7 10000 6.250000e-07 2.000000e+01 (U-235 / total) (scatter / flux) 3.367794e-03 1.443644e-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 [32]:
# 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.63696783e-07]
  [  3.55245846e-01]]

 [[  7.12666800e-03]
  [  8.01091088e-03]]]

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

In [33]:
# Show how to use Tally.get_values(...) with a CrossScore and CrossNuclide
u235_scatter_xs = fuel_xs.get_values(nuclides=['(U-235 / total)'], 
                                scores=['(scatter / flux)'])
print(u235_scatter_xs)
[[[ 0.00555435]]

 [[ 0.00336779]]]
In [34]:
# Show how to use Tally.get_values(...) with a CrossFilter and CrossScore
fast_scatter_xs = fuel_xs.get_values(filters=['energy'], 
                                     filter_bins=[((0.625e-6, 20.),)], 
                                     scores=['(scatter / flux)'])
print(fast_scatter_xs)
[[[ 0.22774598]
  [ 0.00336779]]]

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 [35]:
# "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[35]:
cell energy low [MeV] energy high [MeV] nuclide score mean std. dev.
0 10000 0.000000e+00 6.250000e-07 U-238 nu-fission 0.000002 7.473789e-09
1 10000 0.000000e+00 6.250000e-07 U-235 nu-fission 0.861547 4.131310e-03
2 10000 6.250000e-07 2.000000e+01 U-238 nu-fission 0.082356 5.560461e-04
3 10000 6.250000e-07 2.000000e+01 U-235 nu-fission 0.092574 7.315442e-04
In [36]:
# "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=['H-1'],
                                     filters=['cell'], filter_bins=[(moderator_cell.id,)])
slice_test.get_pandas_dataframe()
Out[36]:
cell energy low [MeV] energy high [MeV] nuclide score mean std. dev.
0 10002 1.000000e-08 1.080060e-07 H-1 scatter 4.599225 0.015973
1 10002 1.080060e-07 1.166529e-06 H-1 scatter 2.037260 0.011236
2 10002 1.166529e-06 1.259921e-05 H-1 scatter 1.662552 0.010280
3 10002 1.259921e-05 1.360790e-04 H-1 scatter 1.872201 0.012136
4 10002 1.360790e-04 1.469734e-03 H-1 scatter 2.080459 0.013155
5 10002 1.469734e-03 1.587401e-02 H-1 scatter 2.154996 0.011975
6 10002 1.587401e-02 1.714488e-01 H-1 scatter 2.218740 0.008528
7 10002 1.714488e-01 1.851749e+00 H-1 scatter 2.010517 0.009187
8 10002 1.851749e+00 2.000000e+01 H-1 scatter 0.372022 0.003196

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

Note: This Notebook illustrates the use of Pandas DataFrames to containerize multi-group cross section data. We recommend using Pandas >v0.15.0 or later since OpenMC's Python API leverages the multi-indexing feature included in the most recent releases of Pandas.

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('H-1')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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.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 = 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.625e-6, 20.])

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
  • NuTransportXS
  • AbsorptionXS
  • CaptureXS
  • FissionXS
  • NuFissionXS
  • KappaFissionXS
  • ScatterXS
  • NuScatterXS
  • ScatterMatrixXS
  • NuScatterMatrixXS
  • Chi

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)

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        =	
                		cell	[1]
                		energy	[  0.00000000e+00   6.25000000e-07   2.00000000e+01]
	Nuclides       =	total 
	Scores         =	['flux']
	Estimator      =	tracklength
), ('absorption', Tally
	ID             =	10001
	Name           =	
	Filters        =	
                		cell	[1]
                		energy	[  0.00000000e+00   6.25000000e-07   2.00000000e+01]
	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()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       19feb55e6d5e8350398627f39fb55ee8e2e63011
      Date/Time:      2016-05-13 10:19:16
      MPI Processes:  1

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 1001.71c
 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.17479    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     =  4.2300E-01 seconds
   Reading cross sections          =  9.3000E-02 seconds
 Total time in simulation          =  1.6549E+01 seconds
   Time in transport only          =  1.6535E+01 seconds
   Time in inactive batches        =  2.3650E+00 seconds
   Time in active batches          =  1.4184E+01 seconds
   Time synchronizing fission bank =  5.0000E-03 seconds
     Sampling source sites         =  3.0000E-03 seconds
     SEND/RECV source sites        =  0.0000E+00 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  0.0000E+00 seconds
 Total time elapsed                =  1.6981E+01 seconds
 Calculation Rate (inactive)       =  10570.8 neutrons/second
 Calculation Rate (active)         =  7050.20 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 [6.25e-07   - 20.0      MeV]:	6.81e-01 +/- 2.69e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	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 [MeV] energy high [MeV] nuclide score mean std. dev.
0 1 0.000000e+00 6.250000e-07 total (((total / flux) - (absorption / flux)) - (sca... -3.774758e-15 0.011292
1 1 6.250000e-07 2.000000e+01 total (((total / flux) - (absorption / flux)) - (sca... 1.443290e-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 [MeV] energy high [MeV] nuclide score mean std. dev.
0 1 0.000000e+00 6.250000e-07 total ((absorption / flux) / (total / flux)) 0.076115 0.000649
1 1 6.250000e-07 2.000000e+01 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 [MeV] energy high [MeV] nuclide score mean std. dev.
0 1 0.000000e+00 6.250000e-07 total ((scatter / flux) / (total / flux)) 0.923885 0.007736
1 1 6.250000e-07 2.000000e+01 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 [MeV] energy high [MeV] nuclide score mean std. dev.
0 1 0.000000e+00 6.250000e-07 total (((absorption / flux) / (total / flux)) + ((sc... 1.0 0.007763
1 1 6.250000e-07 2.000000e+01 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 PyNE 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. In order to run this Notebook in its entirety, you must have OpenMOC installed on your system, along with OpenCG to convert the OpenMC geometries into OpenMOC geometries. 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
import seaborn as sns

import openmc
import openmc.mgxs as mgxs
import openmoc
from openmoc.opencg_compatible import get_openmoc_geometry
import pyne.ace

%matplotlib inline
/usr/local/lib/python2.7/dist-packages/matplotlib-1.5.1+1178.ga40c9ec-py2.7-linux-x86_64.egg/matplotlib/__init__.py:884: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.
  warnings.warn(self.msg_depr % (key, alt_key))
/usr/local/lib/python2.7/dist-packages/matplotlib-1.5.1+1178.ga40c9ec-py2.7-linux-x86_64.egg/matplotlib/__init__.py:1362: 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)
/usr/local/lib/python2.7/dist-packages/IPython/kernel/__main__.py:9: QAWarning: pyne.rxname is not yet QA compliant.
/usr/local/lib/python2.7/dist-packages/IPython/kernel/__main__.py:9: QAWarning: pyne.ace is not yet QA compliant.

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('H-1')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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))
materials_file.default_xs = '71c'

# 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.625e-6, 20.])

# Instantiate a "fine" 8-group EnergyGroups object
fine_groups = mgxs.EnergyGroups()
fine_groups.group_edges = np.array([0., 0.058e-6, 0.14e-6, 0.28e-6,
                                    0.625e-6, 4.e-6, 5.53e-3, 821.e-3, 20.])

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

# 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.NuFissionXS(groups=fine_groups)
    xs_library[cell.id]['nu-scatter'] = mgxs.NuScatterMatrixXS(groups=fine_groups)
    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(output=True)
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       19feb55e6d5e8350398627f39fb55ee8e2e63011
      Date/Time:      2016-05-13 10:13:48
      MPI Processes:  1

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.20332                       
        2/1    1.22209                       
        3/1    1.24309                       
        4/1    1.22833                       
        5/1    1.21786                       
        6/1    1.22005                       
        7/1    1.20894                       
        8/1    1.22071                       
        9/1    1.21279                       
       10/1    1.22198                       
       11/1    1.22287                       
       12/1    1.25490    1.23888 +/- 0.01602
       13/1    1.20224    1.22667 +/- 0.01532
       14/1    1.23375    1.22844 +/- 0.01098
       15/1    1.23068    1.22889 +/- 0.00851
       16/1    1.23073    1.22920 +/- 0.00696
       17/1    1.25364    1.23269 +/- 0.00684
       18/1    1.20820    1.22963 +/- 0.00667
       19/1    1.23138    1.22982 +/- 0.00588
       20/1    1.20682    1.22752 +/- 0.00574
       21/1    1.23580    1.22827 +/- 0.00525
       22/1    1.24190    1.22941 +/- 0.00492
       23/1    1.23125    1.22955 +/- 0.00453
       24/1    1.21606    1.22859 +/- 0.00430
       25/1    1.23653    1.22912 +/- 0.00404
       26/1    1.23850    1.22970 +/- 0.00383
       27/1    1.20986    1.22853 +/- 0.00378
       28/1    1.25277    1.22988 +/- 0.00381
       29/1    1.23334    1.23006 +/- 0.00361
       30/1    1.24345    1.23073 +/- 0.00349
       31/1    1.21565    1.23001 +/- 0.00339
       32/1    1.20555    1.22890 +/- 0.00342
       33/1    1.22995    1.22895 +/- 0.00327
       34/1    1.19763    1.22764 +/- 0.00339
       35/1    1.22645    1.22760 +/- 0.00325
       36/1    1.23900    1.22803 +/- 0.00316
       37/1    1.24305    1.22859 +/- 0.00309
       38/1    1.22484    1.22846 +/- 0.00298
       39/1    1.20986    1.22782 +/- 0.00294
       40/1    1.23764    1.22814 +/- 0.00286
       41/1    1.20476    1.22739 +/- 0.00287
       42/1    1.21652    1.22705 +/- 0.00280
       43/1    1.21279    1.22662 +/- 0.00275
       44/1    1.20210    1.22590 +/- 0.00276
       45/1    1.22644    1.22591 +/- 0.00268
       46/1    1.22907    1.22600 +/- 0.00261
       47/1    1.24057    1.22639 +/- 0.00257
       48/1    1.21610    1.22612 +/- 0.00251
       49/1    1.22199    1.22602 +/- 0.00245
       50/1    1.20860    1.22558 +/- 0.00243
 Triggers unsatisfied, max unc./thresh. is 1.25496 for flux in tally 10051
 The estimated number of batches is 73
 Creating state point statepoint.050.h5...
       51/1    1.21850    1.22541 +/- 0.00237
       52/1    1.22833    1.22548 +/- 0.00232
       53/1    1.20239    1.22494 +/- 0.00233
       54/1    1.24876    1.22548 +/- 0.00234
       55/1    1.20670    1.22506 +/- 0.00232
       56/1    1.24260    1.22545 +/- 0.00230
       57/1    1.21039    1.22512 +/- 0.00228
       58/1    1.23929    1.22542 +/- 0.00225
       59/1    1.21357    1.22518 +/- 0.00221
       60/1    1.23456    1.22537 +/- 0.00218
       61/1    1.23963    1.22565 +/- 0.00215
       62/1    1.24020    1.22593 +/- 0.00213
       63/1    1.22325    1.22587 +/- 0.00209
       64/1    1.22070    1.22578 +/- 0.00205
       65/1    1.22423    1.22575 +/- 0.00201
       66/1    1.22973    1.22582 +/- 0.00198
       67/1    1.21842    1.22569 +/- 0.00195
       68/1    1.19552    1.22517 +/- 0.00198
       69/1    1.21475    1.22500 +/- 0.00196
       70/1    1.21888    1.22489 +/- 0.00193
       71/1    1.19720    1.22444 +/- 0.00195
       72/1    1.23770    1.22465 +/- 0.00193
       73/1    1.23894    1.22488 +/- 0.00191
 Triggers unsatisfied, max unc./thresh. is 1.00243 for flux in tally 10051
 The estimated number of batches is 74
       74/1    1.22437    1.22487 +/- 0.00188
 Triggers satisfied for batch 74
 Creating state point statepoint.074.h5...

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


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

 Total time for initialization     =  5.7400E-01 seconds
   Reading cross sections          =  1.2600E-01 seconds
 Total time in simulation          =  2.6256E+02 seconds
   Time in transport only          =  2.6250E+02 seconds
   Time in inactive batches        =  2.2890E+01 seconds
   Time in active batches          =  2.3967E+02 seconds
   Time synchronizing fission bank =  3.4000E-02 seconds
     Sampling source sites         =  2.1000E-02 seconds
     SEND/RECV source sites        =  1.3000E-02 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  1.3000E-02 seconds
 Total time elapsed                =  2.6320E+02 seconds
 Calculation Rate (inactive)       =  4368.72 neutrons/second
 Calculation Rate (active)         =  1668.93 neutrons/second

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

 k-effective (Collision)     =  1.22358 +/-  0.00179
 k-effective (Track-length)  =  1.22487 +/-  0.00188
 k-effective (Absorption)    =  1.22300 +/-  0.00114
 Combined k-effective        =  1.22347 +/-  0.00106
 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.074.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=['U-235', 'U-238'])
Multi-Group XS
	Reaction Type  =	nu-fission
	Domain Type    =	cell
	Domain ID      =	10000
	Nuclide        =	U-235
	Cross Sections [barns]:
            Group 1 [0.821      - 20.0      MeV]:	3.30e+00 +/- 2.19e-01%
            Group 2 [0.00553    - 0.821     MeV]:	3.96e+00 +/- 1.32e-01%
            Group 3 [4e-06      - 0.00553   MeV]:	5.52e+01 +/- 2.31e-01%
            Group 4 [6.25e-07   - 4e-06     MeV]:	8.83e+01 +/- 2.96e-01%
            Group 5 [2.8e-07    - 6.25e-07  MeV]:	2.90e+02 +/- 4.64e-01%
            Group 6 [1.4e-07    - 2.8e-07   MeV]:	4.49e+02 +/- 4.22e-01%
            Group 7 [5.8e-08    - 1.4e-07   MeV]:	6.87e+02 +/- 2.97e-01%
            Group 8 [0.0        - 5.8e-08   MeV]:	1.44e+03 +/- 2.91e-01%

	Nuclide        =	U-238
	Cross Sections [barns]:
            Group 1 [0.821      - 20.0      MeV]:	1.06e+00 +/- 2.56e-01%
            Group 2 [0.00553    - 0.821     MeV]:	1.21e-03 +/- 2.55e-01%
            Group 3 [4e-06      - 0.00553   MeV]:	5.77e-04 +/- 3.67e+00%
            Group 4 [6.25e-07   - 4e-06     MeV]:	6.54e-06 +/- 2.74e-01%
            Group 5 [2.8e-07    - 6.25e-07  MeV]:	1.07e-05 +/- 4.55e-01%
            Group 6 [1.4e-07    - 2.8e-07   MeV]:	1.55e-05 +/- 4.25e-01%
            Group 7 [5.8e-08    - 1.4e-07   MeV]:	2.30e-05 +/- 2.97e-01%
            Group 8 [0.0        - 5.8e-08   MeV]:	4.24e-05 +/- 2.90e-01%



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 [0.821      - 20.0      MeV]:	2.52e-02 +/- 2.44e-01%
            Group 2 [0.00553    - 0.821     MeV]:	1.51e-03 +/- 1.30e-01%
            Group 3 [4e-06      - 0.00553   MeV]:	2.07e-02 +/- 2.31e-01%
            Group 4 [6.25e-07   - 4e-06     MeV]:	3.31e-02 +/- 2.96e-01%
            Group 5 [2.8e-07    - 6.25e-07  MeV]:	1.09e-01 +/- 4.64e-01%
            Group 6 [1.4e-07    - 2.8e-07   MeV]:	1.69e-01 +/- 4.22e-01%
            Group 7 [5.8e-08    - 1.4e-07   MeV]:	2.58e-01 +/- 2.97e-01%
            Group 8 [0.0        - 5.8e-08   MeV]:	5.40e-01 +/- 2.91e-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)
Out[19]:
cell group in group out nuclide mean std. dev.
126 10002 1 1 H-1 0.234115 0.003568
127 10002 1 1 O-16 1.563707 0.005953
124 10002 1 2 H-1 1.594129 0.002369
125 10002 1 2 O-16 0.285761 0.001676
122 10002 1 3 H-1 0.011089 0.000248
123 10002 1 3 O-16 0.000000 0.000000
120 10002 1 4 H-1 0.000000 0.000000
121 10002 1 4 O-16 0.000000 0.000000
118 10002 1 5 H-1 0.000000 0.000000
119 10002 1 5 O-16 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        =	U-235
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	7.73e-03 +/- 5.06e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	1.82e-01 +/- 2.05e-01%

	Nuclide        =	U-238
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	2.17e-01 +/- 1.44e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	2.53e-01 +/- 2.57e-01%

	Nuclide        =	O-16
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	1.46e-01 +/- 1.60e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	1.75e-01 +/- 2.94e-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 U-235 20.611692 0.104237
4 10000 1 U-238 9.585358 0.013808
5 10000 1 O-16 3.164190 0.005049
0 10000 2 U-235 485.413426 0.996410
1 10000 2 U-238 11.190386 0.028731
2 10000 2 O-16 3.794859 0.011139

Verification with OpenMOC

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

In [23]:
# Create an OpenMOC Geometry from the OpenCG Geometry
openmoc_geometry = get_openmoc_geometry(sp.summary.opencg_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())

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.574672	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.679815	res = 4.253E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.660826	res = 1.830E-01
[  NORMAL ]  Iteration 3:	k_eff = 0.658940	res = 2.793E-02
[  NORMAL ]  Iteration 4:	k_eff = 0.643012	res = 2.853E-03
[  NORMAL ]  Iteration 5:	k_eff = 0.625810	res = 2.417E-02
[  NORMAL ]  Iteration 6:	k_eff = 0.606678	res = 2.675E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.587485	res = 3.057E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.569028	res = 3.164E-02
[  NORMAL ]  Iteration 9:	k_eff = 0.551707	res = 3.142E-02
[  NORMAL ]  Iteration 10:	k_eff = 0.536034	res = 3.044E-02
[  NORMAL ]  Iteration 11:	k_eff = 0.522274	res = 2.841E-02
[  NORMAL ]  Iteration 12:	k_eff = 0.510609	res = 2.567E-02
[  NORMAL ]  Iteration 13:	k_eff = 0.501105	res = 2.234E-02
[  NORMAL ]  Iteration 14:	k_eff = 0.493831	res = 1.861E-02
[  NORMAL ]  Iteration 15:	k_eff = 0.488780	res = 1.452E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.485922	res = 1.023E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.485209	res = 5.846E-03
[  NORMAL ]  Iteration 18:	k_eff = 0.486569	res = 1.467E-03
[  NORMAL ]  Iteration 19:	k_eff = 0.489903	res = 2.801E-03
[  NORMAL ]  Iteration 20:	k_eff = 0.495102	res = 6.853E-03
[  NORMAL ]  Iteration 21:	k_eff = 0.502053	res = 1.061E-02
[  NORMAL ]  Iteration 22:	k_eff = 0.510627	res = 1.404E-02
[  NORMAL ]  Iteration 23:	k_eff = 0.520692	res = 1.708E-02
[  NORMAL ]  Iteration 24:	k_eff = 0.532116	res = 1.971E-02
[  NORMAL ]  Iteration 25:	k_eff = 0.544763	res = 2.194E-02
[  NORMAL ]  Iteration 26:	k_eff = 0.558500	res = 2.377E-02
[  NORMAL ]  Iteration 27:	k_eff = 0.573195	res = 2.522E-02
[  NORMAL ]  Iteration 28:	k_eff = 0.588717	res = 2.631E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.604945	res = 2.708E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.621759	res = 2.756E-02
[  NORMAL ]  Iteration 31:	k_eff = 0.639046	res = 2.779E-02
[  NORMAL ]  Iteration 32:	k_eff = 0.656701	res = 2.780E-02
[  NORMAL ]  Iteration 33:	k_eff = 0.674624	res = 2.763E-02
[  NORMAL ]  Iteration 34:	k_eff = 0.692722	res = 2.729E-02
[  NORMAL ]  Iteration 35:	k_eff = 0.710909	res = 2.683E-02
[  NORMAL ]  Iteration 36:	k_eff = 0.729109	res = 2.626E-02
[  NORMAL ]  Iteration 37:	k_eff = 0.747248	res = 2.560E-02
[  NORMAL ]  Iteration 38:	k_eff = 0.765262	res = 2.488E-02
[  NORMAL ]  Iteration 39:	k_eff = 0.783093	res = 2.411E-02
[  NORMAL ]  Iteration 40:	k_eff = 0.800689	res = 2.330E-02
[  NORMAL ]  Iteration 41:	k_eff = 0.818004	res = 2.247E-02
[  NORMAL ]  Iteration 42:	k_eff = 0.834999	res = 2.163E-02
[  NORMAL ]  Iteration 43:	k_eff = 0.851638	res = 2.078E-02
[  NORMAL ]  Iteration 44:	k_eff = 0.867891	res = 1.993E-02
[  NORMAL ]  Iteration 45:	k_eff = 0.883735	res = 1.909E-02
[  NORMAL ]  Iteration 46:	k_eff = 0.899148	res = 1.826E-02
[  NORMAL ]  Iteration 47:	k_eff = 0.914114	res = 1.744E-02
[  NORMAL ]  Iteration 48:	k_eff = 0.928621	res = 1.664E-02
[  NORMAL ]  Iteration 49:	k_eff = 0.942659	res = 1.587E-02
[  NORMAL ]  Iteration 50:	k_eff = 0.956221	res = 1.512E-02
[  NORMAL ]  Iteration 51:	k_eff = 0.969305	res = 1.439E-02
[  NORMAL ]  Iteration 52:	k_eff = 0.981909	res = 1.368E-02
[  NORMAL ]  Iteration 53:	k_eff = 0.994034	res = 1.300E-02
[  NORMAL ]  Iteration 54:	k_eff = 1.005685	res = 1.235E-02
[  NORMAL ]  Iteration 55:	k_eff = 1.016866	res = 1.172E-02
[  NORMAL ]  Iteration 56:	k_eff = 1.027583	res = 1.112E-02
[  NORMAL ]  Iteration 57:	k_eff = 1.037845	res = 1.054E-02
[  NORMAL ]  Iteration 58:	k_eff = 1.047661	res = 9.986E-03
[  NORMAL ]  Iteration 59:	k_eff = 1.057040	res = 9.458E-03
[  NORMAL ]  Iteration 60:	k_eff = 1.065993	res = 8.952E-03
[  NORMAL ]  Iteration 61:	k_eff = 1.074533	res = 8.470E-03
[  NORMAL ]  Iteration 62:	k_eff = 1.082670	res = 8.011E-03
[  NORMAL ]  Iteration 63:	k_eff = 1.090418	res = 7.573E-03
[  NORMAL ]  Iteration 64:	k_eff = 1.097789	res = 7.156E-03
[  NORMAL ]  Iteration 65:	k_eff = 1.104796	res = 6.760E-03
[  NORMAL ]  Iteration 66:	k_eff = 1.111452	res = 6.383E-03
[  NORMAL ]  Iteration 67:	k_eff = 1.117770	res = 6.025E-03
[  NORMAL ]  Iteration 68:	k_eff = 1.123764	res = 5.685E-03
[  NORMAL ]  Iteration 69:	k_eff = 1.129445	res = 5.362E-03
[  NORMAL ]  Iteration 70:	k_eff = 1.134828	res = 5.056E-03
[  NORMAL ]  Iteration 71:	k_eff = 1.139924	res = 4.766E-03
[  NORMAL ]  Iteration 72:	k_eff = 1.144746	res = 4.491E-03
[  NORMAL ]  Iteration 73:	k_eff = 1.149306	res = 4.230E-03
[  NORMAL ]  Iteration 74:	k_eff = 1.153617	res = 3.984E-03
[  NORMAL ]  Iteration 75:	k_eff = 1.157689	res = 3.750E-03
[  NORMAL ]  Iteration 76:	k_eff = 1.161534	res = 3.530E-03
[  NORMAL ]  Iteration 77:	k_eff = 1.165163	res = 3.321E-03
[  NORMAL ]  Iteration 78:	k_eff = 1.168586	res = 3.124E-03
[  NORMAL ]  Iteration 79:	k_eff = 1.171813	res = 2.938E-03
[  NORMAL ]  Iteration 80:	k_eff = 1.174855	res = 2.762E-03
[  NORMAL ]  Iteration 81:	k_eff = 1.177721	res = 2.596E-03
[  NORMAL ]  Iteration 82:	k_eff = 1.180419	res = 2.439E-03
[  NORMAL ]  Iteration 83:	k_eff = 1.182960	res = 2.291E-03
[  NORMAL ]  Iteration 84:	k_eff = 1.185350	res = 2.152E-03
[  NORMAL ]  Iteration 85:	k_eff = 1.187598	res = 2.021E-03
[  NORMAL ]  Iteration 86:	k_eff = 1.189712	res = 1.897E-03
[  NORMAL ]  Iteration 87:	k_eff = 1.191699	res = 1.780E-03
[  NORMAL ]  Iteration 88:	k_eff = 1.193567	res = 1.670E-03
[  NORMAL ]  Iteration 89:	k_eff = 1.195320	res = 1.567E-03
[  NORMAL ]  Iteration 90:	k_eff = 1.196967	res = 1.469E-03
[  NORMAL ]  Iteration 91:	k_eff = 1.198513	res = 1.378E-03
[  NORMAL ]  Iteration 92:	k_eff = 1.199964	res = 1.292E-03
[  NORMAL ]  Iteration 93:	k_eff = 1.201326	res = 1.211E-03
[  NORMAL ]  Iteration 94:	k_eff = 1.202602	res = 1.134E-03
[  NORMAL ]  Iteration 95:	k_eff = 1.203800	res = 1.063E-03
[  NORMAL ]  Iteration 96:	k_eff = 1.204922	res = 9.955E-04
[  NORMAL ]  Iteration 97:	k_eff = 1.205974	res = 9.323E-04
[  NORMAL ]  Iteration 98:	k_eff = 1.206959	res = 8.730E-04
[  NORMAL ]  Iteration 99:	k_eff = 1.207883	res = 8.173E-04
[  NORMAL ]  Iteration 100:	k_eff = 1.208747	res = 7.649E-04
[  NORMAL ]  Iteration 101:	k_eff = 1.209557	res = 7.159E-04
[  NORMAL ]  Iteration 102:	k_eff = 1.210315	res = 6.698E-04
[  NORMAL ]  Iteration 103:	k_eff = 1.211024	res = 6.266E-04
[  NORMAL ]  Iteration 104:	k_eff = 1.211688	res = 5.861E-04
[  NORMAL ]  Iteration 105:	k_eff = 1.212309	res = 5.481E-04
[  NORMAL ]  Iteration 106:	k_eff = 1.212890	res = 5.125E-04
[  NORMAL ]  Iteration 107:	k_eff = 1.213433	res = 4.791E-04
[  NORMAL ]  Iteration 108:	k_eff = 1.213941	res = 4.479E-04
[  NORMAL ]  Iteration 109:	k_eff = 1.214416	res = 4.186E-04
[  NORMAL ]  Iteration 110:	k_eff = 1.214860	res = 3.912E-04
[  NORMAL ]  Iteration 111:	k_eff = 1.215275	res = 3.655E-04
[  NORMAL ]  Iteration 112:	k_eff = 1.215662	res = 3.414E-04
[  NORMAL ]  Iteration 113:	k_eff = 1.216024	res = 3.189E-04
[  NORMAL ]  Iteration 114:	k_eff = 1.216362	res = 2.979E-04
[  NORMAL ]  Iteration 115:	k_eff = 1.216678	res = 2.781E-04
[  NORMAL ]  Iteration 116:	k_eff = 1.216973	res = 2.597E-04
[  NORMAL ]  Iteration 117:	k_eff = 1.217249	res = 2.425E-04
[  NORMAL ]  Iteration 118:	k_eff = 1.217506	res = 2.263E-04
[  NORMAL ]  Iteration 119:	k_eff = 1.217746	res = 2.112E-04
[  NORMAL ]  Iteration 120:	k_eff = 1.217970	res = 1.971E-04
[  NORMAL ]  Iteration 121:	k_eff = 1.218179	res = 1.840E-04
[  NORMAL ]  Iteration 122:	k_eff = 1.218374	res = 1.716E-04
[  NORMAL ]  Iteration 123:	k_eff = 1.218556	res = 1.601E-04
[  NORMAL ]  Iteration 124:	k_eff = 1.218726	res = 1.494E-04
[  NORMAL ]  Iteration 125:	k_eff = 1.218884	res = 1.393E-04
[  NORMAL ]  Iteration 126:	k_eff = 1.219032	res = 1.299E-04
[  NORMAL ]  Iteration 127:	k_eff = 1.219170	res = 1.212E-04
[  NORMAL ]  Iteration 128:	k_eff = 1.219298	res = 1.130E-04
[  NORMAL ]  Iteration 129:	k_eff = 1.219418	res = 1.053E-04
[  NORMAL ]  Iteration 130:	k_eff = 1.219529	res = 9.821E-05
[  NORMAL ]  Iteration 131:	k_eff = 1.219633	res = 9.155E-05
[  NORMAL ]  Iteration 132:	k_eff = 1.219730	res = 8.534E-05
[  NORMAL ]  Iteration 133:	k_eff = 1.219821	res = 7.954E-05
[  NORMAL ]  Iteration 134:	k_eff = 1.219905	res = 7.412E-05
[  NORMAL ]  Iteration 135:	k_eff = 1.219984	res = 6.907E-05
[  NORMAL ]  Iteration 136:	k_eff = 1.220057	res = 6.436E-05
[  NORMAL ]  Iteration 137:	k_eff = 1.220125	res = 5.997E-05
[  NORMAL ]  Iteration 138:	k_eff = 1.220188	res = 5.587E-05
[  NORMAL ]  Iteration 139:	k_eff = 1.220248	res = 5.205E-05
[  NORMAL ]  Iteration 140:	k_eff = 1.220303	res = 4.848E-05
[  NORMAL ]  Iteration 141:	k_eff = 1.220354	res = 4.516E-05
[  NORMAL ]  Iteration 142:	k_eff = 1.220402	res = 4.206E-05
[  NORMAL ]  Iteration 143:	k_eff = 1.220446	res = 3.917E-05
[  NORMAL ]  Iteration 144:	k_eff = 1.220488	res = 3.648E-05
[  NORMAL ]  Iteration 145:	k_eff = 1.220526	res = 3.397E-05
[  NORMAL ]  Iteration 146:	k_eff = 1.220562	res = 3.163E-05
[  NORMAL ]  Iteration 147:	k_eff = 1.220596	res = 2.945E-05
[  NORMAL ]  Iteration 148:	k_eff = 1.220627	res = 2.742E-05
[  NORMAL ]  Iteration 149:	k_eff = 1.220656	res = 2.552E-05
[  NORMAL ]  Iteration 150:	k_eff = 1.220683	res = 2.376E-05
[  NORMAL ]  Iteration 151:	k_eff = 1.220708	res = 2.212E-05
[  NORMAL ]  Iteration 152:	k_eff = 1.220732	res = 2.059E-05
[  NORMAL ]  Iteration 153:	k_eff = 1.220753	res = 1.916E-05
[  NORMAL ]  Iteration 154:	k_eff = 1.220774	res = 1.783E-05
[  NORMAL ]  Iteration 155:	k_eff = 1.220792	res = 1.660E-05
[  NORMAL ]  Iteration 156:	k_eff = 1.220810	res = 1.545E-05
[  NORMAL ]  Iteration 157:	k_eff = 1.220826	res = 1.437E-05
[  NORMAL ]  Iteration 158:	k_eff = 1.220841	res = 1.337E-05
[  NORMAL ]  Iteration 159:	k_eff = 1.220856	res = 1.244E-05
[  NORMAL ]  Iteration 160:	k_eff = 1.220869	res = 1.158E-05
[  NORMAL ]  Iteration 161:	k_eff = 1.220881	res = 1.077E-05
[  NORMAL ]  Iteration 162:	k_eff = 1.220892	res = 1.002E-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.223474
openmoc keff = 1.220892
bias [pcm]: -258.1

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.opencg_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())
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.495816	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.557477	res = 5.042E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.518300	res = 1.244E-01
[  NORMAL ]  Iteration 3:	k_eff = 0.509211	res = 7.027E-02
[  NORMAL ]  Iteration 4:	k_eff = 0.496489	res = 1.754E-02
[  NORMAL ]  Iteration 5:	k_eff = 0.488581	res = 2.498E-02
[  NORMAL ]  Iteration 6:	k_eff = 0.482897	res = 1.593E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.479775	res = 1.163E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.478834	res = 6.465E-03
[  NORMAL ]  Iteration 9:	k_eff = 0.479871	res = 1.960E-03
[  NORMAL ]  Iteration 10:	k_eff = 0.482684	res = 2.166E-03
[  NORMAL ]  Iteration 11:	k_eff = 0.487084	res = 5.861E-03
[  NORMAL ]  Iteration 12:	k_eff = 0.492900	res = 9.116E-03
[  NORMAL ]  Iteration 13:	k_eff = 0.499971	res = 1.194E-02
[  NORMAL ]  Iteration 14:	k_eff = 0.508153	res = 1.435E-02
[  NORMAL ]  Iteration 15:	k_eff = 0.517312	res = 1.637E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.527324	res = 1.802E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.538079	res = 1.935E-02
[  NORMAL ]  Iteration 18:	k_eff = 0.549472	res = 2.039E-02
[  NORMAL ]  Iteration 19:	k_eff = 0.561410	res = 2.117E-02
[  NORMAL ]  Iteration 20:	k_eff = 0.573807	res = 2.173E-02
[  NORMAL ]  Iteration 21:	k_eff = 0.586585	res = 2.208E-02
[  NORMAL ]  Iteration 22:	k_eff = 0.599674	res = 2.227E-02
[  NORMAL ]  Iteration 23:	k_eff = 0.613007	res = 2.231E-02
[  NORMAL ]  Iteration 24:	k_eff = 0.626528	res = 2.223E-02
[  NORMAL ]  Iteration 25:	k_eff = 0.640181	res = 2.206E-02
[  NORMAL ]  Iteration 26:	k_eff = 0.653920	res = 2.179E-02
[  NORMAL ]  Iteration 27:	k_eff = 0.667700	res = 2.146E-02
[  NORMAL ]  Iteration 28:	k_eff = 0.681483	res = 2.107E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.695234	res = 2.064E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.708921	res = 2.018E-02
[  NORMAL ]  Iteration 31:	k_eff = 0.722515	res = 1.969E-02
[  NORMAL ]  Iteration 32:	k_eff = 0.735993	res = 1.918E-02
[  NORMAL ]  Iteration 33:	k_eff = 0.749331	res = 1.865E-02
[  NORMAL ]  Iteration 34:	k_eff = 0.762510	res = 1.812E-02
[  NORMAL ]  Iteration 35:	k_eff = 0.775514	res = 1.759E-02
[  NORMAL ]  Iteration 36:	k_eff = 0.788326	res = 1.705E-02
[  NORMAL ]  Iteration 37:	k_eff = 0.800934	res = 1.652E-02
[  NORMAL ]  Iteration 38:	k_eff = 0.813328	res = 1.599E-02
[  NORMAL ]  Iteration 39:	k_eff = 0.825497	res = 1.547E-02
[  NORMAL ]  Iteration 40:	k_eff = 0.837433	res = 1.496E-02
[  NORMAL ]  Iteration 41:	k_eff = 0.849131	res = 1.446E-02
[  NORMAL ]  Iteration 42:	k_eff = 0.860584	res = 1.397E-02
[  NORMAL ]  Iteration 43:	k_eff = 0.871789	res = 1.349E-02
[  NORMAL ]  Iteration 44:	k_eff = 0.882742	res = 1.302E-02
[  NORMAL ]  Iteration 45:	k_eff = 0.893442	res = 1.256E-02
[  NORMAL ]  Iteration 46:	k_eff = 0.903888	res = 1.212E-02
[  NORMAL ]  Iteration 47:	k_eff = 0.914078	res = 1.169E-02
[  NORMAL ]  Iteration 48:	k_eff = 0.924013	res = 1.127E-02
[  NORMAL ]  Iteration 49:	k_eff = 0.933694	res = 1.087E-02
[  NORMAL ]  Iteration 50:	k_eff = 0.943122	res = 1.048E-02
[  NORMAL ]  Iteration 51:	k_eff = 0.952299	res = 1.010E-02
[  NORMAL ]  Iteration 52:	k_eff = 0.961228	res = 9.731E-03
[  NORMAL ]  Iteration 53:	k_eff = 0.969911	res = 9.376E-03
[  NORMAL ]  Iteration 54:	k_eff = 0.978351	res = 9.033E-03
[  NORMAL ]  Iteration 55:	k_eff = 0.986552	res = 8.702E-03
[  NORMAL ]  Iteration 56:	k_eff = 0.994517	res = 8.382E-03
[  NORMAL ]  Iteration 57:	k_eff = 1.002250	res = 8.074E-03
[  NORMAL ]  Iteration 58:	k_eff = 1.009756	res = 7.776E-03
[  NORMAL ]  Iteration 59:	k_eff = 1.017037	res = 7.488E-03
[  NORMAL ]  Iteration 60:	k_eff = 1.024100	res = 7.211E-03
[  NORMAL ]  Iteration 61:	k_eff = 1.030948	res = 6.944E-03
[  NORMAL ]  Iteration 62:	k_eff = 1.037585	res = 6.687E-03
[  NORMAL ]  Iteration 63:	k_eff = 1.044017	res = 6.438E-03
[  NORMAL ]  Iteration 64:	k_eff = 1.050248	res = 6.199E-03
[  NORMAL ]  Iteration 65:	k_eff = 1.056282	res = 5.968E-03
[  NORMAL ]  Iteration 66:	k_eff = 1.062125	res = 5.746E-03
[  NORMAL ]  Iteration 67:	k_eff = 1.067782	res = 5.532E-03
[  NORMAL ]  Iteration 68:	k_eff = 1.073256	res = 5.325E-03
[  NORMAL ]  Iteration 69:	k_eff = 1.078553	res = 5.127E-03
[  NORMAL ]  Iteration 70:	k_eff = 1.083677	res = 4.935E-03
[  NORMAL ]  Iteration 71:	k_eff = 1.088633	res = 4.751E-03
[  NORMAL ]  Iteration 72:	k_eff = 1.093425	res = 4.573E-03
[  NORMAL ]  Iteration 73:	k_eff = 1.098059	res = 4.402E-03
[  NORMAL ]  Iteration 74:	k_eff = 1.102538	res = 4.238E-03
[  NORMAL ]  Iteration 75:	k_eff = 1.106867	res = 4.079E-03
[  NORMAL ]  Iteration 76:	k_eff = 1.111050	res = 3.926E-03
[  NORMAL ]  Iteration 77:	k_eff = 1.115091	res = 3.779E-03
[  NORMAL ]  Iteration 78:	k_eff = 1.118996	res = 3.638E-03
[  NORMAL ]  Iteration 79:	k_eff = 1.122767	res = 3.501E-03
[  NORMAL ]  Iteration 80:	k_eff = 1.126408	res = 3.370E-03
[  NORMAL ]  Iteration 81:	k_eff = 1.129925	res = 3.244E-03
[  NORMAL ]  Iteration 82:	k_eff = 1.133320	res = 3.122E-03
[  NORMAL ]  Iteration 83:	k_eff = 1.136598	res = 3.005E-03
[  NORMAL ]  Iteration 84:	k_eff = 1.139762	res = 2.892E-03
[  NORMAL ]  Iteration 85:	k_eff = 1.142815	res = 2.784E-03
[  NORMAL ]  Iteration 86:	k_eff = 1.145762	res = 2.679E-03
[  NORMAL ]  Iteration 87:	k_eff = 1.148605	res = 2.578E-03
[  NORMAL ]  Iteration 88:	k_eff = 1.151348	res = 2.482E-03
[  NORMAL ]  Iteration 89:	k_eff = 1.153995	res = 2.388E-03
[  NORMAL ]  Iteration 90:	k_eff = 1.156548	res = 2.299E-03
[  NORMAL ]  Iteration 91:	k_eff = 1.159010	res = 2.212E-03
[  NORMAL ]  Iteration 92:	k_eff = 1.161384	res = 2.129E-03
[  NORMAL ]  Iteration 93:	k_eff = 1.163674	res = 2.049E-03
[  NORMAL ]  Iteration 94:	k_eff = 1.165883	res = 1.972E-03
[  NORMAL ]  Iteration 95:	k_eff = 1.168012	res = 1.898E-03
[  NORMAL ]  Iteration 96:	k_eff = 1.170064	res = 1.826E-03
[  NORMAL ]  Iteration 97:	k_eff = 1.172043	res = 1.757E-03
[  NORMAL ]  Iteration 98:	k_eff = 1.173951	res = 1.691E-03
[  NORMAL ]  Iteration 99:	k_eff = 1.175790	res = 1.628E-03
[  NORMAL ]  Iteration 100:	k_eff = 1.177562	res = 1.566E-03
[  NORMAL ]  Iteration 101:	k_eff = 1.179270	res = 1.507E-03
[  NORMAL ]  Iteration 102:	k_eff = 1.180916	res = 1.450E-03
[  NORMAL ]  Iteration 103:	k_eff = 1.182502	res = 1.396E-03
[  NORMAL ]  Iteration 104:	k_eff = 1.184030	res = 1.343E-03
[  NORMAL ]  Iteration 105:	k_eff = 1.185503	res = 1.292E-03
[  NORMAL ]  Iteration 106:	k_eff = 1.186922	res = 1.244E-03
[  NORMAL ]  Iteration 107:	k_eff = 1.188289	res = 1.197E-03
[  NORMAL ]  Iteration 108:	k_eff = 1.189605	res = 1.152E-03
[  NORMAL ]  Iteration 109:	k_eff = 1.190874	res = 1.108E-03
[  NORMAL ]  Iteration 110:	k_eff = 1.192096	res = 1.066E-03
[  NORMAL ]  Iteration 111:	k_eff = 1.193273	res = 1.026E-03
[  NORMAL ]  Iteration 112:	k_eff = 1.194407	res = 9.873E-04
[  NORMAL ]  Iteration 113:	k_eff = 1.195498	res = 9.500E-04
[  NORMAL ]  Iteration 114:	k_eff = 1.196550	res = 9.141E-04
[  NORMAL ]  Iteration 115:	k_eff = 1.197563	res = 8.796E-04
[  NORMAL ]  Iteration 116:	k_eff = 1.198538	res = 8.464E-04
[  NORMAL ]  Iteration 117:	k_eff = 1.199477	res = 8.144E-04
[  NORMAL ]  Iteration 118:	k_eff = 1.200381	res = 7.836E-04
[  NORMAL ]  Iteration 119:	k_eff = 1.201252	res = 7.539E-04
[  NORMAL ]  Iteration 120:	k_eff = 1.202091	res = 7.254E-04
[  NORMAL ]  Iteration 121:	k_eff = 1.202898	res = 6.980E-04
[  NORMAL ]  Iteration 122:	k_eff = 1.203675	res = 6.715E-04
[  NORMAL ]  Iteration 123:	k_eff = 1.204423	res = 6.461E-04
[  NORMAL ]  Iteration 124:	k_eff = 1.205144	res = 6.217E-04
[  NORMAL ]  Iteration 125:	k_eff = 1.205837	res = 5.981E-04
[  NORMAL ]  Iteration 126:	k_eff = 1.206505	res = 5.755E-04
[  NORMAL ]  Iteration 127:	k_eff = 1.207148	res = 5.537E-04
[  NORMAL ]  Iteration 128:	k_eff = 1.207766	res = 5.327E-04
[  NORMAL ]  Iteration 129:	k_eff = 1.208362	res = 5.125E-04
[  NORMAL ]  Iteration 130:	k_eff = 1.208935	res = 4.931E-04
[  NORMAL ]  Iteration 131:	k_eff = 1.209487	res = 4.744E-04
[  NORMAL ]  Iteration 132:	k_eff = 1.210018	res = 4.564E-04
[  NORMAL ]  Iteration 133:	k_eff = 1.210529	res = 4.391E-04
[  NORMAL ]  Iteration 134:	k_eff = 1.211021	res = 4.225E-04
[  NORMAL ]  Iteration 135:	k_eff = 1.211495	res = 4.064E-04
[  NORMAL ]  Iteration 136:	k_eff = 1.211950	res = 3.910E-04
[  NORMAL ]  Iteration 137:	k_eff = 1.212389	res = 3.762E-04
[  NORMAL ]  Iteration 138:	k_eff = 1.212811	res = 3.619E-04
[  NORMAL ]  Iteration 139:	k_eff = 1.213217	res = 3.482E-04
[  NORMAL ]  Iteration 140:	k_eff = 1.213608	res = 3.349E-04
[  NORMAL ]  Iteration 141:	k_eff = 1.213984	res = 3.222E-04
[  NORMAL ]  Iteration 142:	k_eff = 1.214346	res = 3.100E-04
[  NORMAL ]  Iteration 143:	k_eff = 1.214695	res = 2.982E-04
[  NORMAL ]  Iteration 144:	k_eff = 1.215030	res = 2.869E-04
[  NORMAL ]  Iteration 145:	k_eff = 1.215353	res = 2.760E-04
[  NORMAL ]  Iteration 146:	k_eff = 1.215663	res = 2.655E-04
[  NORMAL ]  Iteration 147:	k_eff = 1.215962	res = 2.554E-04
[  NORMAL ]  Iteration 148:	k_eff = 1.216249	res = 2.457E-04
[  NORMAL ]  Iteration 149:	k_eff = 1.216526	res = 2.364E-04
[  NORMAL ]  Iteration 150:	k_eff = 1.216792	res = 2.274E-04
[  NORMAL ]  Iteration 151:	k_eff = 1.217048	res = 2.187E-04
[  NORMAL ]  Iteration 152:	k_eff = 1.217294	res = 2.104E-04
[  NORMAL ]  Iteration 153:	k_eff = 1.217531	res = 2.024E-04
[  NORMAL ]  Iteration 154:	k_eff = 1.217759	res = 1.947E-04
[  NORMAL ]  Iteration 155:	k_eff = 1.217979	res = 1.873E-04
[  NORMAL ]  Iteration 156:	k_eff = 1.218190	res = 1.802E-04
[  NORMAL ]  Iteration 157:	k_eff = 1.218393	res = 1.733E-04
[  NORMAL ]  Iteration 158:	k_eff = 1.218588	res = 1.667E-04
[  NORMAL ]  Iteration 159:	k_eff = 1.218776	res = 1.604E-04
[  NORMAL ]  Iteration 160:	k_eff = 1.218957	res = 1.543E-04
[  NORMAL ]  Iteration 161:	k_eff = 1.219131	res = 1.484E-04
[  NORMAL ]  Iteration 162:	k_eff = 1.219298	res = 1.427E-04
[  NORMAL ]  Iteration 163:	k_eff = 1.219459	res = 1.373E-04
[  NORMAL ]  Iteration 164:	k_eff = 1.219614	res = 1.321E-04
[  NORMAL ]  Iteration 165:	k_eff = 1.219763	res = 1.270E-04
[  NORMAL ]  Iteration 166:	k_eff = 1.219907	res = 1.222E-04
[  NORMAL ]  Iteration 167:	k_eff = 1.220045	res = 1.176E-04
[  NORMAL ]  Iteration 168:	k_eff = 1.220177	res = 1.131E-04
[  NORMAL ]  Iteration 169:	k_eff = 1.220305	res = 1.088E-04
[  NORMAL ]  Iteration 170:	k_eff = 1.220428	res = 1.046E-04
[  NORMAL ]  Iteration 171:	k_eff = 1.220546	res = 1.006E-04
[  NORMAL ]  Iteration 172:	k_eff = 1.220660	res = 9.680E-05
[  NORMAL ]  Iteration 173:	k_eff = 1.220769	res = 9.311E-05
[  NORMAL ]  Iteration 174:	k_eff = 1.220874	res = 8.956E-05
[  NORMAL ]  Iteration 175:	k_eff = 1.220975	res = 8.614E-05
[  NORMAL ]  Iteration 176:	k_eff = 1.221073	res = 8.286E-05
[  NORMAL ]  Iteration 177:	k_eff = 1.221166	res = 7.970E-05
[  NORMAL ]  Iteration 178:	k_eff = 1.221256	res = 7.666E-05
[  NORMAL ]  Iteration 179:	k_eff = 1.221343	res = 7.373E-05
[  NORMAL ]  Iteration 180:	k_eff = 1.221426	res = 7.092E-05
[  NORMAL ]  Iteration 181:	k_eff = 1.221506	res = 6.822E-05
[  NORMAL ]  Iteration 182:	k_eff = 1.221583	res = 6.562E-05
[  NORMAL ]  Iteration 183:	k_eff = 1.221658	res = 6.311E-05
[  NORMAL ]  Iteration 184:	k_eff = 1.221729	res = 6.070E-05
[  NORMAL ]  Iteration 185:	k_eff = 1.221797	res = 5.839E-05
[  NORMAL ]  Iteration 186:	k_eff = 1.221863	res = 5.616E-05
[  NORMAL ]  Iteration 187:	k_eff = 1.221927	res = 5.402E-05
[  NORMAL ]  Iteration 188:	k_eff = 1.221988	res = 5.196E-05
[  NORMAL ]  Iteration 189:	k_eff = 1.222047	res = 4.997E-05
[  NORMAL ]  Iteration 190:	k_eff = 1.222103	res = 4.807E-05
[  NORMAL ]  Iteration 191:	k_eff = 1.222158	res = 4.623E-05
[  NORMAL ]  Iteration 192:	k_eff = 1.222210	res = 4.447E-05
[  NORMAL ]  Iteration 193:	k_eff = 1.222260	res = 4.277E-05
[  NORMAL ]  Iteration 194:	k_eff = 1.222308	res = 4.114E-05
[  NORMAL ]  Iteration 195:	k_eff = 1.222355	res = 3.957E-05
[  NORMAL ]  Iteration 196:	k_eff = 1.222400	res = 3.805E-05
[  NORMAL ]  Iteration 197:	k_eff = 1.222443	res = 3.660E-05
[  NORMAL ]  Iteration 198:	k_eff = 1.222484	res = 3.520E-05
[  NORMAL ]  Iteration 199:	k_eff = 1.222524	res = 3.386E-05
[  NORMAL ]  Iteration 200:	k_eff = 1.222562	res = 3.257E-05
[  NORMAL ]  Iteration 201:	k_eff = 1.222599	res = 3.132E-05
[  NORMAL ]  Iteration 202:	k_eff = 1.222635	res = 3.013E-05
[  NORMAL ]  Iteration 203:	k_eff = 1.222669	res = 2.898E-05
[  NORMAL ]  Iteration 204:	k_eff = 1.222701	res = 2.787E-05
[  NORMAL ]  Iteration 205:	k_eff = 1.222733	res = 2.680E-05
[  NORMAL ]  Iteration 206:	k_eff = 1.222763	res = 2.578E-05
[  NORMAL ]  Iteration 207:	k_eff = 1.222792	res = 2.480E-05
[  NORMAL ]  Iteration 208:	k_eff = 1.222820	res = 2.385E-05
[  NORMAL ]  Iteration 209:	k_eff = 1.222847	res = 2.294E-05
[  NORMAL ]  Iteration 210:	k_eff = 1.222873	res = 2.206E-05
[  NORMAL ]  Iteration 211:	k_eff = 1.222898	res = 2.122E-05
[  NORMAL ]  Iteration 212:	k_eff = 1.222922	res = 2.041E-05
[  NORMAL ]  Iteration 213:	k_eff = 1.222945	res = 1.963E-05
[  NORMAL ]  Iteration 214:	k_eff = 1.222968	res = 1.888E-05
[  NORMAL ]  Iteration 215:	k_eff = 1.222989	res = 1.816E-05
[  NORMAL ]  Iteration 216:	k_eff = 1.223009	res = 1.746E-05
[  NORMAL ]  Iteration 217:	k_eff = 1.223029	res = 1.680E-05
[  NORMAL ]  Iteration 218:	k_eff = 1.223048	res = 1.615E-05
[  NORMAL ]  Iteration 219:	k_eff = 1.223067	res = 1.554E-05
[  NORMAL ]  Iteration 220:	k_eff = 1.223084	res = 1.494E-05
[  NORMAL ]  Iteration 221:	k_eff = 1.223101	res = 1.437E-05
[  NORMAL ]  Iteration 222:	k_eff = 1.223117	res = 1.382E-05
[  NORMAL ]  Iteration 223:	k_eff = 1.223133	res = 1.329E-05
[  NORMAL ]  Iteration 224:	k_eff = 1.223148	res = 1.279E-05
[  NORMAL ]  Iteration 225:	k_eff = 1.223162	res = 1.230E-05
[  NORMAL ]  Iteration 226:	k_eff = 1.223176	res = 1.183E-05
[  NORMAL ]  Iteration 227:	k_eff = 1.223190	res = 1.137E-05
[  NORMAL ]  Iteration 228:	k_eff = 1.223203	res = 1.094E-05
[  NORMAL ]  Iteration 229:	k_eff = 1.223215	res = 1.052E-05
[  NORMAL ]  Iteration 230:	k_eff = 1.223227	res = 1.012E-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.223474
openmoc keff = 1.223227
bias [pcm]: -24.7

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 open source PyNE library to parse continuous-energy cross sections from the cross section data library provided with OpenMC. First, we instantiate a pyne.ace.Library object for U-235 as follows.

In [30]:
# Instantiate a PyNE ACE continuous-energy cross sections library
pyne_lib = pyne.ace.Library('../../../../data/nndc/293.6K/U_235_293.6K.ace')
pyne_lib.read('92235.71c')

# Extract the U-235 data from the library
u235 = pyne_lib.tables['92235.71c']

# Extract the continuous-energy U-235 fission cross section data
fission = u235.reactions[18]

Now, we use matplotlib and seaborn to plot the continuous-energy and multi-group cross sections on a single plot.

In [31]:
# Create a loglog plot of the U-235 continuous-energy fission cross section 
plt.loglog(u235.energy, fission.sigma, color='b', linewidth=1)

# Extract energy group bounds and MGXS values to plot
nufission = xs_library[fuel_cell.id]['fission']
energy_groups = nufission.energy_groups
x = energy_groups.group_edges
y = nufission.get_xs(nuclides=['U-235'], order_groups='decreasing', xs_type='micro')

# Fix low energy bound to the value defined by the ACE library
x[0] = u235.energy[0]

# Extend the mgxs values array for matplotlib's step plot
y = np.insert(y, 0, y[0])

# Create a step plot for the MGXS
plt.plot(x, y, drawstyle='steps', color='r', linewidth=3)

plt.title('U-235 Fission Cross Section')
plt.xlabel('Energy [MeV]')
plt.ylabel('Micro Fission XS')
plt.legend(['Continuous', 'Multi-Group'])
plt.xlim((x.min(), x.max()))
Out[31]:
(9.9999999999999994e-12, 20.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 [32]:
# 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'] == 'H-1']['mean']
o16 = df[df['nuclide'] == 'O-16']['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 [33]:
# 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')
plt.grid()

# 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')
plt.grid()

# Show the plot on screen
plt.show()
In [ ]:
 

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. In order to run this Notebook in its entirety, you must have OpenMOC installed on your system, along with OpenCG to convert the OpenMC geometries into OpenMOC geometries. 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
import openmoc
import openmoc.process
from openmoc.opencg_compatible import get_openmoc_geometry
from openmoc.materialize import load_openmc_mgxs_lib

%matplotlib inline
/home/wboyd/anaconda2/lib/python2.7/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('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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))
materials_file.default_xs = '71c'

# 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.625e-6, 20.])

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")
  • NuTransportXS ("nu-transport")
  • AbsorptionXS ("absorption")
  • CaptureXS ("capture")
  • FissionXS ("fission")
  • NuFissionXS ("nu-fission")
  • KappaFissionXS ("kappa-fission")
  • ScatterXS ("scatter")
  • NuScatterXS ("nu-scatter")
  • ScatterMatrixXS ("scatter matrix")
  • NuScatterMatrixXS ("nu-scatter matrix")
  • Chi ("chi")

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," and "universe" 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()

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.Filter()
mesh_filter.mesh = 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()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       47ef320ad517612376e181ec6a6bc42ca0db98ce
      Date/Time:      2016-05-14 12:29:07
      MPI Processes:  1

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 5010.71c
 Loading ACE cross section table: 40090.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 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.04472                       
        5/1    1.02183                       
        6/1    1.05263                       
        7/1    0.99048                       
        8/1    1.02753                       
        9/1    1.03159                       
       10/1    1.04005                       
       11/1    1.05278                       
       12/1    1.02555    1.03917 +/- 0.01362
       13/1    0.99400    1.02411 +/- 0.01699
       14/1    1.03508    1.02685 +/- 0.01232
       15/1    1.00055    1.02159 +/- 0.01090
       16/1    1.01334    1.02022 +/- 0.00900
       17/1    0.99822    1.01707 +/- 0.00823
       18/1    1.01767    1.01715 +/- 0.00713
       19/1    1.05052    1.02086 +/- 0.00730
       20/1    1.03133    1.02190 +/- 0.00661
       21/1    1.04112    1.02365 +/- 0.00623
       22/1    1.04175    1.02516 +/- 0.00588
       23/1    1.01909    1.02469 +/- 0.00543
       24/1    1.07119    1.02801 +/- 0.00603
       25/1    0.97445    1.02444 +/- 0.00665
       26/1    1.04737    1.02588 +/- 0.00638
       27/1    1.04656    1.02709 +/- 0.00612
       28/1    1.03464    1.02751 +/- 0.00578
       29/1    1.02528    1.02739 +/- 0.00547
       30/1    1.02799    1.02742 +/- 0.00519
       31/1    1.05846    1.02890 +/- 0.00516
       32/1    1.03811    1.02932 +/- 0.00493
       33/1    1.00894    1.02843 +/- 0.00480
       34/1    1.02049    1.02810 +/- 0.00460
       35/1    1.00690    1.02726 +/- 0.00450
       36/1    1.03129    1.02741 +/- 0.00432
       37/1    0.98864    1.02597 +/- 0.00440
       38/1    1.00017    1.02505 +/- 0.00434
       39/1    1.03635    1.02544 +/- 0.00421
       40/1    1.07090    1.02696 +/- 0.00434
       41/1    1.03141    1.02710 +/- 0.00420
       42/1    1.02624    1.02707 +/- 0.00406
       43/1    1.02668    1.02706 +/- 0.00394
       44/1    1.05940    1.02801 +/- 0.00394
       45/1    1.01149    1.02754 +/- 0.00385
       46/1    1.06958    1.02871 +/- 0.00392
       47/1    1.02674    1.02866 +/- 0.00381
       48/1    1.02542    1.02857 +/- 0.00371
       49/1    1.03516    1.02874 +/- 0.00362
       50/1    1.06818    1.02973 +/- 0.00366
 Creating state point statepoint.50.h5...

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


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

 Total time for initialization     =  5.7700E-01 seconds
   Reading cross sections          =  1.3400E-01 seconds
 Total time in simulation          =  8.0461E+01 seconds
   Time in transport only          =  8.0422E+01 seconds
   Time in inactive batches        =  6.4060E+00 seconds
   Time in active batches          =  7.4055E+01 seconds
   Time synchronizing fission bank =  6.0000E-03 seconds
     Sampling source sites         =  2.0000E-03 seconds
     SEND/RECV source sites        =  3.0000E-03 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  0.0000E+00 seconds
 Total time elapsed                =  8.1067E+01 seconds
 Calculation Rate (inactive)       =  3902.59 neutrons/second
 Calculation Rate (active)         =  1350.35 neutrons/second

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

 k-effective (Collision)     =  1.02763 +/-  0.00343
 k-effective (Track-length)  =  1.02973 +/-  0.00366
 k-effective (Absorption)    =  1.02732 +/-  0.00319
 Combined k-effective        =  1.02826 +/-  0.00259
 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/wboyd/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1988: 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 U-235 8.055246e-03 2.857567e-05
4 10000 1 U-238 7.339215e-03 4.349466e-05
5 10000 1 O-16 0.000000e+00 0.000000e+00
0 10000 2 U-235 3.615565e-01 2.050486e-03
1 10000 2 U-238 6.742638e-07 3.795256e-09
2 10000 2 O-16 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        =	U-235
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	8.06e-03 +/- 3.55e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	3.62e-01 +/- 5.67e-01%

	Nuclide        =	U-238
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	7.34e-03 +/- 5.93e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	6.74e-07 +/- 5.63e-01%

	Nuclide        =	O-16
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	0.00e+00 +/- nan%
            Group 2 [0.0        - 6.25e-07  MeV]:	0.00e+00 +/- nan%



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

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

# 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()
Out[36]:
cell group in nuclide mean std. dev.
0 10000 1 U-235 0.074860 0.000303
1 10000 1 U-238 0.005952 0.000035
2 10000 1 O-16 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 will extract an OpenCG geometry from the summary file and convert it into an equivalent OpenMOC geometry.

In [37]:
# Create an OpenMOC Geometry from the OpenCG Geometry
openmoc_geometry = get_openmoc_geometry(mgxs_lib.opencg_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)

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.854370	res = 0.000E+00
[  NORMAL ]  Iteration 1:	k_eff = 0.801922	res = 1.521E-01
[  NORMAL ]  Iteration 2:	k_eff = 0.761745	res = 6.349E-02
[  NORMAL ]  Iteration 3:	k_eff = 0.732366	res = 5.029E-02
[  NORMAL ]  Iteration 4:	k_eff = 0.711073	res = 3.869E-02
[  NORMAL ]  Iteration 5:	k_eff = 0.696554	res = 2.912E-02
[  NORMAL ]  Iteration 6:	k_eff = 0.687670	res = 2.044E-02
[  NORMAL ]  Iteration 7:	k_eff = 0.683465	res = 1.277E-02
[  NORMAL ]  Iteration 8:	k_eff = 0.683124	res = 6.142E-03
[  NORMAL ]  Iteration 9:	k_eff = 0.685943	res = 7.897E-04
[  NORMAL ]  Iteration 10:	k_eff = 0.691322	res = 4.180E-03
[  NORMAL ]  Iteration 11:	k_eff = 0.698747	res = 7.873E-03
[  NORMAL ]  Iteration 12:	k_eff = 0.707777	res = 1.076E-02
[  NORMAL ]  Iteration 13:	k_eff = 0.718040	res = 1.295E-02
[  NORMAL ]  Iteration 14:	k_eff = 0.729218	res = 1.452E-02
[  NORMAL ]  Iteration 15:	k_eff = 0.741045	res = 1.559E-02
[  NORMAL ]  Iteration 16:	k_eff = 0.753296	res = 1.624E-02
[  NORMAL ]  Iteration 17:	k_eff = 0.765785	res = 1.655E-02
[  NORMAL ]  Iteration 18:	k_eff = 0.778355	res = 1.659E-02
[  NORMAL ]  Iteration 19:	k_eff = 0.790879	res = 1.643E-02
[  NORMAL ]  Iteration 20:	k_eff = 0.803254	res = 1.610E-02
[  NORMAL ]  Iteration 21:	k_eff = 0.815394	res = 1.566E-02
[  NORMAL ]  Iteration 22:	k_eff = 0.827235	res = 1.513E-02
[  NORMAL ]  Iteration 23:	k_eff = 0.838724	res = 1.453E-02
[  NORMAL ]  Iteration 24:	k_eff = 0.849823	res = 1.390E-02
[  NORMAL ]  Iteration 25:	k_eff = 0.860503	res = 1.324E-02
[  NORMAL ]  Iteration 26:	k_eff = 0.870744	res = 1.258E-02
[  NORMAL ]  Iteration 27:	k_eff = 0.880535	res = 1.191E-02
[  NORMAL ]  Iteration 28:	k_eff = 0.889870	res = 1.125E-02
[  NORMAL ]  Iteration 29:	k_eff = 0.898748	res = 1.061E-02
[  NORMAL ]  Iteration 30:	k_eff = 0.907172	res = 9.985E-03
[  NORMAL ]  Iteration 31:	k_eff = 0.915151	res = 9.382E-03
[  NORMAL ]  Iteration 32:	k_eff = 0.922693	res = 8.802E-03
[  NORMAL ]  Iteration 33:	k_eff = 0.929811	res = 8.248E-03
[  NORMAL ]  Iteration 34:	k_eff = 0.936517	res = 7.720E-03
[  NORMAL ]  Iteration 35:	k_eff = 0.942827	res = 7.219E-03
[  NORMAL ]  Iteration 36:	k_eff = 0.948757	res = 6.744E-03
[  NORMAL ]  Iteration 37:	k_eff = 0.954322	res = 6.295E-03
[  NORMAL ]  Iteration 38:	k_eff = 0.959539	res = 5.871E-03
[  NORMAL ]  Iteration 39:	k_eff = 0.964425	res = 5.472E-03
[  NORMAL ]  Iteration 40:	k_eff = 0.968996	res = 5.096E-03
[  NORMAL ]  Iteration 41:	k_eff = 0.973268	res = 4.744E-03
[  NORMAL ]  Iteration 42:	k_eff = 0.977259	res = 4.413E-03
[  NORMAL ]  Iteration 43:	k_eff = 0.980982	res = 4.104E-03
[  NORMAL ]  Iteration 44:	k_eff = 0.984454	res = 3.814E-03
[  NORMAL ]  Iteration 45:	k_eff = 0.987689	res = 3.543E-03
[  NORMAL ]  Iteration 46:	k_eff = 0.990702	res = 3.289E-03
[  NORMAL ]  Iteration 47:	k_eff = 0.993505	res = 3.053E-03
[  NORMAL ]  Iteration 48:	k_eff = 0.996112	res = 2.832E-03
[  NORMAL ]  Iteration 49:	k_eff = 0.998536	res = 2.627E-03
[  NORMAL ]  Iteration 50:	k_eff = 1.000787	res = 2.435E-03
[  NORMAL ]  Iteration 51:	k_eff = 1.002878	res = 2.257E-03
[  NORMAL ]  Iteration 52:	k_eff = 1.004818	res = 2.091E-03
[  NORMAL ]  Iteration 53:	k_eff = 1.006618	res = 1.937E-03
[  NORMAL ]  Iteration 54:	k_eff = 1.008287	res = 1.793E-03
[  NORMAL ]  Iteration 55:	k_eff = 1.009834	res = 1.660E-03
[  NORMAL ]  Iteration 56:	k_eff = 1.011268	res = 1.536E-03
[  NORMAL ]  Iteration 57:	k_eff = 1.012595	res = 1.421E-03
[  NORMAL ]  Iteration 58:	k_eff = 1.013824	res = 1.314E-03
[  NORMAL ]  Iteration 59:	k_eff = 1.014962	res = 1.215E-03
[  NORMAL ]  Iteration 60:	k_eff = 1.016015	res = 1.123E-03
[  NORMAL ]  Iteration 61:	k_eff = 1.016988	res = 1.038E-03
[  NORMAL ]  Iteration 62:	k_eff = 1.017889	res = 9.595E-04
[  NORMAL ]  Iteration 63:	k_eff = 1.018721	res = 8.864E-04
[  NORMAL ]  Iteration 64:	k_eff = 1.019490	res = 8.187E-04
[  NORMAL ]  Iteration 65:	k_eff = 1.020201	res = 7.560E-04
[  NORMAL ]  Iteration 66:	k_eff = 1.020858	res = 6.980E-04
[  NORMAL ]  Iteration 67:	k_eff = 1.021464	res = 6.444E-04
[  NORMAL ]  Iteration 68:	k_eff = 1.022024	res = 5.947E-04
[  NORMAL ]  Iteration 69:	k_eff = 1.022541	res = 5.488E-04
[  NORMAL ]  Iteration 70:	k_eff = 1.023017	res = 5.063E-04
[  NORMAL ]  Iteration 71:	k_eff = 1.023458	res = 4.670E-04
[  NORMAL ]  Iteration 72:	k_eff = 1.023863	res = 4.308E-04
[  NORMAL ]  Iteration 73:	k_eff = 1.024238	res = 3.972E-04
[  NORMAL ]  Iteration 74:	k_eff = 1.024583	res = 3.663E-04
[  NORMAL ]  Iteration 75:	k_eff = 1.024902	res = 3.376E-04
[  NORMAL ]  Iteration 76:	k_eff = 1.025195	res = 3.112E-04
[  NORMAL ]  Iteration 77:	k_eff = 1.025466	res = 2.868E-04
[  NORMAL ]  Iteration 78:	k_eff = 1.025715	res = 2.643E-04
[  NORMAL ]  Iteration 79:	k_eff = 1.025945	res = 2.435E-04
[  NORMAL ]  Iteration 80:	k_eff = 1.026157	res = 2.244E-04
[  NORMAL ]  Iteration 81:	k_eff = 1.026352	res = 2.067E-04
[  NORMAL ]  Iteration 82:	k_eff = 1.026531	res = 1.904E-04
[  NORMAL ]  Iteration 83:	k_eff = 1.026697	res = 1.753E-04
[  NORMAL ]  Iteration 84:	k_eff = 1.026849	res = 1.614E-04
[  NORMAL ]  Iteration 85:	k_eff = 1.026989	res = 1.487E-04
[  NORMAL ]  Iteration 86:	k_eff = 1.027118	res = 1.369E-04
[  NORMAL ]  Iteration 87:	k_eff = 1.027237	res = 1.260E-04
[  NORMAL ]  Iteration 88:	k_eff = 1.027347	res = 1.160E-04
[  NORMAL ]  Iteration 89:	k_eff = 1.027447	res = 1.067E-04
[  NORMAL ]  Iteration 90:	k_eff = 1.027540	res = 9.823E-05
[  NORMAL ]  Iteration 91:	k_eff = 1.027625	res = 9.039E-05
[  NORMAL ]  Iteration 92:	k_eff = 1.027704	res = 8.317E-05
[  NORMAL ]  Iteration 93:	k_eff = 1.027776	res = 7.652E-05
[  NORMAL ]  Iteration 94:	k_eff = 1.027843	res = 7.040E-05
[  NORMAL ]  Iteration 95:	k_eff = 1.027904	res = 6.476E-05
[  NORMAL ]  Iteration 96:	k_eff = 1.027960	res = 5.957E-05
[  NORMAL ]  Iteration 97:	k_eff = 1.028012	res = 5.479E-05
[  NORMAL ]  Iteration 98:	k_eff = 1.028059	res = 5.039E-05
[  NORMAL ]  Iteration 99:	k_eff = 1.028103	res = 4.635E-05
[  NORMAL ]  Iteration 100:	k_eff = 1.028143	res = 4.262E-05
[  NORMAL ]  Iteration 101:	k_eff = 1.028180	res = 3.919E-05
[  NORMAL ]  Iteration 102:	k_eff = 1.028214	res = 3.603E-05
[  NORMAL ]  Iteration 103:	k_eff = 1.028245	res = 3.313E-05
[  NORMAL ]  Iteration 104:	k_eff = 1.028274	res = 3.046E-05
[  NORMAL ]  Iteration 105:	k_eff = 1.028300	res = 2.800E-05
[  NORMAL ]  Iteration 106:	k_eff = 1.028324	res = 2.574E-05
[  NORMAL ]  Iteration 107:	k_eff = 1.028347	res = 2.366E-05
[  NORMAL ]  Iteration 108:	k_eff = 1.028367	res = 2.175E-05
[  NORMAL ]  Iteration 109:	k_eff = 1.028386	res = 1.999E-05
[  NORMAL ]  Iteration 110:	k_eff = 1.028403	res = 1.837E-05
[  NORMAL ]  Iteration 111:	k_eff = 1.028419	res = 1.688E-05
[  NORMAL ]  Iteration 112:	k_eff = 1.028434	res = 1.551E-05
[  NORMAL ]  Iteration 113:	k_eff = 1.028447	res = 1.426E-05
[  NORMAL ]  Iteration 114:	k_eff = 1.028460	res = 1.310E-05
[  NORMAL ]  Iteration 115:	k_eff = 1.028471	res = 1.204E-05
[  NORMAL ]  Iteration 116:	k_eff = 1.028481	res = 1.106E-05
[  NORMAL ]  Iteration 117:	k_eff = 1.028491	res = 1.016E-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.028263
openmoc keff = 1.028491
bias [pcm]: 22.8

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

MGXS Part IV: Multi-Group Mode Cross-Section Library

mgxs-part-iv

This Notebook illustrates the use of the openmc.mgxs.Library class specifically for application in OpenMC's multi-group mode. This example notebook follows the same process as was done in MGXS Part III, but instead uses OpenMC as the multi-group solver. 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.

Note: This Notebook illustrates the use of Pandas DataFrames to containerize multi-group cross section data. We recommend using Pandas >v0.15.0 or later since OpenMC's Python API leverages the multi-indexing feature included in the most recent releases of Pandas.

Generate Input Files

In [1]:
import math
import pickle

from IPython.display import Image
import matplotlib.pyplot as plt
import numpy as np
import os

import openmc
import openmc.mgxs

%matplotlib inline

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('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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)

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

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

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, zircaloy, water))
materials_file.default_xs = '71c'

# 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(name='root universe', universe_id=0)
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
# 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 5000 particles.

In [12]:
# 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': 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 [13]:
# Instantiate a Plot
plot = openmc.Plot()
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 [14]:
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
Out[14]:
0
In [15]:
# 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[15]:

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 [16]:
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625e-6, 20.])

Next, we will instantiate an openmc.mgxs.Library for the energy groups with our the fuel assembly geometry.

In [17]:
# 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. 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. At this time the MGXS Library class only supports the generation of isotropic flux-weighted cross sections and P0 scattering, so that is what will be used for this example. Therefore, 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 [18]:
# 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," and "universe" domain types. In this simple example, we wish to compute multi-group cross sections only for each material andtherefore will use a "material" domain type.

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

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 [20]:
# 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 [21]:
# Set the Legendre order to 3 for P3 scattering
mgxs_lib.legendre_order = 3
/home/nelsonag/git/openmc/openmc/mgxs/library.py:320: RuntimeWarning: The P0 correction will be ignored since the scattering order 0 is greater than zero
  warnings.warn(msg, RuntimeWarning)

Now that the Library has been setup, lets make sure 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 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 [22]:
# Check the library - if no errors are raised, then the library is satisfactory.
mgxs_lib.check_library_for_openmc_mgxs()

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 [23]:
# 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 parameter (False by default) for the Library.add_to_tallies_file(...) method, as shown below.

In [24]:
# 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 the multi-group result.

In [25]:
# 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.Filter()
mesh_filter.mesh = 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 [26]:
# Run OpenMC
openmc.run()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       826d5a43d85eaec1b6c7b4ce22e1a8f5e9336a4f
      Date/Time:      2016-06-08 19:33:38
      OpenMP Threads: 4

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading ACE cross section table: 92235.71c
 Loading ACE cross section table: 92238.71c
 Loading ACE cross section table: 8016.71c
 Loading ACE cross section table: 40090.71c
 Loading ACE cross section table: 1001.71c
 Loading ACE cross section table: 5010.71c
 Maximum neutron transport energy: 20.0000 MeV for 92235.71c
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.05201                       
        2/1    1.02017                       
        3/1    1.02398                       
        4/1    1.02677                       
        5/1    1.01070                       
        6/1    1.02964                       
        7/1    1.02163                       
        8/1    1.04524                       
        9/1    1.00773                       
       10/1    1.01536                       
       11/1    1.02992                       
       12/1    1.03248    1.03120 +/- 0.00128
       13/1    0.99044    1.01761 +/- 0.01361
       14/1    1.01484    1.01692 +/- 0.00965
       15/1    1.01491    1.01652 +/- 0.00748
       16/1    1.03809    1.02011 +/- 0.00709
       17/1    1.02536    1.02086 +/- 0.00604
       18/1    1.03663    1.02283 +/- 0.00559
       19/1    1.03902    1.02463 +/- 0.00525
       20/1    1.01557    1.02373 +/- 0.00478
       21/1    1.01286    1.02274 +/- 0.00443
       22/1    1.01392    1.02200 +/- 0.00411
       23/1    1.04439    1.02372 +/- 0.00416
       24/1    1.04034    1.02491 +/- 0.00403
       25/1    0.99433    1.02287 +/- 0.00427
       26/1    1.02720    1.02314 +/- 0.00400
       27/1    1.03545    1.02387 +/- 0.00383
       28/1    1.03853    1.02468 +/- 0.00370
       29/1    1.02735    1.02482 +/- 0.00350
       30/1    1.02429    1.02480 +/- 0.00332
       31/1    1.02901    1.02500 +/- 0.00317
       32/1    1.03296    1.02536 +/- 0.00304
       33/1    1.03605    1.02582 +/- 0.00294
       34/1    1.04247    1.02652 +/- 0.00290
       35/1    1.02088    1.02629 +/- 0.00279
       36/1    1.03017    1.02644 +/- 0.00269
       37/1    1.03216    1.02665 +/- 0.00259
       38/1    1.01459    1.02622 +/- 0.00254
       39/1    1.03706    1.02659 +/- 0.00248
       40/1    1.01383    1.02617 +/- 0.00243
       41/1    0.99043    1.02502 +/- 0.00262
       42/1    1.02891    1.02514 +/- 0.00254
       43/1    1.02100    1.02501 +/- 0.00246
       44/1    0.99546    1.02414 +/- 0.00254
       45/1    1.01562    1.02390 +/- 0.00248
       46/1    1.03025    1.02408 +/- 0.00242
       47/1    0.99409    1.02327 +/- 0.00249
       48/1    1.04355    1.02380 +/- 0.00248
       49/1    1.02763    1.02390 +/- 0.00242
       50/1    0.99426    1.02316 +/- 0.00247
 Creating state point statepoint.50.h5...

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


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

 Total time for initialization     =  1.4220E+00 seconds
   Reading cross sections          =  1.1320E+00 seconds
 Total time in simulation          =  1.6571E+01 seconds
   Time in transport only          =  1.6501E+01 seconds
   Time in inactive batches        =  2.1010E+00 seconds
   Time in active batches          =  1.4470E+01 seconds
   Time synchronizing fission bank =  5.0000E-03 seconds
     Sampling source sites         =  4.0000E-03 seconds
     SEND/RECV source sites        =  1.0000E-03 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  0.0000E+00 seconds
 Total time elapsed                =  1.8002E+01 seconds
 Calculation Rate (inactive)       =  23798.2 neutrons/second
 Calculation Rate (active)         =  13821.7 neutrons/second

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

 k-effective (Collision)     =  1.02389 +/-  0.00235
 k-effective (Track-length)  =  1.02316 +/-  0.00247
 k-effective (Absorption)    =  1.02494 +/-  0.00180
 Combined k-effective        =  1.02429 +/-  0.00140
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[26]:
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 [27]:
# Move the StatePoint File
ce_spfile = './ce_statepoint.h5'
os.rename('statepoint.' + str(batches) + '.h5', ce_spfile)
# Move the Summary file
ce_sumfile = './ce_summary.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 [28]:
# 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.mgxs module 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 [29]:
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 [30]:
# 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 divisions by zero errors. This will show up as a runtime warning in the following step.

In [31]:
# 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'],
                                       xs_ids='2m')

# Write the file to disk using the default filename of `mgxs.xml`
mgxs_file.export_to_xml()
/home/nelsonag/git/openmc/openmc/tallies.py:1990: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1991: RuntimeWarning: invalid value encountered in true_divide
  other_rel_err = data['other']['std. dev.'] / data['other']['mean']
/home/nelsonag/git/openmc/openmc/tallies.py:1992: 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 (for example, if using a macroscopic cross section library instead of individual microscopic nuclide cross sections as is done in continuous-energy, or if multiple cross sections exist for the same material due to the material existing in varied spectral regions).

Since this example is using material-wise macroscopic cross sections without considering that the neutron energy spectra and thus cross sections may be changing in space, we only need to modify the materials.xml and settings.xml files. If the material names and ids are not otherwise changed, then the geometry.xml file does not need to be modified from its continuous-energy form. The tallies.xml file will be left untouched as it currently contains the tally types that we will need to perform our comparison.

First we will create the new materials.xml file. Continuous-energy cross section nuclidic data sets are named with the nuclide name followed by a cross section identifier. For example, the data for hydrogen is accessed in OpenMC by the name H-1.71c. The cross-section identifier (in this case, 71c) can be used to distinguish between different variants of H-1 data, such as for different evaluations or temperatures. OpenMC multi-group libraries use the same convention of a name followed by a xs identifier. We will use a cross section identifier here of 2m. Similar to how continuous-energy cross section libraries are named, the openmc.Macroscopic quantities below can either have their xs_id included (i.e., 'fuel.2m'). An alternative is to leave this extension off and simply change the default_xs parameter to .2m.

In [32]:
# Instantiate our Macroscopic Data
fuel_macro = openmc.Macroscopic('fuel')
zircaloy_macro = openmc.Macroscopic('zircaloy')
water_macro = openmc.Macroscopic('water')

# Now re-define our materials to use the Multi-Group macroscopic data
# instead of the continuous-energy data.
# 1.6 enriched fuel UO2
fuel = openmc.Material(name='UO2')
fuel.add_macroscopic(fuel_macro)

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

# moderator
water = openmc.Material(name='Water')
water.add_macroscopic(water_macro)

# Finally, instantiate our Materials object
materials_file = openmc.Materials((fuel, zircaloy, water))
materials_file.default_xs = '2m'

# 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 settings file. These changes are limited to telling OpenMC we will be running a multi-group calculation and pointing to the location of our multi-group cross section file.

In [33]:
# Set the location of the cross sections file
settings_file.cross_sections = './mgxs.xml'
settings_file.energy_mode = 'multi-group'

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

Finally, since we want similar tally data in the end, we will leave our pre-existing tallies.xml file for this calculation.

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

In [34]:
# Run the Multi-Group OpenMC Simulation
openmc.run()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

      Copyright:      2011-2016 Massachusetts Institute of Technology
      License:        http://openmc.readthedocs.io/en/latest/license.html
      Version:        0.7.1
      Git SHA1:       826d5a43d85eaec1b6c7b4ce22e1a8f5e9336a4f
      Date/Time:      2016-06-08 19:33:56
      OpenMP Threads: 4

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

 Reading settings XML file...
 Reading cross sections XML file...
 Reading geometry XML file...
 Reading materials XML file...
 Reading tallies XML file...
 Building neighboring cells lists for each surface...
 Loading Cross Section Data...
 Loading fuel.2m Data...
 Loading zircaloy.2m Data...
 Loading water.2m Data...
 Initializing source particles...

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    0.99367                       
        2/1    1.03173                       
        3/1    1.01999                       
        4/1    1.01421                       
        5/1    1.03980                       
        6/1    1.04540                       
        7/1    1.04199                       
        8/1    1.02680                       
        9/1    1.01267                       
       10/1    1.03420                       
       11/1    1.05773                       
       12/1    1.03475    1.04624 +/- 0.01149
       13/1    1.03632    1.04293 +/- 0.00741
       14/1    0.99297    1.03044 +/- 0.01355
       15/1    1.02413    1.02918 +/- 0.01057
       16/1    1.02359    1.02825 +/- 0.00868
       17/1    0.99913    1.02409 +/- 0.00843
       18/1    1.01493    1.02294 +/- 0.00739
       19/1    1.03010    1.02374 +/- 0.00657
       20/1    1.04890    1.02626 +/- 0.00639
       21/1    1.01267    1.02502 +/- 0.00591
       22/1    1.02637    1.02513 +/- 0.00540
       23/1    1.01374    1.02426 +/- 0.00504
       24/1    1.06661    1.02728 +/- 0.00556
       25/1    1.03212    1.02760 +/- 0.00519
       26/1    1.05433    1.02927 +/- 0.00513
       27/1    0.99891    1.02749 +/- 0.00514
       28/1    1.00616    1.02630 +/- 0.00499
       29/1    1.04583    1.02733 +/- 0.00483
       30/1    1.01512    1.02672 +/- 0.00462
       31/1    0.98104    1.02455 +/- 0.00491
       32/1    1.04202    1.02534 +/- 0.00474
       33/1    1.00779    1.02458 +/- 0.00460
       34/1    1.02450    1.02457 +/- 0.00440
       35/1    0.98882    1.02314 +/- 0.00446
       36/1    1.01541    1.02285 +/- 0.00429
       37/1    1.02050    1.02276 +/- 0.00413
       38/1    1.03573    1.02322 +/- 0.00401
       39/1    1.03649    1.02368 +/- 0.00389
       40/1    1.01434    1.02337 +/- 0.00378
       41/1    1.02345    1.02337 +/- 0.00365
       42/1    1.01900    1.02323 +/- 0.00354
       43/1    1.01450    1.02297 +/- 0.00344
       44/1    1.03127    1.02321 +/- 0.00335
       45/1    1.01598    1.02301 +/- 0.00326
       46/1    1.00851    1.02260 +/- 0.00319
       47/1    1.03406    1.02291 +/- 0.00312
       48/1    1.02373    1.02294 +/- 0.00303
       49/1    1.04066    1.02339 +/- 0.00299
       50/1    1.02011    1.02331 +/- 0.00292
 Creating state point statepoint.50.h5...

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


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

 Total time for initialization     =  3.1000E-02 seconds
   Reading cross sections          =  5.0000E-03 seconds
 Total time in simulation          =  1.1867E+01 seconds
   Time in transport only          =  1.1830E+01 seconds
   Time in inactive batches        =  1.2670E+00 seconds
   Time in active batches          =  1.0600E+01 seconds
   Time synchronizing fission bank =  7.0000E-03 seconds
     Sampling source sites         =  7.0000E-03 seconds
     SEND/RECV source sites        =  0.0000E+00 seconds
   Time accumulating tallies       =  0.0000E+00 seconds
 Total time for finalization       =  0.0000E+00 seconds
 Total time elapsed                =  1.1907E+01 seconds
 Calculation Rate (inactive)       =  39463.3 neutrons/second
 Calculation Rate (active)         =  18867.9 neutrons/second

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

 k-effective (Collision)     =  1.02638 +/-  0.00260
 k-effective (Track-length)  =  1.02331 +/-  0.00292
 k-effective (Absorption)    =  1.02579 +/-  0.00132
 Combined k-effective        =  1.02558 +/-  0.00136
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[34]:
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. Since we did not rename the summary file, we do not need to load it separately this time.

In [35]:
# Load the last statepoint file and keff value
mgsp = openmc.StatePoint('statepoint.' + str(batches) + '.h5')
mg_keff = mgsp.k_combined

Next, we can load the continuous-energy eigenvalue for comparison.

In [36]:
ce_keff = sp.k_combined

Lets compare the two eigenvalues, including their bias

In [37]:
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.024295
Multi-Group keff = 1.025577
bias [pcm]: -128.2

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.

Flux and Pin Power Visualizations

Next we will visualize the mesh tally 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 [38]:
# 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)

Now we can do the same for the Continuous-Energy results.

In [39]:
# 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 [40]:
# 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[40]:
<matplotlib.text.Text at 0x7fbaddf68550>

We also see very good agreement between the fission rate distributions, though these should converge closer together with an increasing number of particle histories in both the continuous-energy run to generate the multi-group cross sections, and in the multi-group calculation itself.

In [ ]:
 

openmc – Basic Functionality

Handling nuclear data

Classes
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.
Functions
openmc.ace.ascii_to_binary Convert an ACE file in ASCII format (type 1) to binary format (type 2).

Simulation Settings

openmc.Source Distribution of phase space coordinates for source sites.
openmc.ResonanceScattering Specification of the elastic scattering model for resonant isotopes
openmc.Settings Settings file used for an OpenMC simulation.

Material Specification

openmc.Nuclide A nuclide that can be used in a material.
openmc.Element A natural element used in a material via <element>.
openmc.Macroscopic A Macroscopic object that can be used in a material.
openmc.Material A material composed of a collection of nuclides/elements that can be assigned to a region of space.
openmc.Materials Collection of Materials used for an OpenMC simulation.

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.

One function is also available to create a hexagonal region defined by the intersection of six surface half-spaces.

openmc.make_hexagon_region Create a hexagon region from six surface planes.

Constructing Tallies

openmc.Filter A filter used to constrain a tally to a specific criterion, e.g.
openmc.Mesh A structured Cartesian mesh in two or three dimensions
openmc.Trigger A criterion for when to finish a simulation based on tally uncertainties.
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.

Plotting

openmc.Plot Definition of a finite region of space to be plotted, either as a slice plot in two dimensions or as a voxel plot in three dimensions.
openmc.Plots Collection of Plots used for an OpenMC simulation.

Running OpenMC

openmc.run Run an OpenMC simulation.
openmc.plot_geometry Run OpenMC in plotting mode

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 Information summarizing the 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.

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.KappaFissionXS A recoverable fission energy production rate multi-group cross section.
openmc.mgxs.MultiplicityMatrixXS The scattering multiplicity matrix.
openmc.mgxs.NuFissionXS A fission neutron production multi-group cross section.
openmc.mgxs.NuFissionMatrixXS A fission production matrix multi-group cross section.
openmc.mgxs.NuScatterXS A scattering neutron production multi-group cross section.
openmc.mgxs.NuScatterMatrixXS A scattering production matrix multi-group cross section for one or more Legendre moments.
openmc.mgxs.ScatterXS A scattering multi-group cross section.
openmc.mgxs.ScatterMatrixXS A scattering matrix multi-group cross section for one or more Legendre moments.
openmc.mgxs.TotalXS A total multi-group cross section.
openmc.mgxs.TransportXS A transport-corrected total multi-group cross section.

Multi-group Cross Section Libraries

openmc.mgxs.Library A multi-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.

File Format Specifications

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.

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 data set, including the name and a file system location where the complete library can be found. In multi-group mode, this mgxs.xml file contains this same meta-data describing the nuclide or material, but also contains the group-wise nuclear data. This portion of the manual describes the format of the multi-group data library required to be used in the mgxs.xml file.

Similar to the other input file types, the multi-group library is provided in the XML format. This library must provide some meta-data about the library itself (such as the number of groups and the group structure, etc.) as well as the actual cross section data itself for each of the necessary nuclides or materials.

MGXS Library Specification

The multi-group library meta-data is contained within the groups, group_structure, and inverse_velocities elements. The actual multi-group data itself is contained within the xsdata element.

<groups> Element

The <groups> element has no attributes and simply provides the number of energy groups contained within the library.

Default: None, this must be provided.
<group_structure> Element

The <group_structure> element has no attributes and should be provided as a monotonically increasing list of bounding energies, in MeV, for a number of groups. To provide proper energy boundaries, the length of the data within the <group_structure> element should be one more than the number of groups in the problem. For example, a two-group problem could be specified as:

<group_structure> 0.0 0.625E-6 20.0 </group_structure>

Default: None, this must be provided.

<inverse_velocities> Element

The <inverse_velocities> element optionally indicates the average inverse velocity corresponding to each of the groups in the problem. This element should therefore be an array with a length which matches the number of groups set in the groups element.

Default: Should this be needed by the presence of an inverse-velocity score in the tallies.xml file and not provided in this element, OpenMC will simply convert the group mid-point energy to an inverse of the velocity and use this information for tallying.

<xsdata> Element

The <xsdata> element contains the nuclide or material-specific meta-data as well as the actual cross section data. The following are the attributes/sub-elements required to describe the meta-data:

name:

The name of the microscopic or macroscopic data set. An extension to the name must be provided (e.g., the .300K in UO2.300K). The name and extension together must be twelve or less characters in length. This extension must follow a period and be five characters or less in length. similar to the equivalent in the continuous-energy cross_sections.xml file, is used to denote variants of the particular nuclide or material of interest (i.e. the UO2 data in this example could have been generated at a temperature of 300K).

Default: None, this must be provided.

alias:

An alternative name to use for the microscopic or macroscopic data set.

Default: If no alias is provided, it will adopt the value of name.

kT:

The temperature times Boltzmann’s constant (in units of MeV) at which the data was generated.

Default: Room temperature, 2.53E-8 MeV

fissionable:

This element states whether or not the data in question is fissionable. Accepted values are “true” or “false”.

Default: None, this element must be provided.

representation:

This element provides 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 an equivalent representation) and thus are angle-independent, or if the data was generated with angular dependent fluxes and thus the data is angle-dependent. The options are either “isotropic” or “angle”.

Default: “isotropic”

num_azimuthal:

This element provides the number of equal width angular bins that the azimuthal angular domain is subdivided in the case of angle-dependent cross sections (i.e., “angle” is passed to the representation element). Note that these bins are equal in azimuthal angle widths, not equal in the cosine of the azimuthal angle widths.

Default: If representation is “angle”, this must be provided. This parameter is not used for other representation types.

num_polar:

This element provides the number of equal width angular bins that the polar angular domain is subdivided in the case of angle-dependent cross sections (i.e., “angle” is passed to the representation element). Note that these bins are equal in polar angle widths, not equal in the cosine of the polar angle widths.

Default: If representation is “angle”, this must be provided. This parameter is not used for other representation types.

scatt_type:

This element provides the representation of the angular distribution associated with each group-to-group transfer probability. The options are either “legendre”, “histogram”, or “tabular”. The “legendre” option means the angular distribution has been expanded via Legendre polynomials of the order provided in the “order” element. The “histogram” option means the angular distribution is provided in an equi-width histogram format with a number of bins as provided in the “order” element. This is useful when the angular distribution was obtained from a Monte Carlo tally and thus is natively in the histogram format. The “tabular” option means the angular distribution is provided in an equi-spaced point-wise representation.

Default: “legendre”

order:

This element provides either the Legendre order, number of bins, or number of points used to describe the angular distribution associated with each group-to-group transfer probability. The specific meaning of this bin depends upon the value of scatt_type as discussed above.

Default: None, this element must be provided.

tabular_legendre:
 

This optional element is used to set how the Legendre scattering kernel, if provided via the scatt_type element above, is represented and thus used during the scattering process. 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 to the tabular format should be performed or not. A value of “true” means the conversion should be performed, “false” means it should 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

The following attributes/sub-elements are the cross section values to be used during the transport process.

total:

This element requires the group-wise total cross section ordered by increasing group index (i.e., fast to thermal). If representation is “isotropic”, then the length of this list should equal the number of groups described in the groups element. If representation is “angle”, then the length of this list should equal the number of groups times the number of azimuthal angles times the number of polar angles, with the inner-dimension being groups, intermediate-dimension being azimuthal angles and outer-dimension being the polar angles.

Default: If not provided, it will be determined by summing the absorption and scattering cross sections.

absorption:

This element requires the group-wise absorption cross section ordered by increasing group index (i.e., fast to thermal). If representation is “isotropic”, then the length of this list should equal the number of groups described in the groups element. If representation is “angle”, then the length of this list should equal the number of groups times the number of azimuthal angles times the number of polar angles, with the inner-dimension being groups, intermediate-dimension being azimuthal angles and outer-dimension being the polar angles.

Default: None, this must be provided.

scatter:

This element requires the scattering moment matrices presented with the columns representing incoming group and rows representing the outgoing group. That is, down-scatter will be above the diagonal of the resultant matrix. This matrix is repeated for every Legendre order (in order of increasing orders) if scatt_type is “legendre”; otherwise, this matrix is repeated for every bin of the histogram or tabular representation. Finally, if representation is “angle”, the above is repeated for every azimuthal angle and every polar angle, in that order.

Default: None, this must be provided.

multiplicity:

This element provides the ratio of neutrons produced in scattering collisions to the neutrons which undergo scattering collisions; that is, the multiplicity provides the code with a scaling factor to account for neutrons being produced in (n,xn) reactions. This information is assumed isotropic and therefore does not need to be repeated for every Legendre moment or histogram/tabular bin. This matrix follows the same arrangement as described for the scatter element, with the exception of the data needed to provide the scattering type information.

Default: Multiplicities of 1.0 are assumed (i.e., (n,xn) reactions are neglected).

The following fission-specific data are only needed should fissionable be “true”.

fission:

This element requires the group-wise fission cross section ordered by increasing group index (i.e., fast to thermal). If representation is “isotropic”, then the length of this list should equal the number of groups described in the groups element. If representation is “angle”, then the length of this list should equal the number of groups times the number of azimuthal angles times the number of polar angles, with the inner-dimension being groups, intermediate-dimension being azimuthal angles and outer-dimension being the polar angles.

Default: None, this is required only if fission tallies are requested and the material is fissionable.

kappa_fission:

This element requires the group-wise kappa-fission cross section ordered by increasing group index (i.e., fast to thermal). If representation is “isotropic”, then the length of this list should equal the number of groups described in the groups element. If representation is “angle”, then the length of this list should equal the number of groups times the number of azimuthal angles times the number of polar angles, with the inner-dimension being groups, intermediate-dimension being azimuthal angles and outer-dimension being the polar angles.

Default: None, this is required only if kappa_fission tallies are requested and the material is fissionable.

chi:

This element requires the group-wise fission spectra ordered by increasing group index (i.e., fast to thermal). This element should be used if making the common approximation that the fission spectra does not depend on incoming energy. If the user does not wish to make this approximation, then this should not be provided and this information included in the nu_fission element instead. If representation is “isotropic”, then the length of this list should equal the number of groups described in the groups element. If representation is “angle”, then the length of this list should equal the number of groups times the number of azimuthal angles times the number of polar angles, with the inner-dimension being groups, intermediate-dimension being azimuthal angles and outer-dimension being the polar angles.

Default: None, either this element is provided or nu_fission is provided in fission matrix form, or the material is not fissionable.

nu_fission:

This element provides either the group-wise fission production cross section vector (i.e., if chi is provided), or is the group-wise fission production matrix. If providing the vector, it should be ordered the same as the fission data. If providing the matrix, it should be ordered the same as the multiplicity matrix.

Default: None, either this element must be provided if the material is fissionable.

State Point File Format

The current revision of the statepoint file format is 15.

/filetype (char[])

String indicating the type of file.

/revision (int)

Revision of the state point file format. Any time a change is made in the format, this integer is incremented.

/version_major (int)

Major version number for OpenMC

/version_minor (int)

Minor version number for OpenMC

/version_release (int)

Release version number for OpenMC

/date_and_time (char[])

Date and time the state point was written.

/path (char[])

Absolute path to directory containing input files.

/seed (int8_t)

Pseudo-random number generator seed.

/run_CE (int)

Flag to denote continuous-energy or multi-group mode. A value of 1 indicates a continuous-energy run while a value of 0 indicates a multi-group run.

/run_mode (char[])

Run mode used. A value of 1 indicates a fixed-source run and a value of 2 indicates an eigenvalue run.

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

if run_mode == ‘k-eigenvalue’:

/n_inactive (int)

Number of inactive batches.

/gen_per_batch (int)

Number of generations per batch.

/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

/cmfd_on (int)

Flag indicating whether CMFD is on (1) or off (0).

if (cmfd_on)

/cmfd/indices (int[4])

Indices for cmfd mesh (i,j,k,g)

/cmfd/k_cmfd (double[])

CMFD eigenvalues

/cmfd/cmfd_src (double[][][][])

CMFD fission source

/cmfd/cmfd_entropy (double[])

CMFD estimate of Shannon entropy

/cmfd/cmfd_balance (double[])

RMS of the residual neutron balance equation on CMFD mesh

/cmfd/cmfd_dominance (double[])

CMFD estimate of dominance ratio

/cmfd/cmfd_srccmp (double[])

RMS comparison of difference between OpenMC and CMFD fission source

/tallies/n_meshes (int)

Number of meshes in tallies.xml file

/tally/meshes/ids (int[])

Internal unique ID of each mesh.

/tally/meshes/keys (int[])

User-identified unique ID of each mesh.

/tallies/meshes/mesh <uid>/type (char[])

Type of mesh.

/tallies/meshes/mesh <uid>/dimension (int)

Number of mesh cells in each dimension.

/tallies/meshes/mesh <uid>/lower_left (double[])

Coordinates of lower-left corner of mesh.

/tallies/meshes/mesh <uid>/upper_right (double[])

Coordinates of upper-right corner of mesh.

/tallies/meshes/mesh <uid>/width (double[])

Width of each mesh cell in each dimension.

/tallies/n_tallies (int)

Number of user-defined tallies.

/tallies/ids (int[])

Internal unique ID of each tally.

/tallies/keys (int[])

User-identified unique ID of each tally.

/tallies/tally <uid>/estimator (char[])

Type of tally estimator, either ‘analog’, ‘tracklength’, or ‘collision’.

/tallies/tally <uid>/n_realizations (int)

Number of realizations.

/tallies/tally <uid>/n_filters (int)

Number of filters used.

/tallies/tally <uid>/filter <j>/type (char[])

Type of the j-th filter. Can be ‘universe’, ‘material’, ‘cell’, ‘cellborn’, ‘surface’, ‘mesh’, ‘energy’, ‘energyout’, or ‘distribcell’.

/tallies/tally <uid>/filter <j>/n_bins (int)

Number of bins for the j-th filter.

/tallies/tally <uid>/filter <j>/bins (int[] or double[])

Value for each filter bin of this type.

/tallies/tally <uid>/nuclides (char[][])

Array of nuclides to tally. Note that if no nuclide is specified in the user input, a single ‘total’ nuclide appears here.

/tallies/tally <uid>/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.

/tallies/tally <uid>/score_bins (char[][])

Values of specified scores.

/tallies/tally <uid>/n_user_scores (int)

Number of scores without accounting for those added by expansions, e.g. scatter-PN.

/tallies/tally <uid>/moment_orders (char[][])

Tallying moment orders for Legendre and spherical harmonic tally expansions (e.g., ‘P2’, ‘Y1,2’, etc.).

/tallies/tally <uid>/results (Compound type)

Accumulated sum and sum-of-squares for each bin of the i-th tally. This is a two-dimensional array, the first dimension of which represents combinations of filter bins and the second dimensions of which represents scoring bins. Each element of the array has fields ‘sum’ and ‘sum_sq’.

/source_present (int)

Flag indicated if source bank is present in the file

/n_realizations (int)

Number of realizations for global tallies.

/n_global_tallies (int)

Number of global tally scores.

/global_tallies (Compound type)

Accumulated sum and sum-of-squares for each global tally. The compound type has fields named sum and sum_sq.

/tallies_present (int)

Flag indicated if tallies are present in the file.

if (run_mode == ‘k-eigenvalue’ and source_present > 0)

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

/runtime/total initialization (double)

Time (in seconds on the master process) spent reading inputs, allocating arrays, etc.

/runtime/reading cross sections (double)

Time (in seconds on the master process) spent loading cross section libraries (this is a subset of initialization).

/runtime/simulation (double)

Time (in seconds on the master process) spent between initialization and finalization.

/runtime/transport (double)

Time (in seconds on the master process) spent transporting particles.

/runtime/inactive batches (double)

Time (in seconds on the master process) spent in the inactive batches (including non-transport activities like communcating sites).

/runtime/active batches (double)

Time (in seconds on the master process) spent in the active batches (including non-transport activities like communicating sites).

/runtime/synchronizing fission bank (double)

Time (in seconds on the master process) spent sampling source particles from fission sites and communicating them to other processes for load balancing.

/runtime/sampling source sites (double)

Time (in seconds on the master process) spent sampling source particles from fission sites.

/runtime/SEND-RECV source sites (double)

Time (in seconds on the master process) spent communicating source sites between processes for load balancing.

/runtime/accumulating tallies (double)

Time (in seconds on the master process) spent communicating tally results and evaluating their statistics.

/runtime/CMFD (double)

Time (in seconds on the master process) spent evaluating CMFD.

/runtime/CMFD building matrices (double)

Time (in seconds on the master process) spent buliding CMFD matrices.

/runtime/CMFD solving matrices (double)

Time (in seconds on the master process) spent solving CMFD matrices.

/runtime/total (double)

Total time spent (in seconds on the master process) 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 revision of the summary file format is 1.

/filetype (char[])

String indicating the type of file.

/revision (int)

Revision of the summary file format. Any time a change is made in the format, this integer is incremented.

/version_major (int)

Major version number for OpenMC

/version_minor (int)

Minor version number for OpenMC

/version_release (int)

Release version number for OpenMC

/date_and_time (char[])

Date and time the summary was written.

/n_procs (int)

Number of MPI processes used.

/n_particles (int8_t)

Number of particles used per generation.

/n_batches (int)

Number of batches to simulate.

/n_inactive (int)

Number of inactive batches. Only present if /run_mode is set to ‘k-eigenvalue’.

/n_active (int)

Number of active batches. Only present if /run_mode is set to ‘k-eigenvalue’.

/gen_per_batch (int)

Number of generations per batch. Only present if /run_mode is set to ‘k-eigenvalue’.

/geometry/n_cells (int)

Number of cells in the problem.

/geometry/n_surfaces (int)

Number of surfaces in the problem.

/geometry/n_universes (int)

Number of unique universes in the problem.

/geometry/n_lattices (int)

Number of lattices in the problem.

/geometry/cells/cell <uid>/index (int)

Index in cells array used internally in OpenMC.

/geometry/cells/cell <uid>/name (char[])

Name of the cell.

/geometry/cells/cell <uid>/universe (int)

Universe assigned to the cell. If none is specified, the default universe (0) is assigned.

/geometry/cells/cell <uid>/fill_type (char[])

Type of fill for the cell. Can be ‘normal’, ‘universe’, or ‘lattice’.

/geometry/cells/cell <uid>/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.

/geometry/cells/cell <uid>/temperature (double[])

Temperature of the cell in Kelvin.

/geometry/cells/cell <uid>/offset (int[])

Offsets used for distribcell tally filter. This dataset is present only if fill_type is set to ‘universe’.

/geometry/cells/cell <uid>/translation (double[3])

Translation applied to the fill universe. This dataset is present only if fill_type is set to ‘universe’.

/geometry/cells/cell <uid>/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’.

/geometry/cells/cell <uid>/lattice (int)

Unique ID of the lattice which fills the cell. Only present if fill_type is set to ‘lattice’.

/geometry/cells/cell <uid>/region (char[])

Region specification for the cell.

/geometry/cells/cell <uid>/distribcell_index (int)

Index of this cell in distribcell filter arrays.

/geometry/surfaces/surface <uid>/index (int)

Index in surfaces array used internally in OpenMC.

/geometry/surfaces/surface <uid>/name (char[])

Name of the surface.

/geometry/surfaces/surface <uid>/type (char[])

Type of the surface. Can be ‘x-plane’, ‘y-plane’, ‘z-plane’, ‘plane’, ‘x-cylinder’, ‘y-cylinder’, ‘sphere’, ‘x-cone’, ‘y-cone’, ‘z-cone’, or ‘quadric’.

/geometry/surfaces/surface <uid>/coefficients (double[])

Array of coefficients that define the surface. See <surface> Element for what coefficients are defined for each surface type.

/geometry/surfaces/surface <uid>/boundary_condition (char[])

Boundary condition applied to the surface. Can be ‘transmission’, ‘vacuum’, ‘reflective’, or ‘periodic’.

/geometry/universes/universe <uid>/index (int)

Index in the universes array used internally in OpenMC.

/geometry/universes/universe <uid>/cells (int[])

Array of unique IDs of cells that appear in the universe.

/geometry/lattices/lattice <uid>/index (int)

Index in the lattices array used internally in OpenMC.

/geometry/lattices/lattice <uid>/name (char[])

Name of the lattice.

/geometry/lattices/lattice <uid>/type (char[])

Type of the lattice, either ‘rectangular’ or ‘hexagonal’.

/geometry/lattices/lattice <uid>/pitch (double[])

Pitch of the lattice.

/geometry/lattices/lattice <uid>/outer (int)

Outer universe assigned to lattice cells outside the defined range.

/geometry/lattices/lattice <uid>/offsets (int[])

Offsets used for distribcell tally filter.

/geometry/lattices/lattice <uid>/universes (int[])

Three-dimensional array of universes assigned to each cell of the lattice.

/geometry/lattices/lattice <uid>/dimension (int[])

The number of lattice cells in each direction. This dataset is present only when the ‘type’ dataset is set to ‘rectangular’.

/geometry/lattices/lattice <uid>/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’.

/geometry/lattices/lattice <uid>/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’.

/geometry/lattices/lattice <uid>/n_axial (int)

Number of lattice positions along the z-axis. This dataset is present only when the ‘type’ dataset is set to ‘hexagonal’.

/geometry/lattices/lattice <uid>/center (double[])

Coordinates of the center of the lattice. This dataset is present only when the ‘type’ dataset is set to ‘hexagonal’.

/n_materials (int)

Number of materials in the problem.

/materials/material <uid>/index (int)

Index in materials array used internally in OpenMC.

/materials/material <uid>/name (char[])

Name of the material.

/materials/material <uid>/atom_density (double[])

Total atom density of the material in atom/b-cm.

/materials/material <uid>/nuclides (char[][])

Array of nuclides present in the material, e.g., ‘U-235.71c’.

/materials/material <uid>/nuclide_densities (double[])

Atom density of each nuclide.

/materials/material <uid>/sab_names (char[][])

Names of S(\(\alpha\),:math:beta) tables assigned to the material.

/tallies/n_tallies (int)

Number of tallies in the problem.

/tallies/n_meshes (int)

Number of meshes in the problem.

/tallies/mesh <uid>/index (int)

Index in the meshes array used internally in OpenMC.

/tallies/mesh <uid>/type (char[])

Type of the mesh. The only valid option is currently ‘regular’.

/tallies/mesh <uid>/dimension (int[])

Number of mesh cells in each direction.

/tallies/mesh <uid>/lower_left (double[])

Coordinates of the lower-left corner of the mesh.

/tallies/mesh <uid>/upper_right (double[])

Coordinates of the upper-right corner of the mesh.

/tallies/mesh <uid>/width (double[])

Width of a single mesh cell in each direction.

/tallies/tally <uid>/index (int)

Index in tallies array used internally in OpenMC.

/tallies/tally <uid>/name (char[])

Name of the tally.

/tallies/tally <uid>/n_filters (int)

Number of filters applied to the tally.

/tallies/tally <uid>/filter <j>/type (char[])

Type of the j-th filter. Can be ‘universe’, ‘material’, ‘cell’, ‘cellborn’, ‘surface’, ‘mesh’, ‘energy’, ‘energyout’, or ‘distribcell’.

/tallies/tally <uid>/filter <j>/offset (int)

Filter offset (used for distribcell filter).

/tallies/tally <uid>/filter <j>/paths (char[][])

The paths traversed through the CSG tree to reach each distribcell instance (for ‘distribcell’ filters only). This consists of the integer IDs for each universe, cell and lattice delimited by ‘->’. Each lattice cell is specified by its (x,y) or (x,y,z) indices.

/tallies/tally <uid>/filter <j>/n_bins (int)

Number of bins for the j-th filter.

/tallies/tally <uid>/filter <j>/bins (int[] or double[])

Value for each filter bin of this type.

/tallies/tally <uid>/nuclides (char[][])

Array of nuclides to tally. Note that if no nuclide is specified in the user input, a single ‘total’ nuclide appears here.

/tallies/tally <uid>/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.

/tallies/tally <uid>/moment_orders (char[][])

Tallying moment orders for Legendre and spherical harmonic tally expansions (e.g., ‘P2’, ‘Y1,2’, etc.).

/tallies/tally <uid>/score_bins (char[][])

Scoring bins for the tally.

Particle Restart File Format

The current revision of the particle restart file format is 1.

/filetype (char[])

String indicating the type of file.

/revision (int)

Revision of the particle restart file format. Any time a change is made in the format, this integer is incremented.

/current_batch (int)

The number of batches already simulated.

/gen_per_batch (int)

Number of generations per batch.

/current_gen (int)

The number of generations already simulated.

/n_particles (int8_t)

Number of particles used per generation.

/run_mode (int)

Run mode used. A value of 1 indicates a fixed-source run and a value of 2 indicates an eigenvalue run.

/id (int8_t)

Unique identifier of the particle.

/weight (double)

Weight of the particle.

/energy (double)

Energy of the particle in MeV 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 1.

/filetype (char[])

String indicating the type of file.

/revision (int)

Revision of the track file format. Any time a change is made in the format, this integer is incremented.

/n_particles (int)

Number of particles for which tracks are recorded.

/n_coords (int[])

Number of coordinates for each particle.

do i = 1, n_particles

/coordinates_i (double[][3])

(x,y,z) coordinates for the i-th particle.

Voxel Plot File Format

/filetype (char[])

String indicating the type of file.

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

/data (int[][][])

Data for each voxel that represents a material or cell ID.

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). http://dx.doi.org/10.1016/j.anucene.2014.07.048
  • 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). http://dx.doi.org/10.1016/j.anucene.2012.06.040

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). http://dx.doi.org/10.1016/j.pnucene.2014.12.018
  • 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

  • Matt Ellis, Benoit Forget, Kord Smith, and Derek Gaston, “Preliminary coupling of the Monte Carlo code OpenMC and the Multiphysics Object-Oriented Simulation Environment (MOOSE) for analyzing Doppler feedback in Monte Carlo simulations,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
  • Bryan R. Herman, Benoit Forget, and Kord Smith, “Progress toward Monte Carlo-thermal hydraulic coupling using low-order nonlinear diffusion acceleration methods.” Ann. Nucl. Energy, 84, 63-72 (2015). http://dx.doi.org/10.1016/j.anucene.2014.10.029
  • Bryan R. Herman, Benoit Forget, and Kord Smith, “Utilizing CMFD in OpenMC to Estimate Dominance Ratio and Adjoint,” Trans. Am. Nucl. Soc., 109, 1389-1392 (2013).

Geometry

  • 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

  • William Boyd, Sterling Harper, and Paul K. Romano, “Equipping OpenMC for the big data era,” Accepted, PHYSOR 2016, Sun Valley, Idaho, May 1-5, 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).
  • 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

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

Nuclear Data

  • Colin Josey, Pablo Ducru, Benoit Forget, and Kord Smith, “Windowed multipole for cross section Doppler broadening,” J. Comput. Phys., In Press (2016). http://dx.doi.org/10.1016/jcp.2015.08.013
  • Colin Josey, Benoit Forget, and Kord Smith, “Windowed multipole sensitivity to target accuracy of the optimization procedure,” J. Nucl. Sci. Technol., 52, 987-992 (2015). http://dx.doi.org/10.1080/00223131.2015.1035353
  • 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). http://dx.doi.org/10.1016/j.cpc.2015.05.025
  • 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).
  • Paul K. Romano and Timothy H. Trumbull, “Comparison of algorithms for Doppler broadening pointwise tabulated cross sections,” Ann. Nucl. Energy, 75, 358–364 (2015). http://dx.doi.org/10.1016/j.anucene.2014.08.046
  • Tuomas Viitanen, Jaakko Leppanen, and Benoit Forget, “Target motion sampling temperature treatment technique with track-length esimators in OpenMC – Preliminary results,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
  • 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). http://dx.doi.org/10.1016/j.anucene.2014.01.017
  • Benoit Forget, Sheng Xu, and Kord Smith, “Direct Doppler broadening in Monte Carlo simulations using the multipole representation,” Ann. Nucl. Energy, 64, 78–85 (2014). http://dx.doi.org/10.1016/j.anucene.2013.09.043

Parallelism

  • 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,” Accepted, 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).
  • 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).
  • Nicholas Horelik, Andrew Siegel, Benoit Forget, and Kord Smith, “Monte Carlo domain decomposition for robust nuclear reactor analysis,” Parallel Comput., 40, 646–660 (2014). http://dx.doi.org/10.1016/j.parco.2014.10.001
  • 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). http://dx.doi.org/10.1016/j.cpc.2013.10.008
  • 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). http://dx.doi.org/10.1051/snamc/201404301
  • 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). http://dx.doi.org/10.1051/snamc/201404207
  • 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). http://dx.doi.org/10.1051/snamc/201404208
  • 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). http://dx.doi.org/10.1177/1094342013492179
  • 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). http://dx.doi.org/10.1016/j.jcp.2013.06.011
  • 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). http://dx.doi.org/10.1016/j.jcp.2012.06.012
  • 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). http://hdl.handle.net/1721.1/73569

License Agreement

Copyright © 2011-2016 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

Active development of the OpenMC Monte Carlo code is currently led by: