The OpenMC Monte Carlo Code¶
OpenMC is a Monte Carlo particle transport simulation code focused on neutron criticality calculations. It is capable of simulating 3D models based on constructive solid geometry with second-order surfaces. OpenMC supports either continuous-energy or multi-group transport. The continuous-energy particle interaction data is based on a native HDF5 format that can be generated from ACE files used by the MCNP and Serpent Monte Carlo codes.
OpenMC was originally developed by members of the Computational Reactor Physics Group at the Massachusetts Institute of Technology starting in 2011. Various universities, laboratories, and other organizations now contribute to the development of OpenMC. For more information on OpenMC, feel free to send a message to the User’s Group mailing list.
Contents¶
Quick Install Guide¶
This quick install guide outlines the basic steps needed to install OpenMC on your computer. For more detailed instructions on configuring and installing OpenMC, see Installation and Configuration in the User’s Manual.
Installing on Linux/Mac with conda-forge¶
Conda is an open source package management system and environment management system for installing multiple versions of software packages and their dependencies and switching easily between them. If you have conda installed on your system, OpenMC can be installed via the conda-forge channel. First, add the conda-forge channel with:
conda config --add channels conda-forge
OpenMC can then be installed with:
conda install openmc
Installing on Ubuntu through PPA¶
For users with Ubuntu 15.04 or later, a binary package for OpenMC is available through a Personal Package Archive (PPA) and can be installed through the APT package manager. First, add the following PPA to the repository sources:
sudo apt-add-repository ppa:paulromano/staging
Next, resynchronize the package index files:
sudo apt-get update
Now OpenMC should be recognized within the repository and can be installed:
sudo apt-get install openmc
Binary packages from this PPA may exist for earlier versions of Ubuntu, but they are no longer supported.
Installing from Source on Ubuntu 15.04+¶
To build OpenMC from source, several prerequisites are needed. If you are using Ubuntu 15.04 or higher, all prerequisites can be installed directly from the package manager.
sudo apt-get install gfortran
sudo apt-get install cmake
sudo apt-get install libhdf5-dev
After the packages have been installed, follow the instructions below for building and installing OpenMC from source.
Note
Before Ubuntu 15.04, the HDF5 package included in the Ubuntu Package archive was not built with support for the Fortran 2003 HDF5 interface, which is needed by OpenMC. If you are using Ubuntu 14.10 or before you will need to build HDF5 from source.
Installing from Source on Linux or Mac OS X¶
All OpenMC source code is hosted on GitHub. If you have git, the gfortran compiler, CMake, and HDF5 installed, you can download and install OpenMC be entering the following commands in a terminal:
git clone https://github.com/mit-crpg/openmc.git
cd openmc
mkdir build && cd build
cmake ..
make
sudo make install
This will build an executable named openmc
and install it (by default in
/usr/local/bin). If you do not have administrator privileges, the cmake command
should specify an installation directory where you have write access, e.g.
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local ..
If you want to build a parallel version of OpenMC (using OpenMP or MPI), directions can be found in the detailed installation instructions.
Example Notebooks¶
The following series of Jupyter Notebooks provide examples for usage of OpenMC features via the Python API.
Basic Usage¶
Modeling a Pin-Cell¶
This notebook is intended to demonstrate the basic features of the Python API for constructing input files and running OpenMC. In it, we will show how to create a basic reflective pin-cell model that is equivalent to modeling an infinite array of fuel pins. If you have never used OpenMC, this can serve as a good starting point to learn the Python API. We highly recommend having a copy of the Python API reference documentation open in another browser tab that you can refer to.
%matplotlib inline
import openmc
Defining Materials¶
Materials in OpenMC are defined as a set of nuclides or elements with specified atom/weight fractions. There are two ways we can go about adding nuclides or elements to materials. The first way involves creating Nuclide
or Element
objects explicitly.
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
o16 = openmc.Nuclide('O16')
zr = openmc.Element('Zr')
h1 = openmc.Nuclide('H1')
Now that we have all the nuclides/elements that we need, we can start creating materials. In OpenMC, many objects are identified by a "unique ID" that is simply just a positive integer. These IDs are used when exporting XML files that the solver reads in. They also appear in the output and can be used for identification. Assigning an ID is required -- we can also give a name
as well.
uo2 = openmc.Material(1, "uo2")
print(uo2)
On the XML side, you have no choice but to supply an ID. However, in the Python API, if you don't give an ID, one will be automatically generated for you:
mat = openmc.Material()
print(mat)
We see that an ID of 10000 was automatically assigned. Let's now move on to adding nuclides to our uo2
material. The Material
object has a method add_nuclide()
whose first argument is the nuclide and second argument is the atom or weight fraction.
help(uo2.add_nuclide)
We see that by default it assumes we want an atom fraction.
# Add nuclides to uo2
uo2.add_nuclide(u235, 0.03)
uo2.add_nuclide(u238, 0.97)
uo2.add_nuclide(o16, 2.0)
Now we need to assign a total density to the material. We'll use the set_density
for this.
uo2.set_density('g/cm3', 10.0)
You may sometimes be given a material specification where all the nuclide densities are in units of atom/b-cm. In this case, you just want the density to be the sum of the constituents. In that case, you can simply run mat.set_density('sum')
.
With UO2 finished, let's now create materials for the clad and coolant. Note the use of add_element()
for zirconium.
zirconium = openmc.Material(2, "zirconium")
zirconium.add_element(zr, 1.0)
zirconium.set_density('g/cm3', 6.6)
water = openmc.Material(3, "h2o")
water.add_nuclide(h1, 2.0)
water.add_nuclide(o16, 1.0)
water.set_density('g/cm3', 1.0)
An astute observer might now point out that this water material we just created will only use free-atom cross sections. We need to tell it to use an $S(\alpha,\beta)$ table so that the bound atom cross section is used at thermal energies. To do this, there's an add_s_alpha_beta()
method. Note the use of the GND-style name "c_H_in_H2O".
water.add_s_alpha_beta('c_H_in_H2O')
So far you've seen the "hard" way to create a material. The "easy" way is to just pass strings to add_nuclide()
and add_element()
-- they are implicitly coverted to Nuclide
and Element
objects. For example, we could have created our UO2 material as follows:
uo2 = openmc.Material(1, "uo2")
uo2.add_nuclide('U235', 0.03)
uo2.add_nuclide('U238', 0.97)
uo2.add_nuclide('O16', 2.0)
uo2.set_density('g/cm3', 10.0)
When we go to run the transport solver in OpenMC, it is going to look for a materials.xml
file. Thus far, we have only created objects in memory. To actually create a materials.xml
file, we need to instantiate a Materials
collection and export it to XML.
mats = openmc.Materials([uo2, zirconium, water])
Note that Materials
is actually a subclass of Python's built-in list
, so we can use methods like append()
, insert()
, pop()
, etc.
mats = openmc.Materials()
mats.append(uo2)
mats += [zirconium, water]
isinstance(mats, list)
Finally, we can create the XML file with the export_to_xml()
method. In a Jupyter notebook, we can run a shell command by putting !
before it, so in this case we are going to display the materials.xml
file that we created.
mats.export_to_xml()
!cat materials.xml
Element Expansion¶
Did you notice something really cool that happened to our Zr element? OpenMC automatically turned it into a list of nuclides when it exported it! The way this feature works is as follows:
- First, it checks whether
Materials.cross_sections
has been set, indicating the path to across_sections.xml
file. - If
Materials.cross_sections
isn't set, it looks for theOPENMC_CROSS_SECTIONS
environment variable. - If either of these are found, it scans the file to see what nuclides are actually available and will expand elements accordingly.
Let's see what happens if we change O16 in water to elemental O.
water.remove_nuclide('O16')
water.add_element('O', 1.0)
mats.export_to_xml()
!cat materials.xml
We see that now O16 and O17 were automatically added. O18 is missing because our cross sections file (which is based on ENDF/B-VII.1) doesn't have O18. If OpenMC didn't know about the cross sections file, it would have assumed that all isotopes exist.
The cross_sections.xml
file¶
The cross_sections.xml
tells OpenMC where it can find nuclide cross sections and $S(\alpha,\beta)$ tables. It serves the same purpose as MCNP's xsdir
file and Serpent's xsdata
file. As we mentioned, this can be set either by the OPENMC_CROSS_SECTIONS
environment variable or the Materials.cross_sections
attribute.
Let's have a look at what's inside this file:
!cat $OPENMC_CROSS_SECTIONS | head -n 10
print(' ...')
!cat $OPENMC_CROSS_SECTIONS | tail -n 10
Enrichment¶
Note that the add_element()
method has a special argument enrichment
that can be used for Uranium. For example, if we know that we want to create 3% enriched UO2, the following would work:
uo2_three = openmc.Material()
uo2_three.add_element('U', 1.0, enrichment=3.0)
uo2_three.add_element('O', 2.0)
uo2_three.set_density('g/cc', 10.0)
Defining Geometry¶
At this point, we have three materials defined, exported to XML, and ready to be used in our model. To finish our model, we need to define the geometric arrangement of materials. OpenMC represents physical volumes using constructive solid geometry (CSG), also known as combinatorial geometry. The object that allows us to assign a material to a region of space is called a Cell
(same concept in MCNP, for those familiar). In order to define a region that we can assign to a cell, we must first define surfaces which bound the region. A surface is a locus of zeros of a function of Cartesian coordinates $x$, $y$, and $z$, e.g.
- A plane perpendicular to the x axis: $x - x_0 = 0$
- A cylinder perpendicular to the z axis: $(x - x_0)^2 + (y - y_0)^2 - R^2 = 0$
- A sphere: $(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0$
Between those three classes of surfaces (planes, cylinders, spheres), one can construct a wide variety of models. It is also possible to define cones and general second-order surfaces (torii are not currently supported).
Note that defining a surface is not sufficient to specify a volume -- in order to define an actual volume, one must reference the half-space of a surface. A surface half-space is the region whose points satisfy a positive of negative inequality of the surface equation. For example, for a sphere of radius one centered at the origin, the surface equation is $f(x,y,z) = x^2 + y^2 + z^2 - 1 = 0$. Thus, we say that the negative half-space of the sphere, is defined as the collection of points satisfying $f(x,y,z) < 0$, which one can reason is the inside of the sphere. Conversely, the positive half-space of the sphere would correspond to all points outside of the sphere.
Let's go ahead and create a sphere and confirm that what we've told you is true.
sph = openmc.Sphere(R=1.0)
Note that by default the sphere is centered at the origin so we didn't have to supply x0
, y0
, or z0
arguments. Strictly speaking, we could have omitted R
as well since it defaults to one. To get the negative or positive half-space, we simply need to apply the -
or +
unary operators, respectively.
(NOTE: Those unary operators are defined by special methods: __pos__
and __neg__
in this case).
inside_sphere = -sph
outside_sphere = +sph
Now let's see if inside_sphere
actually contains points inside the sphere:
print((0,0,0) in inside_sphere, (0,0,2) in inside_sphere)
print((0,0,0) in outside_sphere, (0,0,2) in outside_sphere)
Everything works as expected! Now that we understand how to create half-spaces, we can create more complex volumes by combining half-spaces using Boolean operators: &
(intersection), |
(union), and ~
(complement). For example, let's say we want to define a region that is the top part of the sphere (all points inside the sphere that have $z > 0$.
z_plane = openmc.ZPlane(z0=0)
northern_hemisphere = -sph & +z_plane
For many regions, OpenMC can automatically determine a bounding box. To get the bounding box, we use the bounding_box
property of a region, which returns a tuple of the lower-left and upper-right Cartesian coordinates for the bounding box:
northern_hemisphere.bounding_box
Now that we see how to create volumes, we can use them to create a cell.
cell = openmc.Cell()
cell.region = northern_hemisphere
# or...
cell = openmc.Cell(region=northern_hemisphere)
By default, the cell is not filled by any material (void). In order to assign a material, we set the fill
property of a Cell
.
cell.fill = water
Universes and in-line plotting¶
A collection of cells is known as a universe (again, this will be familiar to MCNP/Serpent users) and can be used as a repeatable unit when creating a model. Although we don't need it yet, the benefit of creating a universe is that we can visualize our geometry while we're creating it.
universe = openmc.Universe()
universe.add_cell(cell)
# this also works
universe = openmc.Universe(cells=[cell])
The Universe
object has a plot
method that will display our the universe as current constructed:
universe.plot(width=(2.0, 2.0))
By default, the plot will appear in the $x$-$y$ plane. We can change that with the basis
argument.
universe.plot(width=(2.0, 2.0), basis='xz')
If we have particular fondness for, say, fuchsia, we can tell the plot()
method to make our cell that color.
universe.plot(width=(2.0, 2.0), basis='xz',
colors={cell: 'fuchsia'})
Pin cell geometry¶
We now have enough knowledge to create our pin-cell. We need three surfaces to define the fuel and clad:
- The outer surface of the fuel -- a cylinder perpendicular to the z axis
- The inner surface of the clad -- same as above
- The outer surface of the clad -- same as above
These three surfaces will all be instances of openmc.ZCylinder
, each with a different radius according to the specification.
fuel_or = openmc.ZCylinder(R=0.39)
clad_ir = openmc.ZCylinder(R=0.40)
clad_or = openmc.ZCylinder(R=0.46)
With the surfaces created, we can now take advantage of the built-in operators on surfaces to create regions for the fuel, the gap, and the clad:
fuel_region = -fuel_or
gap_region = +fuel_or & -clad_ir
clad_region = +clad_ir & -clad_or
Now we can create corresponding cells that assign materials to these regions. As with materials, cells have unique IDs that are assigned either manually or automatically. Note that the gap cell doesn't have any material assigned (it is void by default).
fuel = openmc.Cell(1, 'fuel')
fuel.fill = uo2
fuel.region = fuel_region
gap = openmc.Cell(2, 'air gap')
gap.region = gap_region
clad = openmc.Cell(3, 'clad')
clad.fill = zirconium
clad.region = clad_region
Finally, we need to handle the coolant outside of our fuel pin. To do this, we create x- and y-planes that bound the geometry.
pitch = 1.26
left = openmc.XPlane(x0=-pitch/2, boundary_type='reflective')
right = openmc.XPlane(x0=pitch/2, boundary_type='reflective')
bottom = openmc.YPlane(y0=-pitch/2, boundary_type='reflective')
top = openmc.YPlane(y0=pitch/2, boundary_type='reflective')
The water region is going to be everything outside of the clad outer radius and within the box formed as the intersection of four half-spaces.
water_region = +left & -right & +bottom & -top & +clad_or
moderator = openmc.Cell(4, 'moderator')
moderator.fill = water
moderator.region = water_region
OpenMC also includes a factory function that generates a rectangular prism that could have made our lives easier.
box = openmc.get_rectangular_prism(width=pitch, height=pitch,
boundary_type='reflective')
type(box)
Pay attention here -- the object that was returned is NOT a surface. It is actually the intersection of four surface half-spaces, just like we created manually before. Thus, we don't need to apply the unary operator (-box
). Instead, we can directly combine it with +clad_or
.
water_region = box & +clad_or
The final step is to assign the cells we created to a universe and tell OpenMC that this universe is the "root" universe in our geometry. The Geometry
is the final object that is actually exported to XML.
root = openmc.Universe(cells=(fuel, gap, clad, moderator))
geom = openmc.Geometry()
geom.root_universe = root
# or...
geom = openmc.Geometry(root)
geom.export_to_xml()
!cat geometry.xml
Starting source and settings¶
The Python API has a module openmc.stats
with various univariate and multivariate probability distributions. We can use these distributions to create a starting source using the openmc.Source
object.
point = openmc.stats.Point((0, 0, 0))
src = openmc.Source(space=point)
Now let's create a Settings
object and give it the source we created along with specifying how many batches and particles we want to run.
settings = openmc.Settings()
settings.source = src
settings.batches = 100
settings.inactive = 10
settings.particles = 1000
settings.export_to_xml()
!cat settings.xml
User-defined tallies¶
We actually have all the required files needed to run a simulation. Before we do that though, let's give a quick example of how to create tallies. We will show how one would tally the total, fission, absorption, and (n,$\gamma$) reaction rates for $^{235}$U in the cell containing fuel. Recall that filters allow us to specify where in phase-space we want events to be tallied and scores tell us what we want to tally:
$$X = \underbrace{\int d\mathbf{r} \int d\mathbf{\Omega} \int dE}_{\text{filters}} \; \underbrace{f(\mathbf{r},\mathbf{\Omega},E)}_{\text{scores}} \psi (\mathbf{r},\mathbf{\Omega},E)$$In this case, the where is "the fuel cell". So, we will create a cell filter specifying the fuel cell.
cell_filter = openmc.CellFilter(fuel)
t = openmc.Tally(1)
t.filters = [cell_filter]
The what is the total, fission, absorption, and (n,$\gamma$) reaction rates in $^{235}$U. By default, if we only specify what reactions, it will gives us tallies over all nuclides. We can use the nuclides
attribute to name specific nuclides we're interested in.
t.nuclides = ['U235']
t.scores = ['total', 'fission', 'absorption', '(n,gamma)']
Similar to the other files, we need to create a Tallies
collection and export it to XML.
tallies = openmc.Tallies([t])
tallies.export_to_xml()
!cat tallies.xml
Running OpenMC¶
Running OpenMC from Python can be done using the openmc.run()
function. This function allows you to set the number of MPI processes and OpenMP threads, if need be.
openmc.run()
Great! OpenMC already told us our k-effective. It also spit out a file called tallies.out
that shows our tallies. This is a very basic method to look at tally data; for more sophisticated methods, see other example notebooks.
!cat tallies.out
Geometry plotting¶
We saw before that we could call the Universe.plot()
method to show a universe while we were creating our geometry. There is also a built-in plotter in the Fortran codebase that is much faster than the Python plotter and has more options. The interface looks somewhat similar to the Universe.plot()
method. Instead though, we create Plot
instances, assign them to a Plots
collection, export it to XML, and then run OpenMC in geometry plotting mode. As an example, let's specify that we want the plot to be colored by material (rather than by cell) and we assign yellow to fuel and blue to water.
p = openmc.Plot()
p.filename = 'pinplot'
p.width = (pitch, pitch)
p.pixels = (200, 200)
p.color_by = 'material'
p.colors = {uo2: 'yellow', water: 'blue'}
With our plot created, we need to add it to a Plots
collection which can be exported to XML.
plots = openmc.Plots([p])
plots.export_to_xml()
!cat plots.xml
Now we can run OpenMC in plotting mode by calling the plot_geometry()
function. Under the hood this is calling openmc --plot
.
openmc.plot_geometry()
OpenMC writes out a peculiar image with a .ppm
extension. If you have ImageMagick installed, this can be converted into a more normal .png
file.
!convert pinplot.ppm pinplot.png
We can use functionality from IPython to display the image inline in our notebook:
from IPython.display import Image
Image("pinplot.png")
That was a little bit cumbersome. Thankfully, OpenMC provides us with a function that does all that "boilerplate" work.
openmc.plot_inline(p)
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.
%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.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pin.
# 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.
# Instantiate a Materials collection
materials_file = openmc.Materials((fuel, water, zircaloy))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces -- in this case two cylinders and six reflective planes.
# 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.
# 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.
# 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.
# 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 90 active batches each with 5000 particles.
# 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.
# 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.
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png
# Display the materials plot inline
Image(filename='materials-xy.png')
As we can see from the plot, we have a nice pin cell with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a 2D mesh tally.
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
# Create mesh which will be used for tally
mesh = openmc.Mesh()
mesh.dimension = [100, 100]
mesh.lower_left = [-0.63, -0.63]
mesh.upper_right = [0.63, 0.63]
# Create mesh filter for tally
mesh_filter = openmc.MeshFilter(mesh)
# Create mesh tally to score flux and fission rate
tally = openmc.Tally(name='flux')
tally.filters = [mesh_filter]
tally.scores = ['flux', 'fission']
tallies_file.append(tally)
# 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.
# Run OpenMC!
openmc.run()
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.
# 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.
tally = sp.get_tally(scores=['flux'])
print(tally)
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:
tally.sum
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.
print(tally.mean.shape)
(tally.mean, tally.std_dev)
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.
flux = tally.get_slice(scores=['flux'])
fission = tally.get_slice(scores=['fission'])
print(flux)
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.
flux.std_dev.shape = (100, 100)
flux.mean.shape = (100, 100)
fission.std_dev.shape = (100, 100)
fission.mean.shape = (100, 100)
fig = plt.subplot(121)
fig.imshow(flux.mean)
fig2 = plt.subplot(122)
fig2.imshow(fission.mean)
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.
# 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.
sp.source
If we want, say, only the energies from the source sites, we can simply index the source array with the name of the field:
sp.source['E']
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.
# Create log-spaced energy bins from 1 keV to 100 MeV
energy_bins = np.logspace(3,7)
# Calculate pdf for source energies
probability, bin_edges = np.histogram(sp.source['E'], energy_bins, density=True)
# Make sure integrating the PDF gives us unity
print(sum(probability*np.diff(energy_bins)))
# Plot source energy PDF
plt.semilogx(energy_bins[:-1], probability*np.diff(energy_bins), linestyle='steps')
plt.xlabel('Energy (eV)')
plt.ylabel('Probability/eV')
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.
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))
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.
import glob
from IPython.display import Image
import matplotlib.pyplot as plt
import scipy.stats
import numpy as np
import pandas as pd
import openmc
%matplotlib inline
Generate Input Files¶
First we need to define materials that will be used in the problem. We will create three materials for the fuel, water, and cladding of the fuel pin.
# 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.
# Instantiate a Materials collection
materials_file = openmc.Materials((fuel, water, zircaloy))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. This problem will be a square array of fuel pins for which we can use OpenMC's lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.
# 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.
# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel', fill=fuel,
region=-fuel_outer_radius)
# Create a clad Cell
clad_cell = openmc.Cell(name='1.6% Clad', fill=zircaloy)
clad_cell.region = +fuel_outer_radius & -clad_outer_radius
# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator', fill=water,
region=+clad_outer_radius)
# Create a Universe to encapsulate a fuel pin
pin_cell_universe = openmc.Universe(name='1.6% Fuel Pin', cells=[
fuel_cell, clad_cell, moderator_cell
])
Using the pin cell universe, we can construct a 17x17 rectangular lattice with a 1.26 cm pitch.
# 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.
# Create root Cell
root_cell = openmc.Cell(name='root cell', fill=assembly)
# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z
# Create root Universe
root_universe = openmc.Universe(name='root universe')
root_universe.add_cell(root_cell)
We now must create a geometry that is assigned a root universe and export it to XML.
# Create Geometry and export to "geometry.xml"
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()
With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 5 inactive batches and 15 minimum active batches each with 2500 particles. We also tell OpenMC to turn tally triggers on, which means it will keep running until some criterion on the uncertainty of tallies is reached.
# OpenMC simulation parameters
min_batches = 20
max_batches = 200
inactive = 5
particles = 2500
# Instantiate a Settings object
settings = openmc.Settings()
settings.batches = min_batches
settings.inactive = inactive
settings.particles = particles
settings.output = {'tallies': False}
settings.trigger_active = True
settings.trigger_max_batches = max_batches
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings.export_to_xml()
Let us also create a plot file that we can use to verify that our pin cell geometry was created successfully.
# 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.
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png
# Display the materials plot inline
Image(filename='materials-xy.png')
As we can see from the plot, we have a nice array of pin cells with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a variety of tallies.
# Instantiate an empty Tallies object
tallies = openmc.Tallies()
Instantiate a fission rate mesh Tally
# Instantiate a tally Mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.width = [1.26, 1.26]
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate energy Filter
energy_filter = openmc.EnergyFilter([0, 0.625, 20.0e6])
# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['fission', 'nu-fission']
# Add mesh and Tally to Tallies
tallies.append(tally)
Instantiate a cell Tally with nuclides
# Instantiate tally Filter
cell_filter = openmc.CellFilter(fuel_cell)
# Instantiate the tally
tally = openmc.Tally(name='cell tally')
tally.filters = [cell_filter]
tally.scores = ['scatter-y2']
tally.nuclides = ['U235', 'U238']
# Add mesh and tally to Tallies
tallies.append(tally)
Create a "distribcell" Tally. The distribcell filter allows us to tally multiple repeated instances of the same cell throughout the geometry.
# Instantiate tally Filter
distribcell_filter = openmc.DistribcellFilter(moderator_cell)
# Instantiate tally Trigger for kicks
trigger = openmc.Trigger(trigger_type='std_dev', threshold=5e-5)
trigger.scores = ['absorption']
# Instantiate the Tally
tally = openmc.Tally(name='distribcell tally')
tally.filters = [distribcell_filter]
tally.scores = ['absorption', 'scatter']
tally.triggers = [trigger]
# Add mesh and tally to Tallies
tallies.append(tally)
# Export to "tallies.xml"
tallies.export_to_xml()
Now we a have a complete set of inputs, so we can go ahead and run our simulation.
# Remove old HDF5 (summary, statepoint) files
!rm statepoint.*
# Run OpenMC!
openmc.run()
Tally Data Processing¶
# 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
# 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)
Use the new Tally data retrieval API with pure NumPy
# Get the relative error for the thermal fission reaction
# rates in the four corner pins
data = tally.get_values(scores=['fission'],
filters=[openmc.MeshFilter, openmc.EnergyFilter], \
filter_bins=[((1,1),(1,17), (17,1), (17,17)), \
((0., 0.625),)], value='rel_err')
print(data)
# Get a pandas dataframe for the mesh tally data
df = tally.get_pandas_dataframe(nuclides=False)
# Set the Pandas float display settings
pd.options.display.float_format = '{:.2e}'.format
# Print the first twenty rows in the dataframe
df.head(20)
# Create a boxplot to view the distribution of
# fission and nu-fission rates in the pins
bp = df.boxplot(column='mean', by='score')
# Extract thermal nu-fission rates from pandas
fiss = df[df['score'] == 'nu-fission']
fiss = fiss[fiss['energy low [eV]'] == 0.0]
# Extract mean and reshape as 2D NumPy arrays
mean = fiss['mean'].values.reshape((17,17))
plt.imshow(mean, interpolation='nearest')
plt.title('fission rate')
plt.xlabel('x')
plt.ylabel('y')
plt.colorbar()
Analyze the cell+nuclides scatter-y2 rate tally
# 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)
# 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)
Use the new Tally data retrieval API with pure NumPy
# Get the standard deviations for two of the spherical harmonic
# scattering reaction rates
data = tally.get_values(scores=['scatter-Y2,2', 'scatter-Y0,0'],
nuclides=['U238', 'U235'], value='std_dev')
print(data)
Analyze the distribcell tally
# 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)
Use the new Tally data retrieval API with pure NumPy
# Get the relative error for the scattering reaction rates in
# the first 10 distribcell instances
data = tally.get_values(scores=['scatter'], filters=[openmc.DistribcellFilter],
filter_bins=[(i,) for i in range(10)], value='rel_err')
print(data)
Print the distribcell tally dataframe
# 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)
# 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
Perform a statistical test comparing the tally sample distributions for two categories of fuel pins.
# 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))
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.
# 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))
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.
# 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')
# 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'])
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.
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.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pin.
# 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.
# Instantiate a Materials collection
materials_file = openmc.Materials((fuel, water, zircaloy))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces -- in this case two cylinders and six reflective planes.
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.45720)
# Create boundary planes to surround the geometry
# Use both reflective and vacuum boundaries to make life interesting
min_x = openmc.XPlane(x0=-0.63, boundary_type='reflective')
max_x = openmc.XPlane(x0=+0.63, boundary_type='reflective')
min_y = openmc.YPlane(y0=-0.63, boundary_type='reflective')
max_y = openmc.YPlane(y0=+0.63, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-100., boundary_type='vacuum')
max_z = openmc.ZPlane(z0=+100., boundary_type='vacuum')
With the surfaces defined, we can now create cells that are defined by intersections of half-spaces created by the surfaces.
# 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.
# 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.
# 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 5 inactive batches and 15 active batches each with 2500 particles.
# OpenMC simulation parameters
batches = 20
inactive = 5
particles = 2500
# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -100., 0.63, 0.63, 100.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings_file.export_to_xml()
Let us also create a plot file that we can use to verify that our pin cell geometry was created successfully.
# 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.
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png
# Display the materials plot inline
Image(filename='materials-xy.png')
As we can see from the plot, we have a nice pin cell with fuel, cladding, and water! Before we run our simulation, we need to tell the code what we want to tally. The following code shows how to create a variety of tallies.
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
# Create Tallies to compute microscopic multi-group cross-sections
# Instantiate energy filter for multi-group cross-section Tallies
energy_filter = openmc.EnergyFilter([0., 0.625, 20.0e6])
# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='flux')
tally.filters = [openmc.CellFilter([fuel_cell, moderator_cell])]
tally.filters.append(energy_filter)
tally.scores = ['flux']
tallies_file.append(tally)
# Instantiate reaction rate Tally in fuel
tally = openmc.Tally(name='fuel rxn rates')
tally.filters = [openmc.CellFilter(fuel_cell)]
tally.filters.append(energy_filter)
tally.scores = ['nu-fission', 'scatter']
tally.nuclides = [u238, u235]
tallies_file.append(tally)
# Instantiate reaction rate Tally in moderator
tally = openmc.Tally(name='moderator rxn rates')
tally.filters = [openmc.CellFilter(moderator_cell)]
tally.filters.append(energy_filter)
tally.scores = ['absorption', 'total']
tally.nuclides = [o16, h1]
tallies_file.append(tally)
# Instantiate a tally mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [1, 1, 1]
mesh.lower_left = [-0.63, -0.63, -100.]
mesh.width = [1.26, 1.26, 200.]
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate thermal, fast, and total leakage tallies
leak = openmc.Tally(name='leakage')
leak.filters = [mesh_filter]
leak.scores = ['current']
tallies_file.append(leak)
thermal_leak = openmc.Tally(name='thermal leakage')
thermal_leak.filters = [mesh_filter, openmc.EnergyFilter([0., 0.625])]
thermal_leak.scores = ['current']
tallies_file.append(thermal_leak)
fast_leak = openmc.Tally(name='fast leakage')
fast_leak.filters = [mesh_filter, openmc.EnergyFilter([0.625, 20.0e6])]
fast_leak.scores = ['current']
tallies_file.append(fast_leak)
# 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)
# Resonance Escape Probability tallies
therm_abs_rate = openmc.Tally(name='therm. abs. rate')
therm_abs_rate.scores = ['absorption']
therm_abs_rate.filters = [openmc.EnergyFilter([0., 0.625])]
tallies_file.append(therm_abs_rate)
# Thermal Flux Utilization tallies
fuel_therm_abs_rate = openmc.Tally(name='fuel therm. abs. rate')
fuel_therm_abs_rate.scores = ['absorption']
fuel_therm_abs_rate.filters = [openmc.EnergyFilter([0., 0.625]),
openmc.CellFilter([fuel_cell])]
tallies_file.append(fuel_therm_abs_rate)
# Fast Fission Factor tallies
therm_fiss_rate = openmc.Tally(name='therm. fiss. rate')
therm_fiss_rate.scores = ['nu-fission']
therm_fiss_rate.filters = [openmc.EnergyFilter([0., 0.625])]
tallies_file.append(therm_fiss_rate)
# Instantiate energy filter to illustrate Tally slicing
fine_energy_filter = openmc.EnergyFilter(np.logspace(np.log10(1e-2), np.log10(20.0e6), 10))
# Instantiate flux Tally in moderator and fuel
tally = openmc.Tally(name='need-to-slice')
tally.filters = [openmc.CellFilter([fuel_cell, moderator_cell])]
tally.filters.append(fine_energy_filter)
tally.scores = ['nu-fission', 'scatter']
tally.nuclides = [h1, u238]
tallies_file.append(tally)
# 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.
# Remove old HDF5 (summary, statepoint) files
!rm statepoint.*
# Run OpenMC!
openmc.run()
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.
# Load the statepoint file
sp = openmc.StatePoint('statepoint.20.h5')
We have a tally of the total fission rate and the total absorption rate, so we can calculate k-eff as: $$k_{eff} = \frac{\langle \nu \Sigma_f \phi \rangle}{\langle \Sigma_a \phi \rangle + \langle L \rangle}$$ In this notation, $\langle \cdot \rangle^a_b$ represents an OpenMC that is integrated over region $a$ and energy range $b$. If $a$ or $b$ is not reported, it means the value represents an integral over all space or all energy, respectively.
# Get the fission and absorption rate tallies
fiss_rate = sp.get_tally(name='fiss. rate')
abs_rate = sp.get_tally(name='abs. rate')
# Get the leakage tally
leak = sp.get_tally(name='leakage')
leak = leak.summation(filter_type=openmc.SurfaceFilter, remove_filter=True)
leak = leak.summation(filter_type=openmc.MeshFilter, remove_filter=True)
# Compute k-infinity using tally arithmetic
keff = fiss_rate / (abs_rate + leak)
keff.get_pandas_dataframe()
Notice that even though the neutron production rate, absorption rate, and current are separate tallies, we still get a first-order estimate of the uncertainty on the quotient of them automatically!
Often in textbooks you'll see k-eff represented using the six-factor formula $$k_{eff} = p \epsilon f \eta P_{FNL} P_{TNL}.$$ Let's analyze each of these factors, starting with the resonance escape probability which is defined as $$p=\frac{\langle\Sigma_a\phi\rangle_T + \langle L \rangle_T}{\langle\Sigma_a\phi\rangle + \langle L \rangle_T}$$ where the subscript $T$ means thermal energies.
# Compute resonance escape probability using tally arithmetic
therm_abs_rate = sp.get_tally(name='therm. abs. rate')
thermal_leak = sp.get_tally(name='thermal leakage')
thermal_leak = thermal_leak.summation(filter_type=openmc.SurfaceFilter, remove_filter=True)
thermal_leak = thermal_leak.summation(filter_type=openmc.MeshFilter, remove_filter=True)
res_esc = (therm_abs_rate + thermal_leak) / (abs_rate + thermal_leak)
res_esc.get_pandas_dataframe()
The fast fission factor can be calculated as $$\epsilon=\frac{\langle\nu\Sigma_f\phi\rangle}{\langle\nu\Sigma_f\phi\rangle_T}$$
# 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()
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.
# 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()
The next factor is the number of fission neutrons produced per absorption in fuel, calculated as $$\eta = \frac{\langle \nu\Sigma_f\phi \rangle_T}{\langle \Sigma_a \phi \rangle^F_T}$$
# Compute neutrons produced per absorption (eta) using tally arithmetic
eta = therm_fiss_rate / fuel_therm_abs_rate
eta.get_pandas_dataframe()
There are two leakage factors to account for fast and thermal leakage. The fast non-leakage probability is computed as $$P_{FNL} = \frac{\langle \Sigma_a\phi \rangle + \langle L \rangle_T}{\langle \Sigma_a \phi \rangle + \langle L \rangle}$$
p_fnl = (abs_rate + thermal_leak) / (abs_rate + leak)
p_fnl.get_pandas_dataframe()
The final factor is the thermal non-leakage probability and is computed as $$P_{TNL} = \frac{\langle \Sigma_a\phi \rangle_T}{\langle \Sigma_a \phi \rangle_T + \langle L \rangle_T}$$
p_tnl = therm_abs_rate / (therm_abs_rate + thermal_leak)
p_tnl.get_pandas_dataframe()
Now we can calculate $k_{eff}$ using the product of the factors form the four-factor formula.
keff = res_esc * fast_fiss * therm_util * eta * p_fnl * p_tnl
keff.get_pandas_dataframe()
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.
# Compute microscopic multi-group cross-sections
flux = sp.get_tally(name='flux')
flux = flux.get_slice(filters=[openmc.CellFilter], filter_bins=[(fuel_cell.id,)])
fuel_rxn_rates = sp.get_tally(name='fuel rxn rates')
mod_rxn_rates = sp.get_tally(name='moderator rxn rates')
fuel_xs = fuel_rxn_rates / flux
fuel_xs.get_pandas_dataframe()
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.
# 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)
The same idea can be used not only for scores but also for filters and nuclides.
# Show how to use Tally.get_values(...) with a CrossScore and CrossNuclide
u235_scatter_xs = fuel_xs.get_values(nuclides=['(U235 / total)'],
scores=['(scatter / flux)'])
print(u235_scatter_xs)
# Show how to use Tally.get_values(...) with a CrossFilter and CrossScore
fast_scatter_xs = fuel_xs.get_values(filters=[openmc.EnergyFilter],
filter_bins=[((0.625, 20.0e6),)],
scores=['(scatter / flux)'])
print(fast_scatter_xs)
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.
# "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()
# "Slice" the H-1 scatter data in the moderator Cell into a new derived Tally
need_to_slice = sp.get_tally(name='need-to-slice')
slice_test = need_to_slice.get_slice(scores=['scatter'], nuclides=['H1'],
filters=[openmc.CellFilter], filter_bins=[(moderator_cell.id,)])
slice_test.get_pandas_dataframe()
Criticality Search¶
This Notebook illustrates the usage of the OpenMC Python API's generic eigenvalue search capability. In this Notebook, we will do a critical boron concentration search of a typical PWR pin cell.
To use the search functionality, we must create a function which creates our model according to the input parameter we wish to search for (in this case, the boron concentration).
This notebook will first create that function, and then, run the search.
# Initialize third-party libraries and the OpenMC Python API
import matplotlib.pyplot as plt
import numpy as np
import openmc
import openmc.model
%matplotlib inline
Create Parametrized Model¶
To perform the search we will use the openmc.search_for_keff
function. This function requires a different function be defined which creates an parametrized model to analyze. This model is required to be stored in an openmc.model.Model
object. The first parameter of this function will be modified during the search process for our critical eigenvalue.
Our model will be a pin-cell from the Multi-Group Mode Part II assembly, except this time the entire model building process will be contained within a function, and the Boron concentration will be parametrized.
# Create the model. `ppm_Boron` will be the parametric variable.
def build_model(ppm_Boron):
# Create the pin materials
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_element('U', 1., enrichment=1.6)
fuel.add_element('O', 2.)
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_element('Zr', 1.)
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.741)
water.add_element('H', 2.)
water.add_element('O', 1.)
# Include the amount of boron in the water based on the ppm,
# neglecting the other constituents of boric acid
water.add_element('B', ppm_Boron * 1E-6)
# Instantiate a Materials object
materials = openmc.Materials((fuel, zircaloy, water))
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(R=0.39218)
clad_outer_radius = openmc.ZCylinder(R=0.45720)
# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-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')
# Create fuel Cell
fuel_cell = openmc.Cell(name='1.6% Fuel')
fuel_cell.fill = fuel
fuel_cell.region = -fuel_outer_radius
# 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
# Create a moderator Cell
moderator_cell = openmc.Cell(name='1.6% Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius & (+min_x & -max_x & +min_y & -max_y)
# Create root Universe
root_universe = openmc.Universe(name='root universe', universe_id=0)
root_universe.add_cells([fuel_cell, clad_cell, moderator_cell])
# Create Geometry and set root universe
geometry = openmc.Geometry(root_universe)
# Finish with the settings file
settings = openmc.Settings()
settings.batches = 300
settings.inactive = 20
settings.particles = 1000
settings.run_mode = 'eigenvalue'
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -10, 0.63, 0.63, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.source.Source(space=uniform_dist)
# We dont need a tallies file so dont waste the disk input/output time
settings.output = {'tallies': False}
model = openmc.model.Model(geometry, materials, settings)
return model
Search for the Critical Boron Concentration¶
To perform the search we imply call the openmc.search_for_keff
function and pass in the relvant arguments. For our purposes we will be passing in the model building function (build_model
defined above), a bracketed range for the expected critical Boron concentration (1,000 to 2,500 ppm), the tolerance, and the method we wish to use.
Instead of the bracketed range we could have used a single initial guess, but have elected not to in this example. Finally, due to the high noise inherent in using as few histories as are used in this example, our tolerance on the final keff value will be rather large (1.e-2) and a bisection method will be used for the search.
# Perform the search
crit_ppm, guesses, keffs = openmc.search_for_keff(build_model, bracket=[1000., 2500.],
tol=1.E-2, bracketed_method='bisect',
print_iterations=True)
print('Critical Boron Concentration: {:4.0f} ppm'.format(crit_ppm))
Finally, the openmc.search_for_keff
function also provided us with List
s of the guesses and corresponding keff values generated during the search process with OpenMC. Let's use that information to make a quick plot of the value of keff versus the boron concentration.
plt.figure(figsize=(8, 4.5))
plt.title('Eigenvalue versus Boron Concentration')
# Create a scatter plot using the mean value of keff
plt.scatter(guesses, [keffs[i][0] for i in range(len(keffs))])
plt.xlabel('Boron Concentration [ppm]')
plt.ylabel('Eigenvalue')
plt.show()
We see a nearly linear reactivity coefficient for the boron concentration, exactly as one would expect for a pure 1/v absorber at small concentrations.
Modeling TRISO Particles¶
OpenMC includes a few convenience functions for generationing TRISO particle locations and placing them in a lattice. To be clear, this capability is not a stochastic geometry capability like that included in MCNP. It's also important to note that OpenMC does not use delta tracking, which would normally speed up calculations in geometries with tons of surfaces and cells. However, the computational burden can be eased by placing TRISO particles in a lattice.
%matplotlib inline
from math import pi
import numpy as np
import matplotlib.pyplot as plt
import openmc
import openmc.model
Let's first start by creating materials that will be used in our TRISO particles and the background material.
fuel = openmc.Material(name='Fuel')
fuel.set_density('g/cm3', 10.5)
fuel.add_nuclide('U235', 4.6716e-02)
fuel.add_nuclide('U238', 2.8697e-01)
fuel.add_nuclide('O16', 5.0000e-01)
fuel.add_element('C', 1.6667e-01)
buff = openmc.Material(name='Buffer')
buff.set_density('g/cm3', 1.0)
buff.add_element('C', 1.0)
buff.add_s_alpha_beta('c_Graphite')
PyC1 = openmc.Material(name='PyC1')
PyC1.set_density('g/cm3', 1.9)
PyC1.add_element('C', 1.0)
PyC1.add_s_alpha_beta('c_Graphite')
PyC2 = openmc.Material(name='PyC2')
PyC2.set_density('g/cm3', 1.87)
PyC2.add_element('C', 1.0)
PyC2.add_s_alpha_beta('c_Graphite')
SiC = openmc.Material(name='SiC')
SiC.set_density('g/cm3', 3.2)
SiC.add_element('C', 0.5)
SiC.add_element('Si', 0.5)
graphite = openmc.Material()
graphite.set_density('g/cm3', 1.1995)
graphite.add_element('C', 1.0)
graphite.add_s_alpha_beta('c_Graphite')
To actually create individual TRISO particles, we first need to create a universe that will be used within each particle. The reason we use the same universe for each TRISO particle is to reduce the total number of cells/surfaces needed which can substantially improve performance over using unique cells/surfaces in each.
# Create TRISO universe
spheres = [openmc.Sphere(R=r*1e-4)
for r in [215., 315., 350., 385.]]
cells = [openmc.Cell(fill=fuel, region=-spheres[0]),
openmc.Cell(fill=buff, region=+spheres[0] & -spheres[1]),
openmc.Cell(fill=PyC1, region=+spheres[1] & -spheres[2]),
openmc.Cell(fill=SiC, region=+spheres[2] & -spheres[3]),
openmc.Cell(fill=PyC2, region=+spheres[3])]
triso_univ = openmc.Universe(cells=cells)
Now that we have a universe that can be used for each TRISO particle, we need to randomly select locations. In this example, we will select locations at random within a 1 cm x 1 cm x 1 cm box centered at the origin with a packing fraction of 30%. Note that pack_trisos
can handle up to the theoretical maximum of 60% (it will just be slow).
outer_radius = 425.*1e-4
trisos = openmc.model.pack_trisos(
radius=outer_radius,
fill=triso_univ,
domain_shape='cube',
domain_length=1,
packing_fraction=0.3
)
Each TRISO object actually is a Cell, in fact; we can look at the properties of the TRISO just as we would a cell:
print(trisos[0])
Let's confirm that all our TRISO particles are within the box.
centers = np.vstack([t.center for t in trisos])
print(centers.min(axis=0))
print(centers.max(axis=0))
We can also look at what the actual packing fraction turned out to be:
len(trisos)*4/3*pi*outer_radius**3
Now that we have our TRISO particles created, we need to place them in a lattice to provide optimal tracking performance in OpenMC. We'll start by creating a box that the lattice will be placed within.
min_x = openmc.XPlane(x0=-0.5, boundary_type='reflective')
max_x = openmc.XPlane(x0=0.5, boundary_type='reflective')
min_y = openmc.YPlane(y0=-0.5, boundary_type='reflective')
max_y = openmc.YPlane(y0=0.5, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-0.5, boundary_type='reflective')
max_z = openmc.ZPlane(z0=0.5, boundary_type='reflective')
box = openmc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z)
Our last step is to actually create a lattice containing TRISO particles which can be done with model.create_triso_lattice()
function. This function requires that we give it a list of TRISO particles, the lower-left coordinates of the lattice, the pitch of each lattice cell, the overall shape of the lattice (number of cells in each direction), and a background material.
lower_left, upper_right = box.region.bounding_box
shape = (3, 3, 3)
pitch = (upper_right - lower_left)/shape
lattice = openmc.model.create_triso_lattice(
trisos, lower_left, pitch, shape, graphite)
Now we can set the fill of our box cell to be the lattice:
box.fill = lattice
Finally, let's take a look at our geometry by putting the box in a universe and plotting it. We're going to use the Fortran-side plotter since it's much faster.
univ = openmc.Universe(cells=[box])
geom = openmc.Geometry(univ)
geom.export_to_xml()
mats = list(geom.get_all_materials().values())
openmc.Materials(mats).export_to_xml()
settings = openmc.Settings()
settings.run_mode = 'plot'
settings.export_to_xml()
p = openmc.Plot.from_geometry(geom)
openmc.plot_inline(p)
If we plot the universe by material rather than by cell, we can see that the entire background is just graphite.
p.color_by = 'material'
p.colors = {graphite: 'gray'}
openmc.plot_inline(p)
Modeling a CANDU Bundle¶
In this example, we will create a typical CANDU bundle with rings of fuel pins. At present, OpenMC does not have a specialized lattice for this type of fuel arrangement, so we must resort to manual creation of the array of fuel pins.
%matplotlib inline
from math import pi, sin, cos
import numpy as np
import openmc
Let's begin by creating the materials that will be used in our model.
fuel = openmc.Material(name='fuel')
fuel.add_element('U', 1.0)
fuel.add_element('O', 2.0)
fuel.set_density('g/cm3', 10.0)
clad = openmc.Material(name='zircaloy')
clad.add_element('Zr', 1.0)
clad.set_density('g/cm3', 6.0)
heavy_water = openmc.Material(name='heavy water')
heavy_water.add_nuclide('H2', 2.0)
heavy_water.add_nuclide('O16', 1.0)
heavy_water.add_s_alpha_beta('c_D_in_D2O')
heavy_water.set_density('g/cm3', 1.1)
With out materials created, we'll now define key dimensions in our model. These dimensions are taken from the example in section 11.1.3 of the Serpent manual.
# Outer radius of fuel and clad
r_fuel = 0.6122
r_clad = 0.6540
# Pressure tube and calendria radii
pressure_tube_ir = 5.16890
pressure_tube_or = 5.60320
calendria_ir = 6.44780
calendria_or = 6.58750
# Radius to center of each ring of fuel pins
ring_radii = np.array([0.0, 1.4885, 2.8755, 4.3305])
To begin creating the bundle, we'll first create annular regions completely filled with heavy water and add in the fuel pins later. The radii that we've specified above correspond to the center of each ring. We actually need to create cylindrical surfaces at radii that are half-way between the centers.
# These are the surfaces that will divide each of the rings
radial_surf = [openmc.ZCylinder(R=r) for r in
(ring_radii[:-1] + ring_radii[1:])/2]
water_cells = []
for i in range(ring_radii.size):
# Create annular region
if i == 0:
water_region = -radial_surf[i]
elif i == ring_radii.size - 1:
water_region = +radial_surf[i-1]
else:
water_region = +radial_surf[i-1] & -radial_surf[i]
water_cells.append(openmc.Cell(fill=heavy_water, region=water_region))
Let's see what our geometry looks like so far. In order to plot the geometry, we create a universe that contains the annular water cells and then use the Universe.plot()
method. While we're at it, we'll set some keyword arguments that can be reused for later plots.
plot_args = {'width': (2*calendria_or, 2*calendria_or)}
bundle_universe = openmc.Universe(cells=water_cells)
bundle_universe.plot(**plot_args)
Now we need to create a universe that contains a fuel pin. Note that we don't actually need to put water outside of the cladding in this universe because it will be truncated by a higher universe.
surf_fuel = openmc.ZCylinder(R=r_fuel)
fuel_cell = openmc.Cell(fill=fuel, region=-surf_fuel)
clad_cell = openmc.Cell(fill=clad, region=+surf_fuel)
pin_universe = openmc.Universe(cells=(fuel_cell, clad_cell))
pin_universe.plot(**plot_args)
The code below works through each ring to create a cell containing the fuel pin universe. As each fuel pin is created, we modify the region of the water cell to include everything outside the fuel pin.
num_pins = [1, 6, 12, 18]
angles = [0, 0, 15, 0]
for i, (r, n, a) in enumerate(zip(ring_radii, num_pins, angles)):
for j in range(n):
# Determine location of center of pin
theta = (a + j/n*360.) * pi/180.
x = r*cos(theta)
y = r*sin(theta)
pin_boundary = openmc.ZCylinder(x0=x, y0=y, R=r_clad)
water_cells[i].region &= +pin_boundary
# Create each fuel pin -- note that we explicitly assign an ID so
# that we can identify the pin later when looking at tallies
pin = openmc.Cell(fill=pin_universe, region=-pin_boundary)
pin.translation = (x, y, 0)
pin.id = (i + 1)*100 + j
bundle_universe.add_cell(pin)
bundle_universe.plot(**plot_args)
Looking pretty good! Finally, we create cells for the pressure tube and calendria and then put our bundle in the middle of the pressure tube.
pt_inner = openmc.ZCylinder(R=pressure_tube_ir)
pt_outer = openmc.ZCylinder(R=pressure_tube_or)
calendria_inner = openmc.ZCylinder(R=calendria_ir)
calendria_outer = openmc.ZCylinder(R=calendria_or, boundary_type='vacuum')
bundle = openmc.Cell(fill=bundle_universe, region=-pt_inner)
pressure_tube = openmc.Cell(fill=clad, region=+pt_inner & -pt_outer)
v1 = openmc.Cell(region=+pt_outer & -calendria_inner)
calendria = openmc.Cell(fill=clad, region=+calendria_inner & -calendria_outer)
root_universe = openmc.Universe(cells=[bundle, pressure_tube, v1, calendria])
Let's look at the final product. We'll export our geometry and materials and then use plot_inline()
to get a nice-looking plot.
geom = openmc.Geometry(root_universe)
geom.export_to_xml()
mats = openmc.Materials(geom.get_all_materials().values())
mats.export_to_xml()
p = openmc.Plot.from_geometry(geom)
p.color_by = 'material'
p.colors = {
fuel: 'black',
clad: 'silver',
heavy_water: 'blue'
}
openmc.plot_inline(p)
Interpreting Results¶
One of the difficulties of a geometry like this is identifying tally results when there was no lattice involved. To address this, we specifically gave an ID to each fuel pin of the form 100*ring + azimuthal position. Consequently, we can use a distribcell tally and then look at our DataFrame
which will show these cell IDs.
settings = openmc.Settings()
settings.particles = 1000
settings.batches = 20
settings.inactive = 10
settings.source = openmc.Source(space=openmc.stats.Point())
settings.export_to_xml()
fuel_tally = openmc.Tally()
fuel_tally.filters = [openmc.DistribcellFilter(fuel_cell)]
fuel_tally.scores = ['flux']
tallies = openmc.Tallies([fuel_tally])
tallies.export_to_xml()
openmc.run(output=False)
The return code of 0
indicates that OpenMC ran successfully. Now let's load the statepoint into a openmc.StatePoint
object and use the Tally.get_pandas_dataframe(...)
method to see our results.
sp = openmc.StatePoint('statepoint.{}.h5'.format(settings.batches))
t = sp.get_tally()
t.get_pandas_dataframe()
We can see that in the 'level 2' column, the 'cell id' tells us how each row corresponds to a ring and azimuthal position.
Nuclear Data¶
In this notebook, we will go through the salient features of the openmc.data
package in the Python API. This package enables inspection, analysis, and conversion of nuclear data from ACE files. Most importantly, the package provides a mean to generate HDF5 nuclear data libraries that are used by the transport solver.
%matplotlib inline
import os
from pprint import pprint
import shutil
import subprocess
import urllib.request
import h5py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm
from matplotlib.patches import Rectangle
import openmc.data
Physical Data¶
Some very helpful physical data is available as part of openmc.data
: atomic masses, natural abundances, and atomic weights.
openmc.data.atomic_mass('Fe54')
openmc.data.NATURAL_ABUNDANCE['H2']
openmc.data.atomic_weight('C')
The IncidentNeutron class¶
The most useful class within the openmc.data
API is IncidentNeutron
, which stores to continuous-energy incident neutron data. This class has factory methods from_ace
, from_endf
, and from_hdf5
which take a data file on disk and parse it into a hierarchy of classes in memory. To demonstrate this feature, we will download an ACE file (which can be produced with NJOY 2016) and then load it in using the IncidentNeutron.from_ace
method.
url = 'https://anl.box.com/shared/static/kxm7s57z3xgfbeq29h54n7q6js8rd11c.ace'
filename, headers = urllib.request.urlretrieve(url, 'gd157.ace')
# Load ACE data into object
gd157 = openmc.data.IncidentNeutron.from_ace('gd157.ace')
gd157
Cross sections¶
From Python, it's easy to explore (and modify) the nuclear data. Let's start off by reading the total cross section. Reactions are indexed using their "MT" number -- a unique identifier for each reaction defined by the ENDF-6 format. The MT number for the total cross section is 1.
total = gd157[1]
total
Cross sections for each reaction can be stored at multiple temperatures. To see what temperatures are available, we can look at the reaction's xs
attribute.
total.xs
To find the cross section at a particular energy, 1 eV for example, simply get the cross section at the appropriate temperature and then call it as a function. Note that our nuclear data uses eV as the unit of energy.
total.xs['294K'](1.0)
The xs
attribute can also be called on an array of energies.
total.xs['294K']([1.0, 2.0, 3.0])
A quick way to plot cross sections is to use the energy
attribute of IncidentNeutron
. This gives an array of all the energy values used in cross section interpolation for each temperature present.
gd157.energy
energies = gd157.energy['294K']
total_xs = total.xs['294K'](energies)
plt.loglog(energies, total_xs)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
Reaction Data¶
Most of the interesting data for an IncidentNeutron
instance is contained within the reactions
attribute, which is a dictionary mapping MT values to Reaction
objects.
pprint(list(gd157.reactions.values())[:10])
Let's suppose we want to look more closely at the (n,2n) reaction. This reaction has an energy threshold
n2n = gd157[16]
print('Threshold = {} eV'.format(n2n.xs['294K'].x[0]))
The (n,2n) cross section, like all basic cross sections, is represented by the Tabulated1D
class. The energy and cross section values in the table can be directly accessed with the x
and y
attributes. Using the x
and y
has the nice benefit of automatically acounting for reaction thresholds.
n2n.xs
xs = n2n.xs['294K']
plt.plot(xs.x, xs.y)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
plt.xlim((xs.x[0], xs.x[-1]))
To get information on the energy and angle distribution of the neutrons emitted in the reaction, we need to look at the products
attribute.
n2n.products
neutron = n2n.products[0]
neutron.distribution
We see that the neutrons emitted have a correlated angle-energy distribution. Let's look at the energy_out
attribute to see what the outgoing energy distributions are.
dist = neutron.distribution[0]
dist.energy_out
Here we see we have a tabulated outgoing energy distribution for each incoming energy. Note that the same probability distribution classes that we could use to create a source definition are also used within the openmc.data
package. Let's plot every fifth distribution to get an idea of what they look like.
for e_in, e_out_dist in zip(dist.energy[::5], dist.energy_out[::5]):
plt.semilogy(e_out_dist.x, e_out_dist.p, label='E={:.2f} MeV'.format(e_in/1e6))
plt.ylim(ymax=1e-6)
plt.legend()
plt.xlabel('Outgoing energy (eV)')
plt.ylabel('Probability/eV')
plt.show()
There is also summed_reactions
attribute for cross sections (like total) which are built from summing up other cross sections.
pprint(list(gd157.summed_reactions.values()))
Note that the cross sections for these reactions are represented by the Sum
class rather than Tabulated1D
. They do not support the x
and y
attributes.
gd157[27].xs
Unresolved resonance probability tables¶
We can also look at unresolved resonance probability tables which are stored in a ProbabilityTables
object. In the following example, we'll create a plot showing what the total cross section probability tables look like as a function of incoming energy.
fig = plt.figure()
ax = fig.add_subplot(111)
cm = matplotlib.cm.Spectral_r
# Determine size of probability tables
urr = gd157.urr['294K']
n_energy = urr.table.shape[0]
n_band = urr.table.shape[2]
for i in range(n_energy):
# Get bounds on energy
if i > 0:
e_left = urr.energy[i] - 0.5*(urr.energy[i] - urr.energy[i-1])
else:
e_left = urr.energy[i] - 0.5*(urr.energy[i+1] - urr.energy[i])
if i < n_energy - 1:
e_right = urr.energy[i] + 0.5*(urr.energy[i+1] - urr.energy[i])
else:
e_right = urr.energy[i] + 0.5*(urr.energy[i] - urr.energy[i-1])
for j in range(n_band):
# Determine maximum probability for a single band
max_prob = np.diff(urr.table[i,0,:]).max()
# Determine bottom of band
if j > 0:
xs_bottom = urr.table[i,1,j] - 0.5*(urr.table[i,1,j] - urr.table[i,1,j-1])
value = (urr.table[i,0,j] - urr.table[i,0,j-1])/max_prob
else:
xs_bottom = urr.table[i,1,j] - 0.5*(urr.table[i,1,j+1] - urr.table[i,1,j])
value = urr.table[i,0,j]/max_prob
# Determine top of band
if j < n_band - 1:
xs_top = urr.table[i,1,j] + 0.5*(urr.table[i,1,j+1] - urr.table[i,1,j])
else:
xs_top = urr.table[i,1,j] + 0.5*(urr.table[i,1,j] - urr.table[i,1,j-1])
# Draw rectangle with appropriate color
ax.add_patch(Rectangle((e_left, xs_bottom), e_right - e_left, xs_top - xs_bottom,
color=cm(value)))
# Overlay total cross section
ax.plot(gd157.energy['294K'], total.xs['294K'](gd157.energy['294K']), 'k')
# Make plot pretty and labeled
ax.set_xlim(1.0, 1.0e5)
ax.set_ylim(1e-1, 1e4)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('Energy (eV)')
ax.set_ylabel('Cross section(b)')
Exporting HDF5 data¶
If you have an instance IncidentNeutron
that was created from ACE or HDF5 data, you can easily write it to disk using the export_to_hdf5()
method. This can be used to convert ACE to HDF5 or to take an existing data set and actually modify cross sections.
gd157.export_to_hdf5('gd157.h5', 'w')
With few exceptions, the HDF5 file encodes the same data as the ACE file.
gd157_reconstructed = openmc.data.IncidentNeutron.from_hdf5('gd157.h5')
np.all(gd157[16].xs['294K'].y == gd157_reconstructed[16].xs['294K'].y)
And one of the best parts of using HDF5 is that it is a widely used format with lots of third-party support. You can use h5py
, for example, to inspect the data.
h5file = h5py.File('gd157.h5', 'r')
main_group = h5file['Gd157/reactions']
for name, obj in sorted(list(main_group.items()))[:10]:
if 'reaction_' in name:
print('{}, {}'.format(name, obj.attrs['label'].decode()))
n2n_group = main_group['reaction_016']
pprint(list(n2n_group.values()))
So we see that the hierarchy of data within the HDF5 mirrors the hierarchy of Python objects that we manipulated before.
n2n_group['294K/xs'].value
Working with ENDF files¶
In addition to being able to load ACE and HDF5 data, we can also load ENDF data directly into an IncidentNeutron
instance using the from_endf()
factory method. Let's download the ENDF/B-VII.1 evaluation for $^{157}$Gd and load it in:
# Download ENDF file
url = 'https://t2.lanl.gov/nis/data/data/ENDFB-VII.1-neutron/Gd/157'
filename, headers = urllib.request.urlretrieve(url, 'gd157.endf')
# Load into memory
gd157_endf = openmc.data.IncidentNeutron.from_endf(filename)
gd157_endf
Just as before, we can get a reaction by indexing the object directly:
elastic = gd157_endf[2]
However, if we look at the cross section now, we see that it isn't represented as tabulated data anymore.
elastic.xs
If had Cython installed when you built/installed OpenMC, you should be able to evaluate resonant cross sections from ENDF data directly, i.e., OpenMC will reconstruct resonances behind the scenes for you.
elastic.xs['0K'](0.0253)
When data is loaded from an ENDF file, there is also a special resonances
attribute that contains resolved and unresolved resonance region data (from MF=2 in an ENDF file).
gd157_endf.resonances.ranges
We see that $^{157}$Gd has a resolved resonance region represented in the Reich-Moore format as well as an unresolved resonance region. We can look at the min/max energy of each region by doing the following:
[(r.energy_min, r.energy_max) for r in gd157_endf.resonances.ranges]
With knowledge of the energy bounds, let's create an array of energies over the entire resolved resonance range and plot the elastic scattering cross section.
# Create log-spaced array of energies
resolved = gd157_endf.resonances.resolved
energies = np.logspace(np.log10(resolved.energy_min),
np.log10(resolved.energy_max), 1000)
# Evaluate elastic scattering xs at energies
xs = elastic.xs['0K'](energies)
# Plot cross section vs energies
plt.loglog(energies, xs)
plt.xlabel('Energy (eV)')
plt.ylabel('Cross section (b)')
Resonance ranges also have a useful parameters
attribute that shows the energies and widths for resonances.
resolved.parameters.head(10)
Heavy-nuclide resonance scattering¶
OpenMC has two methods for accounting for resonance upscattering in heavy nuclides, DBRC and ARES. These methods rely on 0 K elastic scattering data being present. If you have an existing ACE/HDF5 dataset and you need to add 0 K elastic scattering data to it, this can be done using the IncidentNeutron.add_elastic_0K_from_endf()
method. Let's do this with our original gd157
object that we instantiated from an ACE file.
gd157.add_elastic_0K_from_endf('gd157.endf')
Let's check to make sure that we have both the room temperature elastic scattering cross section as well as a 0K cross section.
gd157[2].xs
Generating data from NJOY¶
To run OpenMC in continuous-energy mode, you generally need to have ACE files already available that can be converted to OpenMC's native HDF5 format. If you don't already have suitable ACE files or need to generate new data, both the IncidentNeutron
and ThermalScattering
classes include from_njoy()
methods that will run NJOY to generate ACE files and then read those files to create OpenMC class instances. The from_njoy()
methods take as input the name of an ENDF file on disk. By default, it is assumed that you have an executable named njoy
available on your path. This can be configured with the optional njoy_exec
argument. Additionally, if you want to show the progress of NJOY as it is running, you can pass stdout=True
.
Let's use IncidentNeutron.from_njoy()
to run NJOY to create data for $^2$H using an ENDF file. We'll specify that we want data specifically at 300, 400, and 500 K.
# Download ENDF file
url = 'https://t2.lanl.gov/nis/data/data/ENDFB-VII.1-neutron/H/2'
filename, headers = urllib.request.urlretrieve(url, 'h2.endf')
# Run NJOY to create deuterium data
h2 = openmc.data.IncidentNeutron.from_njoy('h2.endf', temperatures=[300., 400., 500.], stdout=True)
Now we can use our h2
object just as we did before.
h2[2].xs
Note that 0 K elastic scattering data is automatically added when using from_njoy()
so that resonance elastic scattering treatments can be used.
Multi-Group Cross Section Generation¶
MGXS Part I: Introduction¶
This IPython Notebook introduces the use of the openmc.mgxs
module to calculate multi-group cross sections for an infinite homogeneous medium. In particular, this Notebook introduces the the following features:
- General equations for scalar-flux averaged multi-group cross sections
- Creation of multi-group cross sections for an infinite homogeneous medium
- Use of tally arithmetic to manipulate multi-group cross sections
Introduction to Multi-Group Cross Sections (MGXS)¶
Many Monte Carlo particle transport codes, including OpenMC, use continuous-energy nuclear cross section data. However, most deterministic neutron transport codes use multi-group cross sections defined over discretized energy bins or energy groups. An example of U-235's continuous-energy fission cross section along with a 16-group cross section computed for a light water reactor spectrum is displayed below.
from IPython.display import Image
Image(filename='images/mgxs.png', width=350)
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:
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¶
%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.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create a material for the homogeneous medium.
# 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.
# Instantiate a Materials collection and export to XML
materials_file = openmc.Materials([inf_medium])
materials_file.export_to_xml()
Now let's move on to the geometry. This problem will be a simple square cell with reflective boundary conditions to simulate an infinite homogeneous medium. The first step is to create the outer bounding surfaces of the problem.
# 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.
# 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.
# 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.
# 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.
# 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.
# Instantiate a 2-group EnergyGroups object
groups = mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])
We can now use the EnergyGroups
object, along with our previously created materials and geometry, to instantiate some MGXS
objects from the openmc.mgxs
module. In particular, the following are subclasses of the generic and abstract MGXS
class:
TotalXS
TransportXS
AbsorptionXS
CaptureXS
FissionXS
KappaFissionXS
ScatterXS
ScatterMatrixXS
Chi
ChiPrompt
InverseVelocity
PromptNuFissionXS
Of course, we are aware that the fission cross section (FissionXS
) can sometimes be paired with the fission neutron multiplication to become $\nu\sigma_f$. This can be accomodated in to the FissionXS
class by setting the nu
parameter to True
as shown below.
Additionally, scattering reactions (like (n,2n)) can also be defined to take in to account the neutron multiplication to become $\nu\sigma_s$. This can be accomodated in the the transport (TransportXS
), scattering (ScatterXS
), and scattering-matrix (ScatterMatrixXS
) cross sections types by setting the nu
parameter to True
as shown below.
These classes provide us with an interface to generate the tally inputs as well as perform post-processing of OpenMC's tally data to compute the respective multi-group cross sections. In this case, let's create the multi-group total, absorption and scattering cross sections with our 2-group structure.
# Instantiate a few different sections
total = mgxs.TotalXS(domain=cell, groups=groups)
absorption = mgxs.AbsorptionXS(domain=cell, groups=groups)
scattering = mgxs.ScatterXS(domain=cell, groups=groups)
# Note that if we wanted to incorporate neutron multiplication in the
# scattering cross section we would write the previous line as:
# scattering = mgxs.ScatterXS(domain=cell, groups=groups, nu=True)
Each multi-group cross section object stores its tallies in a Python dictionary called tallies
. We can inspect the tallies in the dictionary for our Absorption
object as follows.
absorption.tallies
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.
# 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.
# Run OpenMC
openmc.run()
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint
object.
# 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.
# 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.
total.print_xs()
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.
df = scattering.get_pandas_dataframe()
df.head(10)
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.
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.
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.
# 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()
Similarly, we can use tally arithmetic to compute the ratio of AbsorptionXS
and ScatterXS
to the TotalXS
.
# 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()
# 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()
Lastly, we sum the derived scatter-to-total and absorption-to-total ratios to confirm that they sum to unity.
# 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()
MGXS Part II: Advanced Features¶
This IPython Notebook illustrates the use of the openmc.mgxs
module to calculate multi-group cross sections for a heterogeneous fuel pin cell geometry. In particular, this Notebook illustrates the following features:
- Creation of multi-group cross sections on a heterogeneous geometry
- Calculation of cross sections on a nuclide-by-nuclide basis
- The use of tally precision triggers with multi-group cross sections
- Built-in features for energy condensation in downstream data processing
- The use of the
openmc.data
module to plot continuous-energy vs. multi-group cross sections - Validation of multi-group cross sections with OpenMOC
Note: This Notebook was created using OpenMOC to verify the multi-group cross-sections generated by OpenMC. You must install OpenMOC on your system in order to run this Notebook in its entirety. In addition, this Notebook illustrates the use of Pandas DataFrames
to containerize multi-group cross section data.
Generate Input Files¶
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-dark')
import openmoc
import openmc
import openmc.mgxs as mgxs
import openmc.data
from openmc.openmoc_compatible import get_openmoc_geometry
%matplotlib inline
First we need to define materials that will be used in the problem. Before defining a material, we must create nuclides that are used in the material.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create three distinct materials for water, clad and fuel.
# 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.
# Instantiate a Materials collection
materials_file = openmc.Materials((fuel, water, zircaloy))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. Our problem will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces -- in this case two cylinders and six reflective planes.
# 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.
# 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.
# 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.
# 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.
# 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.
# Instantiate a "coarse" 2-group EnergyGroups object
coarse_groups = mgxs.EnergyGroups()
coarse_groups.group_edges = np.array([0., 0.625, 20.0e6])
# Instantiate a "fine" 8-group EnergyGroups object
fine_groups = mgxs.EnergyGroups()
fine_groups.group_edges = np.array([0., 0.058, 0.14, 0.28,
0.625, 4.0, 5.53e3, 821.0e3, 20.0e6])
Now we will instantiate a variety of MGXS
objects needed to run an OpenMOC simulation to verify the accuracy of our cross sections. In particular, we define transport, fission, nu-fission, nu-scatter and chi cross sections for each of the three cells in the fuel pin with the 8-group structure as our energy groups.
# Extract all Cells filled by Materials
openmc_cells = openmc_geometry.get_all_material_cells().values()
# Create dictionary to store multi-group cross sections for all cells
xs_library = {}
# Instantiate 8-group cross sections for each cell
for cell in openmc_cells:
xs_library[cell.id] = {}
xs_library[cell.id]['transport'] = mgxs.TransportXS(groups=fine_groups)
xs_library[cell.id]['fission'] = mgxs.FissionXS(groups=fine_groups)
xs_library[cell.id]['nu-fission'] = mgxs.FissionXS(groups=fine_groups, nu=True)
xs_library[cell.id]['nu-scatter'] = mgxs.ScatterMatrixXS(groups=fine_groups, nu=True)
xs_library[cell.id]['chi'] = mgxs.Chi(groups=fine_groups)
Next, we showcase the use of OpenMC's tally precision trigger feature in conjunction with the openmc.mgxs
module. In particular, we will assign a tally trigger of 1E-2 on the standard deviation for each of the tallies used to compute multi-group cross sections.
# 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.
# 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.
# Run OpenMC
openmc.run()
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint
object.
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.082.h5')
The statepoint is now ready to be analyzed by our multi-group cross sections. We simply have to load the tallies from the StatePoint
into each object as follows and our MGXS
objects will compute the cross sections for us under-the-hood.
# 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.
nufission = xs_library[fuel_cell.id]['nu-fission']
nufission.print_xs(xs_type='micro', nuclides=['U235', 'U238'])
Our multi-group cross sections are capable of summing across all nuclides to provide us with macroscopic cross sections as well.
nufission = xs_library[fuel_cell.id]['nu-fission']
nufission.print_xs(xs_type='macro', nuclides='sum')
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
.
nuscatter = xs_library[moderator_cell.id]['nu-scatter']
df = nuscatter.get_pandas_dataframe(xs_type='micro')
df.head(10)
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.
# 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.
condensed_xs.print_xs()
df = condensed_xs.get_pandas_dataframe(xs_type='micro')
df
Verification with OpenMOC¶
Now, let's verify our cross sections using OpenMOC. First, we construct an equivalent OpenMOC geometry.
# Create an OpenMOC Geometry from the OpenMC Geometry
openmoc_geometry = get_openmoc_geometry(sp.summary.geometry)
Next, we we can inject the multi-group cross sections into the equivalent fuel pin cell OpenMOC geometry.
# 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.
# 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()
We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.
# 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))
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.
openmoc_geometry = get_openmoc_geometry(sp.summary.geometry)
openmoc_cells = openmoc_geometry.getRootUniverse().getAllCells()
# Inject multi-group cross sections into OpenMOC Materials
for cell_id, cell in openmoc_cells.items():
# Ignore the root cell
if cell.getName() == 'root cell':
continue
openmoc_material = cell.getFillMaterial()
openmoc_material.setNumEnergyGroups(coarse_groups.num_groups)
# Extract the appropriate cross section objects for this cell
transport = xs_library[cell_id]['transport']
nufission = xs_library[cell_id]['nu-fission']
nuscatter = xs_library[cell_id]['nu-scatter']
chi = xs_library[cell_id]['chi']
# Perform group condensation
transport = transport.get_condensed_xs(coarse_groups)
nufission = nufission.get_condensed_xs(coarse_groups)
nuscatter = nuscatter.get_condensed_xs(coarse_groups)
chi = chi.get_condensed_xs(coarse_groups)
# Inject NumPy arrays of cross section data into the Material
openmoc_material.setSigmaT(transport.get_xs(nuclides='sum').flatten())
openmoc_material.setNuSigmaF(nufission.get_xs(nuclides='sum').flatten())
openmoc_material.setSigmaS(nuscatter.get_xs(nuclides='sum').flatten())
openmoc_material.setChi(chi.get_xs(nuclides='sum').flatten())
# 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()
# 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))
There is a non-trivial bias in both the 2-group and 8-group cases. In the case of a pin cell, one can show that these biases do not converge to <100 pcm with more particle histories. For heterogeneous geometries, additional measures must be taken to address the following three sources of bias:
- Appropriate transport-corrected cross sections
- Spatial discretization of OpenMOC's mesh
- Constant-in-angle multi-group cross sections
Visualizing MGXS Data¶
It is often insightful to generate visual depictions of multi-group cross sections. There are many different types of plots which may be useful for multi-group cross section visualization, only a few of which will be shown here for enrichment and inspiration.
One particularly useful visualization is a comparison of the continuous-energy and multi-group cross sections for a particular nuclide and reaction type. We illustrate one option for generating such plots with the use of the openmc.plotter
module to plot continuous-energy cross sections from the openly available cross section library distributed by NNDC.
The MGXS data can also be plotted using the openmc.plot_xs command, however we will do this manually here to show how the openmc.Mgxs.get_xs method can be used to obtain data.
# Create a figure of the U-235 continuous-energy fission cross section
fig = openmc.plot_xs(u235, ['fission'])
# Get the axis to use for plotting the MGXS
ax = fig.gca()
# Extract energy group bounds and MGXS values to plot
fission = xs_library[fuel_cell.id]['fission']
energy_groups = fission.energy_groups
x = energy_groups.group_edges
y = fission.get_xs(nuclides=['U235'], order_groups='decreasing', xs_type='micro')
y = np.squeeze(y)
# Fix low energy bound
x[0] = 1.e-5
# Extend the mgxs values array for matplotlib's step plot
y = np.insert(y, 0, y[0])
# Create a step plot for the MGXS
ax.plot(x, y, drawstyle='steps', color='r', linewidth=3)
ax.set_title('U-235 Fission Cross Section')
ax.legend(['Continuous', 'Multi-Group'])
ax.set_xlim((x.min(), x.max()))
Another useful type of illustration is scattering matrix sparsity structures. First, we extract Pandas DataFrames
for the H-1 and O-16 scattering matrices.
# Construct a Pandas DataFrame for the microscopic nu-scattering matrix
nuscatter = xs_library[moderator_cell.id]['nu-scatter']
df = nuscatter.get_pandas_dataframe(xs_type='micro')
# Slice DataFrame in two for each nuclide's mean values
h1 = df[df['nuclide'] == 'H1']['mean']
o16 = df[df['nuclide'] == 'O16']['mean']
# Cast DataFrames as NumPy arrays
h1 = h1.as_matrix()
o16 = o16.as_matrix()
# Reshape arrays to 2D matrix for plotting
h1.shape = (fine_groups.num_groups, fine_groups.num_groups)
o16.shape = (fine_groups.num_groups, fine_groups.num_groups)
Matplotlib's imshow
routine can be used to plot the matrices to illustrate their sparsity structures.
# Create plot of the H-1 scattering matrix
fig = plt.subplot(121)
fig.imshow(h1, interpolation='nearest', cmap='jet')
plt.title('H-1 Scattering Matrix')
plt.xlabel('Group Out')
plt.ylabel('Group In')
# Create plot of the O-16 scattering matrix
fig2 = plt.subplot(122)
fig2.imshow(o16, interpolation='nearest', cmap='jet')
plt.title('O-16 Scattering Matrix')
plt.xlabel('Group Out')
plt.ylabel('Group In')
# Show the plot on screen
plt.show()
MGXS Part III: Libraries¶
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
withopenmc.mgxs.Library
- Validation of multi-group cross sections with OpenMOC
- Steady-state pin-by-pin fission rates comparison between OpenMC and OpenMOC
Note: This Notebook was created using OpenMOC to verify the multi-group cross-sections generated by OpenMC. You must install OpenMOC on your system to run this Notebook in its entirety. In addition, this Notebook illustrates the use of Pandas DataFrames
to containerize multi-group cross section data.
Generate Input Files¶
import math
import pickle
from IPython.display import Image
import matplotlib.pyplot as plt
import numpy as np
import openmc
import openmc.mgxs
from openmc.openmoc_compatible import get_openmoc_geometry
import openmoc
import openmoc.process
from openmoc.materialize import load_openmc_mgxs_lib
%matplotlib inline
First we need to define materials that will be used in the problem. Before defining a material, we must create nuclides that are used in the material.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
b10 = openmc.Nuclide('B10')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create three materials for the fuel, water, and cladding of the fuel pins.
# 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.
# Instantiate a Materials object
materials_file = openmc.Materials((fuel, water, zircaloy))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC's lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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 2500 particles.
# 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.
# 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.
# Run openmc in plotting mode
openmc.plot_geometry(output=False)
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png
# Display the materials plot inline
Image(filename='materials-xy.png')
As we can see from the plot, we have a nice array of fuel and guide tube pin cells with fuel, cladding, and water!
Create an MGXS Library¶
Now we are ready to generate multi-group cross sections! First, let's define a 2-group structure using the built-in EnergyGroups
class.
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])
Next, we will instantiate an openmc.mgxs.Library
for the energy groups with our the fuel assembly geometry.
# Initialize an 2-group MGXS Library for OpenMOC
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = groups
Now, we must specify to the Library
which types of cross sections to compute. In particular, the following are the multi-group cross section MGXS
subclasses that are mapped to string codes accepted by the Library
class:
TotalXS
("total"
)TransportXS
("transport"
or"nu-transport
withnu
set toTrue
)AbsorptionXS
("absorption"
)CaptureXS
("capture"
)FissionXS
("fission"
or"nu-fission"
withnu
set toTrue
)KappaFissionXS
("kappa-fission"
)ScatterXS
("scatter"
or"nu-scatter"
withnu
set toTrue
)ScatterMatrixXS
("scatter matrix"
or"nu-scatter matrix"
withnu
set toTrue
)Chi
("chi"
)ChiPrompt
("chi prompt"
)InverseVelocity
("inverse-velocity"
)PromptNuFissionXS
("prompt-nu-fission"
)DelayedNuFissionXS
("delayed-nu-fission"
)ChiDelayed
("chi-delayed"
)Beta
("beta"
)
In this case, let's create the multi-group cross sections needed to run an OpenMOC simulation to verify the accuracy of our cross sections. In particular, we will define "transport"
, "nu-fission"
, '"fission"
, "nu-scatter matrix"
and "chi"
cross sections for our Library
.
Note: A variety of different approximate transport-corrected total multi-group cross sections (and corresponding scattering matrices) can be found in the literature. At the present time, the openmc.mgxs
module only supports the "P0"
transport correction. This correction can be turned on and off through the boolean Library.correction
property which may take values of "P0"
(default) or None
.
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['transport', 'nu-fission', 'fission', 'nu-scatter matrix', 'chi']
Now we must specify the type of domain over which we would like the Library
to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library
supports "material"
, "cell"
, "universe"
, and "mesh"
domain types. We will use a "cell"
domain type here to compute cross sections in each of the cells in the fuel assembly geometry.
Note: By default, the Library
class will instantiate MGXS
objects for each and every domain (material, cell or universe) in the geometry of interest. However, one may specify a subset of these domains to the Library.domains
property. In our case, we wish to compute multi-group cross sections in each and every cell since they will be needed in our downstream OpenMOC calculation on the identical combinatorial geometry mesh.
# Specify a "cell" domain type for the cross section tally filters
mgxs_lib.domain_type = 'cell'
# Specify the cell domains over which to compute multi-group cross sections
mgxs_lib.domains = geometry.get_all_material_cells().values()
We can easily instruct the Library
to compute multi-group cross sections on a nuclide-by-nuclide basis with the boolean Library.by_nuclide
property. By default, by_nuclide
is set to False
, but we will set it to True
here.
# 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.
# 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.
# 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.
# Instantiate a tally Mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.upper_right = [+10.71, +10.71]
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission', 'nu-fission']
# Add tally to collection
tallies_file.append(tally)
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
# Run OpenMC
openmc.run()
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint
object.
# 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.
# 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.
# 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.
df = fuel_mgxs.get_pandas_dataframe()
df
Similarly, we can use the MGXS.print_xs(...)
method to view a string representation of the multi-group cross section data.
fuel_mgxs.print_xs()
One can export the entire Library
to HDF5 with the Library.build_hdf5_store(...)
method as follows:
# 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.
# Store a Library and its MGXS objects in a pickled binary file "mgxs/mgxs.pkl"
mgxs_lib.dump_to_file(filename='mgxs', directory='mgxs')
# 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.
# Create a 1-group structure
coarse_groups = openmc.mgxs.EnergyGroups(group_edges=[0., 20.0e6])
# Create a new MGXS Library on the coarse 1-group structure
coarse_mgxs_lib = mgxs_lib.get_condensed_library(coarse_groups)
# 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()
Verification with OpenMOC¶
Of course it is always a good idea to verify that one's cross sections are accurate. We can easily do so here with the deterministic transport code OpenMOC. We first construct an equivalent OpenMOC geometry.
# Create an OpenMOC Geometry from the OpenMC Geometry
openmoc_geometry = get_openmoc_geometry(mgxs_lib.geometry)
Now, we can inject the multi-group cross sections into the equivalent fuel assembly OpenMOC geometry. The openmoc.materialize
module supports the loading of Library
objects from OpenMC as illustrated below.
# 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.
# 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()
We report the eigenvalues computed by OpenMC and OpenMOC here together to summarize our results.
# 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))
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.
# 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.
# 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.
# 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')
Multi-Group (Delayed) Cross Section Generation Part I: Introduction¶
This IPython Notebook introduces the use of the openmc.mgxs
module to calculate multi-energy-group and multi-delayed-group cross sections for an infinite homogeneous medium. In particular, this Notebook introduces the the following features:
- Creation of multi-delayed-group cross sections for an infinite homogeneous medium
- Calculation of delayed neutron precursor concentrations
Introduction to Multi-Delayed-Group Cross Sections (MDGXS)¶
Many Monte Carlo particle transport codes, including OpenMC, use continuous-energy nuclear cross section data. However, most deterministic neutron transport codes use multi-group cross sections defined over discretized energy bins or energy groups. Furthermore, kinetics calculations typically separate out parameters that involve delayed neutrons into prompt and delayed components and further subdivide delayed components by delayed groups. An example is the energy spectrum for prompt and delayed neutrons for U-235 and Pu-239 computed for a light water reactor spectrum.
from IPython.display import Image
Image(filename='images/mdgxs.png', width=350)
A variety of tools employing different methodologies have been developed over the years to compute multi-group cross sections for certain applications, including NJOY (LANL), MC$^2$-3 (ANL), and Serpent (VTT). The openmc.mgxs
Python module is designed to leverage OpenMC's tally system to calculate multi-group cross sections with arbitrary energy discretizations and different delayed group models (e.g. 6, 7, or 8 delayed group models) for fine-mesh heterogeneous deterministic neutron transport applications.
Before proceeding to illustrate how one may use the openmc.mgxs
module, it is worthwhile to define the general equations used to calculate multi-energy-group and multi-delayed-group cross sections. This is only intended as a brief overview of the methodology used by openmc.mgxs
- we refer the interested reader to the large body of literature on the subject for a more comprehensive understanding of this complex topic.
Introductory Notation¶
The continuous real-valued microscopic cross section may be denoted $\sigma_{n,x}(\mathbf{r}, E)$ for position vector $\mathbf{r}$, energy $E$, nuclide $n$ and interaction type $x$. Similarly, the scalar neutron flux may be denoted by $\Phi(\mathbf{r},E)$ for position $\mathbf{r}$ and energy $E$. Note: Although nuclear cross sections are dependent on the temperature $T$ of the interacting medium, the temperature variable is neglected here for brevity.
Spatial and Energy Discretization¶
The energy domain for critical systems such as thermal reactors spans more than 10 orders of magnitude of neutron energies from 10$^{-5}$ - 10$^7$ eV. The multi-group approximation discretization divides this energy range into one or more energy groups. In particular, for $G$ total groups, we denote an energy group index $g$ such that $g \in \{1, 2, ..., G\}$. The energy group indices are defined such that the smaller group the higher the energy, and vice versa. The integration over neutron energies across a discrete energy group is commonly referred to as energy condensation.
The delayed neutrons created from fissions are created from > 30 delayed neutron precursors. Modeling each of the delayed neutron precursors is possible, but this approach has not recieved much attention due to large uncertainties in certain precursors. Therefore, the delayed neutrons are often combined into "delayed groups" that have a set time constant, $\lambda_d$. Some cross section libraries use the same group time constants for all nuclides (e.g. JEFF 3.1) while other libraries use different time constants for all nuclides (e.g. ENDF/B-VII.1). Multi-delayed-group cross sections can either be created with the entire delayed group set, a subset of delayed groups, or integrated over all delayed groups.
Multi-group cross sections are computed for discretized spatial zones in the geometry of interest. The spatial zones may be defined on a structured and regular fuel assembly or pin cell mesh, an arbitrary unstructured mesh or the constructive solid geometry used by OpenMC. For a geometry with $K$ distinct spatial zones, we designate each spatial zone an index $k$ such that $k \in \{1, 2, ..., K\}$. The volume of each spatial zone is denoted by $V_{k}$. The integration over discrete spatial zones is commonly referred to as spatial homogenization.
General Scalar-Flux Weighted MDGXS¶
The multi-group cross sections computed by openmc.mgxs
are defined as a scalar flux-weighted average of the microscopic cross sections across each discrete energy group. This formulation is employed in order to preserve the reaction rates within each energy group and spatial zone. In particular, spatial homogenization and energy condensation are used to compute the general multi-group cross section. For instance, the delayed-nu-fission multi-energy-group and multi-delayed-group cross section, $\nu_d \sigma_{f,x,k,g}$, can be computed as follows:
This scalar flux-weighted average microscopic cross section is computed by openmc.mgxs
for only the delayed-nu-fission and delayed neutron fraction reaction type at the moment. These double integrals are stochastically computed with OpenMC's tally system - in particular, filters on the energy range and spatial zone (material, cell, universe, or mesh) define the bounds of integration for both numerator and denominator.
Multi-Group Prompt and Delayed Fission Spectrum¶
The energy spectrum of neutrons emitted from fission is denoted by $\chi_{n}(\mathbf{r},E' \rightarrow E'')$ for incoming and outgoing energies $E'$ and $E''$, respectively. Unlike the multi-group cross sections $\sigma_{n,x,k,g}$ considered up to this point, the fission spectrum is a probability distribution and must sum to unity. The outgoing energy is typically much less dependent on the incoming energy for fission than for scattering interactions. As a result, it is common practice to integrate over the incoming neutron energy when computing the multi-group fission spectrum. The fission spectrum may be simplified as $\chi_{n}(\mathbf{r},E)$ with outgoing energy $E$.
Computing the cumulative energy spectrum of emitted neutrons, $\chi_{n}(\mathbf{r},E)$, has been presented in the mgxs-part-i.ipynb
notebook. Here, we will present the energy spectrum of prompt and delayed emission neutrons, $\chi_{n,p}(\mathbf{r},E)$ and $\chi_{n,d}(\mathbf{r},E)$, respectively. Unlike the multi-group cross sections defined up to this point, the multi-group fission spectrum is weighted by the fission production rate rather than the scalar flux. This formulation is intended to preserve the total fission production rate in the multi-group deterministic calculation. In order to mathematically define the multi-group fission spectrum, we denote the microscopic fission cross section as $\sigma_{n,f}(\mathbf{r},E)$ and the average number of neutrons emitted from fission interactions with nuclide $n$ as $\nu_{n,p}(\mathbf{r},E)$ and $\nu_{n,d}(\mathbf{r},E)$ for prompt and delayed neutrons, respectively. The multi-group fission spectrum $\chi_{n,k,g,d}$ is then the probability of fission neutrons emitted into energy group $g$ and delayed group $d$. There are not prompt groups, so inserting $p$ in place of $d$ just denotes all prompt neutrons.
Similar to before, spatial homogenization and energy condensation are used to find the multi-energy-group and multi-delayed-group fission spectrum $\chi_{n,k,g,d}$ as follows:
$$\chi_{n,k,g',d} = \frac{\int_{E_{g'}}^{E_{g'-1}}\mathrm{d}E''\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\chi_{n,d}(\mathbf{r},E'\rightarrow E'')\nu_{n,d}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')}{\int_{0}^{\infty}\mathrm{d}E'\int_{\mathbf{r} \in V_{k}}\mathrm{d}\mathbf{r}\nu_{n,d}(\mathbf{r},E')\sigma_{n,f}(\mathbf{r},E')\Phi(\mathbf{r},E')}$$The fission production-weighted multi-energy-group and multi-delayed-group fission spectrum for delayed neutrons is computed using OpenMC tallies with energy in, energy out, and delayed group filters. Alternatively, the delayed group filter can be omitted to compute the fission spectrum integrated over all delayed groups.
This concludes our brief overview on the methodology to compute multi-energy-group and multi-delayed-group cross sections. The following sections detail more concretely how users may employ the openmc.mgxs
module to power simulation workflows requiring multi-group cross sections for downstream deterministic calculations.
Generate Input Files¶
%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.
# Instantiate some Nuclides
h1 = openmc.Nuclide('H1')
o16 = openmc.Nuclide('O16')
u235 = openmc.Nuclide('U235')
u238 = openmc.Nuclide('U238')
pu239 = openmc.Nuclide('Pu239')
zr90 = openmc.Nuclide('Zr90')
With the nuclides we defined, we will now create a material for the homogeneous medium.
# Instantiate a Material and register the Nuclides
inf_medium = openmc.Material(name='moderator')
inf_medium.set_density('g/cc', 5.)
inf_medium.add_nuclide(h1, 0.03)
inf_medium.add_nuclide(o16, 0.015)
inf_medium.add_nuclide(u235 , 0.0001)
inf_medium.add_nuclide(u238 , 0.007)
inf_medium.add_nuclide(pu239, 0.00003)
inf_medium.add_nuclide(zr90, 0.002)
With our material, we can now create a Materials
object that can be exported to an actual XML file.
# 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.
# 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.
# 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.
# 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.
# 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.
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 5000
# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': True}
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings_file.export_to_xml()
Now we are ready to generate multi-group cross sections! First, let's define a 100-energy-group structure and 1-energy-group structure using the built-in EnergyGroups
class. We will also create a 6-delayed-group list.
# Instantiate a 100-group EnergyGroups object
energy_groups = mgxs.EnergyGroups()
energy_groups.group_edges = np.logspace(-3, 7.3, 101)
# Instantiate a 1-group EnergyGroups object
one_group = mgxs.EnergyGroups()
one_group.group_edges = np.array([energy_groups.group_edges[0], energy_groups.group_edges[-1]])
delayed_groups = list(range(1,7))
We can now use the EnergyGroups
object and delayed group list, along with our previously created materials and geometry, to instantiate some MGXS
objects from the openmc.mgxs
module. In particular, the following are subclasses of the generic and abstract MGXS
class:
TotalXS
TransportXS
AbsorptionXS
CaptureXS
FissionXS
NuFissionMatrixXS
KappaFissionXS
ScatterXS
ScatterMatrixXS
Chi
InverseVelocity
A separate abstract MDGXS
class is used for cross-sections and parameters that involve delayed neutrons. The subclasses of MDGXS
include:
DelayedNuFissionXS
ChiDelayed
Beta
DecayRate
These classes provide us with an interface to generate the tally inputs as well as perform post-processing of OpenMC's tally data to compute the respective multi-group cross sections.
In this case, let's create the multi-group chi-prompt, chi-delayed, and prompt-nu-fission cross sections with our 100-energy-group structure and multi-group delayed-nu-fission and beta cross sections with our 100-energy-group and 6-delayed-group structures.
The prompt chi and nu-fission data can actually be gathered using the Chi
and FissionXS
classes, respectively, by passing in a value of True
for the optional prompt
parameter upon initialization.
# Instantiate a few different sections
chi_prompt = mgxs.Chi(domain=cell, groups=energy_groups, by_nuclide=True, prompt=True)
prompt_nu_fission = mgxs.FissionXS(domain=cell, groups=energy_groups, by_nuclide=True, nu=True, prompt=True)
chi_delayed = mgxs.ChiDelayed(domain=cell, energy_groups=energy_groups, by_nuclide=True)
delayed_nu_fission = mgxs.DelayedNuFissionXS(domain=cell, energy_groups=energy_groups, delayed_groups=delayed_groups, by_nuclide=True)
beta = mgxs.Beta(domain=cell, energy_groups=energy_groups, delayed_groups=delayed_groups, by_nuclide=True)
decay_rate = mgxs.DecayRate(domain=cell, energy_groups=one_group, delayed_groups=delayed_groups, by_nuclide=True)
chi_prompt.nuclides = ['U235', 'Pu239']
prompt_nu_fission.nuclides = ['U235', 'Pu239']
chi_delayed.nuclides = ['U235', 'Pu239']
delayed_nu_fission.nuclides = ['U235', 'Pu239']
beta.nuclides = ['U235', 'Pu239']
decay_rate.nuclides = ['U235', 'Pu239']
Each multi-group cross section object stores its tallies in a Python dictionary called tallies
. We can inspect the tallies in the dictionary for our Decay Rate
object as follows.
decay_rate.tallies
The Beta
object includes tracklength tallies for the 'nu-fission' and 'delayed-nu-fission' scores in the 100-energy-group and 6-delayed-group structure in cell 1. Now that each MGXS
and MDGXS
object contains the tallies that it needs, we must add these tallies to a Tallies
object to generate the "tallies.xml" input file for OpenMC.
# Instantiate an empty Tallies object
tallies_file = openmc.Tallies()
# Add chi-prompt tallies to the tallies file
tallies_file += chi_prompt.tallies.values()
# Add prompt-nu-fission tallies to the tallies file
tallies_file += prompt_nu_fission.tallies.values()
# Add chi-delayed tallies to the tallies file
tallies_file += chi_delayed.tallies.values()
# Add delayed-nu-fission tallies to the tallies file
tallies_file += delayed_nu_fission.tallies.values()
# Add beta tallies to the tallies file
tallies_file += beta.tallies.values()
# Add decay rate tallies to the tallies file
tallies_file += decay_rate.tallies.values()
# Export to "tallies.xml"
tallies_file.export_to_xml()
Now we a have a complete set of inputs, so we can go ahead and run our simulation.
# Run OpenMC
openmc.run()
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint
object.
# 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.
# Load the tallies from the statepoint into each MGXS object
chi_prompt.load_from_statepoint(sp)
prompt_nu_fission.load_from_statepoint(sp)
chi_delayed.load_from_statepoint(sp)
delayed_nu_fission.load_from_statepoint(sp)
beta.load_from_statepoint(sp)
decay_rate.load_from_statepoint(sp)
Voila! Our multi-group cross sections are now ready to rock 'n roll!
Extracting and Storing MGXS Data¶
Let's first inspect our delayed-nu-fission section by printing it to the screen after condensing the cross section down to one group.
delayed_nu_fission.get_condensed_xs(one_group).get_xs()
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.
df = delayed_nu_fission.get_pandas_dataframe()
df.head(10)
df = decay_rate.get_pandas_dataframe()
df.head(12)
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.
beta.export_xs_data(filename='beta', format='excel')
The following code snippet shows how to export the chi-prompt and chi-delayed MGXS
to the same HDF5 binary data store.
chi_prompt.build_hdf5_store(filename='mdgxs', append=True)
chi_delayed.build_hdf5_store(filename='mdgxs', append=True)
Using Tally Arithmetic to Compute the Delayed Neutron Precursor Concentrations¶
Finally, we illustrate how one can leverage OpenMC's tally arithmetic data processing feature with MGXS
objects. The openmc.mgxs
module uses tally arithmetic to compute multi-group cross sections with automated uncertainty propagation. Each MGXS
object includes an xs_tally
attribute which is a "derived" Tally
based on the tallies needed to compute the cross section type of interest. These derived tallies can be used in subsequent tally arithmetic operations. For example, we can use tally artithmetic to compute the delayed neutron precursor concentrations using the Beta
, DelayedNuFissionXS
, and DecayRate
objects. The delayed neutron precursor concentrations are modeled using the following equations:
First, let's investigate the decay rates for U235 and Pu235. The fraction of the delayed neutron precursors remaining as a function of time after fission for each delayed group and fissioning isotope have been plotted below.
# Get the decay rate data
dr_tally = decay_rate.xs_tally
dr_u235 = dr_tally.get_values(nuclides=['U235']).flatten()
dr_pu239 = dr_tally.get_values(nuclides=['Pu239']).flatten()
# Compute the exponential decay of the precursors
time = np.logspace(-3,3)
dr_u235_points = np.exp(-np.outer(dr_u235, time))
dr_pu239_points = np.exp(-np.outer(dr_pu239, time))
# Create a plot of the fraction of the precursors remaining as a f(time)
colors = ['b', 'g', 'r', 'c', 'm', 'k']
legend = []
fig = plt.figure(figsize=(8,6))
for g,c in enumerate(colors):
plt.semilogx(time, dr_u235_points [g,:], color=c, linestyle='--', linewidth=3)
plt.semilogx(time, dr_pu239_points[g,:], color=c, linestyle=':' , linewidth=3)
legend.append('U-235 $t_{1/2}$ = ' + '{0:1.2f} seconds'.format(np.log(2) / dr_u235[g]))
legend.append('Pu-239 $t_{1/2}$ = ' + '{0:1.2f} seconds'.format(np.log(2) / dr_pu239[g]))
plt.title('Delayed Neutron Precursor Decay Rates')
plt.xlabel('Time (s)')
plt.ylabel('Fraction Remaining')
plt.legend(legend, loc=1, bbox_to_anchor=(1.55, 0.95))
Now let's compute the initial concentration of the delayed neutron precursors:
# Use tally arithmetic to compute the precursor concentrations
precursor_conc = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) * \
delayed_nu_fission.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) / \
decay_rate.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True)
# Get the Pandas DataFrames for inspection
precursor_conc.get_pandas_dataframe()
We can plot the delayed neutron fractions for each nuclide.
energy_filter = [f for f in beta.xs_tally.filters if type(f) is openmc.EnergyFilter]
beta_integrated = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True)
beta_u235 = beta_integrated.get_values(nuclides=['U235'])
beta_pu239 = beta_integrated.get_values(nuclides=['Pu239'])
# Reshape the betas
beta_u235.shape = (beta_u235.shape[0])
beta_pu239.shape = (beta_pu239.shape[0])
df = beta_integrated.summation(filter_type=openmc.DelayedGroupFilter, remove_filter=True).get_pandas_dataframe()
print('Beta (U-235) : {:.6f} +/- {:.6f}'.format(df[df['nuclide'] == 'U235']['mean'][0], df[df['nuclide'] == 'U235']['std. dev.'][0]))
print('Beta (Pu-239): {:.6f} +/- {:.6f}'.format(df[df['nuclide'] == 'Pu239']['mean'][1], df[df['nuclide'] == 'Pu239']['std. dev.'][1]))
beta_u235 = np.append(beta_u235[0], beta_u235)
beta_pu239 = np.append(beta_pu239[0], beta_pu239)
# Create a step plot for the MGXS
plt.plot(np.arange(0.5, 7.5, 1), beta_u235, drawstyle='steps', color='b', linewidth=3)
plt.plot(np.arange(0.5, 7.5, 1), beta_pu239, drawstyle='steps', color='g', linewidth=3)
plt.title('Delayed Neutron Fraction (beta)')
plt.xlabel('Delayed Group')
plt.ylabel('Beta(fraction total neutrons)')
plt.legend(['U-235', 'Pu-239'])
plt.xlim([0,7])
We can also plot the energy spectrum for fission emission of prompt and delayed neutrons.
chi_d_u235 = np.squeeze(chi_delayed.get_xs(nuclides=['U235'], order_groups='decreasing'))
chi_d_pu239 = np.squeeze(chi_delayed.get_xs(nuclides=['Pu239'], order_groups='decreasing'))
chi_p_u235 = np.squeeze(chi_prompt.get_xs(nuclides=['U235'], order_groups='decreasing'))
chi_p_pu239 = np.squeeze(chi_prompt.get_xs(nuclides=['Pu239'], order_groups='decreasing'))
chi_d_u235 = np.append(chi_d_u235 , chi_d_u235[0])
chi_d_pu239 = np.append(chi_d_pu239, chi_d_pu239[0])
chi_p_u235 = np.append(chi_p_u235 , chi_p_u235[0])
chi_p_pu239 = np.append(chi_p_pu239, chi_p_pu239[0])
# Create a step plot for the MGXS
plt.semilogx(energy_groups.group_edges, chi_d_u235 , drawstyle='steps', color='b', linestyle='--', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_d_pu239, drawstyle='steps', color='g', linestyle='--', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_p_u235 , drawstyle='steps', color='b', linestyle=':', linewidth=3)
plt.semilogx(energy_groups.group_edges, chi_p_pu239, drawstyle='steps', color='g', linestyle=':', linewidth=3)
plt.title('Energy Spectrum for Fission Neutrons')
plt.xlabel('Energy (eV)')
plt.ylabel('Fraction on emitted neutrons')
plt.legend(['U-235 delayed', 'Pu-239 delayed', 'U-235 prompt', 'Pu-239 prompt'],loc=2)
plt.xlim(1.0e3, 20.0e6)
Multi-Group (Delayed) Cross Section Generation Part II: Advanced Features¶
This IPython Notebook illustrates the use of the openmc.mgxs.Library
class. The Library
class is designed to automate the calculation of multi-group cross sections for use cases with one or more domains, cross section types, and/or nuclides. In particular, this Notebook illustrates the following features:
- Calculation of multi-energy-group and multi-delayed-group cross sections for a fuel assembly
- Automated creation, manipulation and storage of
MGXS
withopenmc.mgxs.Library
- Steady-state pin-by-pin delayed neutron fractions (beta) for each delayed group.
- Generation of surface currents on the interfaces and surfaces of a Mesh.
Generate Input Files¶
%matplotlib inline
import math
import matplotlib.pyplot as plt
import numpy as np
import openmc
import openmc.mgxs
First we need to define materials that will be used in the problem: fuel, water, and cladding.
# 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.
# Create a materials collection and export to XML
materials = openmc.Materials((fuel, water, zircaloy))
materials.export_to_xml()
Now let's move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC's lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.
# Create cylinders for the fuel and clad
fuel_outer_radius = openmc.ZCylinder(R=0.39218)
clad_outer_radius = openmc.ZCylinder(R=0.45720)
# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='reflective')
min_y = openmc.YPlane(y0=-10.71, boundary_type='reflective')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10., boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10., boundary_type='reflective')
With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.
# 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.
# 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.
# 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.
# Create array indices for guide tube locations in lattice
template_x = np.array([5, 8, 11, 3, 13, 2, 5, 8, 11, 14, 2, 5, 8,
11, 14, 2, 5, 8, 11, 14, 3, 13, 5, 8, 11])
template_y = np.array([2, 2, 2, 3, 3, 5, 5, 5, 5, 5, 8, 8, 8, 8,
8, 11, 11, 11, 11, 11, 13, 13, 14, 14, 14])
# Create universes array with the fuel pin and guide tube universes
universes = np.tile(fuel_pin_universe, (17,17))
universes[template_x, template_y] = guide_tube_universe
# Store the array of universes in the lattice
assembly.universes = universes
OpenMC requires that there is a "root" universe. Let us create a root cell that is filled by the pin cell universe and then assign it to the root universe.
# Create root Cell
root_cell = openmc.Cell(name='root cell', fill=assembly)
# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z
# Create root Universe
root_universe = openmc.Universe(universe_id=0, name='root universe')
root_universe.add_cell(root_cell)
We now must create a geometry that is assigned a root universe and export it to XML.
# Create Geometry and export to XML
geometry = openmc.Geometry(root_universe)
geometry.export_to_xml()
With the geometry and materials finished, we now just need to define simulation parameters. In this case, we will use 10 inactive batches and 40 active batches each with 2500 particles.
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 2500
# Instantiate a Settings object
settings = openmc.Settings()
settings.batches = batches
settings.inactive = inactive
settings.particles = particles
settings.output = {'tallies': False}
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings.export_to_xml()
Let us also create a plot to verify that our fuel assembly geometry was created successfully.
# Plot our geometry
plot = openmc.Plot.from_geometry(geometry)
plot.pixels = (250, 250)
plot.color_by = 'material'
openmc.plot_inline(plot)
As we can see from the plot, we have a nice array of fuel and guide tube pin cells with fuel, cladding, and water!
Create an MGXS Library¶
Now we are ready to generate multi-group cross sections! First, let's define a 20-energy-group and 1-energy-group.
# Instantiate a 20-group EnergyGroups object
energy_groups = openmc.mgxs.EnergyGroups()
energy_groups.group_edges = np.logspace(-3, 7.3, 21)
# Instantiate a 1-group EnergyGroups object
one_group = openmc.mgxs.EnergyGroups()
one_group.group_edges = np.array([energy_groups.group_edges[0], energy_groups.group_edges[-1]])
Next, we will instantiate an openmc.mgxs.Library
for the energy and delayed groups with our the fuel assembly geometry.
# Instantiate a tally mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [17, 17, 1]
mesh.lower_left = [-10.71, -10.71, -10000.]
mesh.width = [1.26, 1.26, 20000.]
# Initialize an 20-energy-group and 6-delayed-group MGXS Library
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = energy_groups
mgxs_lib.num_delayed_groups = 6
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['total', 'transport', 'nu-scatter matrix', 'kappa-fission', 'inverse-velocity', 'chi-prompt',
'prompt-nu-fission', 'chi-delayed', 'delayed-nu-fission', 'beta']
# Specify a "mesh" domain type for the cross section tally filters
mgxs_lib.domain_type = 'mesh'
# Specify the mesh domain over which to compute multi-group cross sections
mgxs_lib.domains = [mesh]
# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)
# Instantiate a current tally
mesh_filter = openmc.MeshFilter(mesh)
current_tally = openmc.Tally(name='current tally')
current_tally.scores = ['current']
current_tally.filters = [mesh_filter]
# Add current tally to the tallies file
tallies_file.append(current_tally)
# Export to "tallies.xml"
tallies_file.export_to_xml()
Now, we can run OpenMC to generate the cross sections.
# Run OpenMC
openmc.run()
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. We begin our analysis by instantiating a StatePoint
object.
# 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.
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)
# Extrack the current tally separately
current_tally = sp.get_tally(name='current tally')
Using Tally Arithmetic to Compute the Delayed Neutron Precursor Concentrations¶
Finally, we illustrate how one can leverage OpenMC's tally arithmetic data processing feature with MGXS
objects. The openmc.mgxs
module uses tally arithmetic to compute multi-group cross sections with automated uncertainty propagation. Each MGXS
object includes an xs_tally
attribute which is a "derived" Tally
based on the tallies needed to compute the cross section type of interest. These derived tallies can be used in subsequent tally arithmetic operations. For example, we can use tally artithmetic to compute the delayed neutron precursor concentrations using the Beta
and DelayedNuFissionXS
objects. The delayed neutron precursor concentrations are modeled using the following equations:
# Set the time constants for the delayed precursors (in seconds^-1)
precursor_halflife = np.array([55.6, 24.5, 16.3, 2.37, 0.424, 0.195])
precursor_lambda = math.log(2.0) / precursor_halflife
beta = mgxs_lib.get_mgxs(mesh, 'beta')
# Create a tally object with only the delayed group filter for the time constants
beta_filters = [f for f in beta.xs_tally.filters if type(f) is not openmc.DelayedGroupFilter]
lambda_tally = beta.xs_tally.summation(nuclides=beta.xs_tally.nuclides)
for f in beta_filters:
lambda_tally = lambda_tally.summation(filter_type=type(f), remove_filter=True) * 0. + 1.
# Set the mean of the lambda tally and reshape to account for nuclides and scores
lambda_tally._mean = precursor_lambda
lambda_tally._mean.shape = lambda_tally.std_dev.shape
# Set a total nuclide and lambda score
lambda_tally.nuclides = [openmc.Nuclide(name='total')]
lambda_tally.scores = ['lambda']
delayed_nu_fission = mgxs_lib.get_mgxs(mesh, 'delayed-nu-fission')
# Use tally arithmetic to compute the precursor concentrations
precursor_conc = beta.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) * \
delayed_nu_fission.xs_tally.summation(filter_type=openmc.EnergyFilter, remove_filter=True) / lambda_tally
# The difference is a derived tally which can generate Pandas DataFrames for inspection
precursor_conc.get_pandas_dataframe().head(10)
Another useful feature of the Python API is the ability to extract the surface currents for the interfaces and surfaces of a mesh. We can inspect the currents for the mesh by getting the pandas dataframe.
current_tally.get_pandas_dataframe().head(10)
Cross Section Visualizations¶
In addition to inspecting the data in the tallies by getting the pandas dataframe, we can also plot the tally data on the domain mesh. Below is the delayed neutron fraction tallied in each mesh cell for each delayed group.
# Extract the energy-condensed delayed neutron fraction tally
beta_by_group = beta.get_condensed_xs(one_group).xs_tally.summation(filter_type='energy', remove_filter=True)
beta_by_group.mean.shape = (17, 17, 6)
beta_by_group.mean[beta_by_group.mean == 0] = np.nan
# Plot the betas
plt.figure(figsize=(18,9))
fig = plt.subplot(231)
plt.imshow(beta_by_group.mean[:,:,0], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 1')
fig = plt.subplot(232)
plt.imshow(beta_by_group.mean[:,:,1], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 2')
fig = plt.subplot(233)
plt.imshow(beta_by_group.mean[:,:,2], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 3')
fig = plt.subplot(234)
plt.imshow(beta_by_group.mean[:,:,3], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 4')
fig = plt.subplot(235)
plt.imshow(beta_by_group.mean[:,:,4], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 5')
fig = plt.subplot(236)
plt.imshow(beta_by_group.mean[:,:,5], interpolation='none', cmap='jet')
plt.colorbar()
plt.title('Beta - delayed group 6')
Multi-Group Mode¶
Multi-Group Mode Part I: Introduction¶
This Notebook illustrates the usage of OpenMC's multi-group calculational mode with the Python API. This example notebook creates and executes the 2-D C5G7 benchmark model using the openmc.MGXSLibrary
class to create the supporting data library on the fly.
Generate MGXS Library¶
import os
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import numpy as np
import openmc
%matplotlib inline
We will now create the multi-group library using data directly from Appendix A of the C5G7 benchmark documentation. All of the data below will be created at 294K, consistent with the benchmark.
This notebook will first begin by setting the group structure and building the groupwise data for UO2. As you can see, the cross sections are input in the order of increasing groups (or decreasing energy).
Note: The C5G7 benchmark uses transport-corrected cross sections. So the total cross section we input here will technically be the transport cross section.
# Create a 7-group structure with arbitrary boundaries (the specific boundaries are unimportant)
groups = openmc.mgxs.EnergyGroups(np.logspace(-5, 7, 8))
uo2_xsdata = openmc.XSdata('uo2', groups)
uo2_xsdata.order = 0
# When setting the data let the object know you are setting the data for a temperature of 294K.
uo2_xsdata.set_total([1.77949E-1, 3.29805E-1, 4.80388E-1, 5.54367E-1,
3.11801E-1, 3.95168E-1, 5.64406E-1], temperature=294.)
uo2_xsdata.set_absorption([8.0248E-03, 3.7174E-3, 2.6769E-2, 9.6236E-2,
3.0020E-02, 1.1126E-1, 2.8278E-1], temperature=294.)
uo2_xsdata.set_fission([7.21206E-3, 8.19301E-4, 6.45320E-3, 1.85648E-2,
1.78084E-2, 8.30348E-2, 2.16004E-1], temperature=294.)
uo2_xsdata.set_nu_fission([2.005998E-2, 2.027303E-3, 1.570599E-2, 4.518301E-2,
4.334208E-2, 2.020901E-1, 5.257105E-1], temperature=294.)
uo2_xsdata.set_chi([5.87910E-1, 4.11760E-1, 3.39060E-4, 1.17610E-7,
0.00000E-0, 0.00000E-0, 0.00000E-0], temperature=294.)
We will now add the scattering matrix data.
Note: Most users familiar with deterministic transport libraries are already familiar with the idea of entering one scattering matrix for every order (i.e. scattering order as the outer dimension). However, the shape of OpenMC's scattering matrix entry is instead [Incoming groups, Outgoing Groups, Scattering Order] to best enable other scattering representations. We will follow the more familiar approach in this notebook, and then use numpy's numpy.rollaxis
function to change the ordering to what we need (scattering order on the inner dimension).
# The scattering matrix is ordered with incoming groups as rows and outgoing groups as columns
# (i.e., below the diagonal is up-scattering).
scatter_matrix = \
[[[1.27537E-1, 4.23780E-2, 9.43740E-6, 5.51630E-9, 0.00000E-0, 0.00000E-0, 0.00000E-0],
[0.00000E-0, 3.24456E-1, 1.63140E-3, 3.14270E-9, 0.00000E-0, 0.00000E-0, 0.00000E-0],
[0.00000E-0, 0.00000E-0, 4.50940E-1, 2.67920E-3, 0.00000E-0, 0.00000E-0, 0.00000E-0],
[0.00000E-0, 0.00000E-0, 0.00000E-0, 4.52565E-1, 5.56640E-3, 0.00000E-0, 0.00000E-0],
[0.00000E-0, 0.00000E-0, 0.00000E-0, 1.25250E-4, 2.71401E-1, 1.02550E-2, 1.00210E-8],
[0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 1.29680E-3, 2.65802E-1, 1.68090E-2],
[0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 0.00000E-0, 8.54580E-3, 2.73080E-1]]]
scatter_matrix = np.array(scatter_matrix)
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
uo2_xsdata.set_scatter_matrix(scatter_matrix, temperature=294.)
Now that the UO2 data has been created, we can move on to the remaining materials using the same process.
However, we will actually skip repeating the above for now. Our simulation will instead use the c5g7.h5
file that has already been created using exactly the same logic as above, but for the remaining materials in the benchmark problem.
For now we will show how you would use the uo2_xsdata
information to create an openmc.MGXSLibrary
object and write to disk.
# Initialize the library
mg_cross_sections_file = openmc.MGXSLibrary(groups)
# Add the UO2 data to it
mg_cross_sections_file.add_xsdata(uo2_xsdata)
# And write to disk
mg_cross_sections_file.export_to_hdf5('mgxs.h5')
Generate 2-D C5G7 Problem Input Files¶
To build the actual 2-D model, we will first begin by creating the materials.xml
file.
First we need to define materials that will be used in the problem. In other notebooks, either openmc.Nuclide
s or openmc.Element
s were created at the equivalent stage. We can do that in multi-group mode as well. However, multi-group cross-sections are sometimes provided as macroscopic cross-sections; the C5G7 benchmark data are macroscopic. In this case, we can instead use openmc.Macroscopic
objects to in-place of openmc.Nuclide
or openmc.Element
objects.
openmc.Macroscopic
, unlike openmc.Nuclide
and openmc.Element
objects, do not need to be provided enough information to calculate number densities, as no number densities are needed.
When assigning openmc.Macroscopic
objects to openmc.Material
objects, the density can still be scaled by setting the density to a value that is not 1.0. This would be useful, for example, when slightly perturbing the density of water due to a small change in temperature (while of course ignoring any resultant spectral shift). The density of a macroscopic dataset is set to 1.0 in the openmc.Material
object by default when an openmc.Macroscopic
dataset is used; so we will show its use the first time and then afterwards it will not be required.
Aside from these differences, the following code is very similar to similar code in other OpenMC example Notebooks.
# For every cross section data set in the library, assign an openmc.Macroscopic object to a material
materials = {}
for xs in ['uo2', 'mox43', 'mox7', 'mox87', 'fiss_chamber', 'guide_tube', 'water']:
materials[xs] = openmc.Material(name=xs)
materials[xs].set_density('macro', 1.)
materials[xs].add_macroscopic(xs)
Now we can go ahead and produce a materials.xml
file for use by OpenMC
# Instantiate a Materials collection, register all Materials, and export to XML
materials_file = openmc.Materials(materials.values())
# Set the location of the cross sections file to our pre-written set
materials_file.cross_sections = 'c5g7.h5'
materials_file.export_to_xml()
Our next step will be to create the geometry information needed for our assembly and to write that to the geometry.xml
file.
We will begin by defining the surfaces, cells, and universes needed for each of the individual fuel pins, guide tubes, and fission chambers.
# Create the surface used for each pin
pin_surf = openmc.ZCylinder(x0=0, y0=0, R=0.54, name='pin_surf')
# Create the cells which will be used to represent each pin type.
cells = {}
universes = {}
for material in materials.values():
# Create the cell for the material inside the cladding
cells[material.name] = openmc.Cell(name=material.name)
# Assign the half-spaces to the cell
cells[material.name].region = -pin_surf
# Register the material with this cell
cells[material.name].fill = material
# Repeat the above for the material outside the cladding (i.e., the moderator)
cell_name = material.name + '_moderator'
cells[cell_name] = openmc.Cell(name=cell_name)
cells[cell_name].region = +pin_surf
cells[cell_name].fill = materials['water']
# Finally add the two cells we just made to a Universe object
universes[material.name] = openmc.Universe(name=material.name)
universes[material.name].add_cells([cells[material.name], cells[cell_name]])
The next step is to take our universes (representing the different pin types) and lay them out in a lattice to represent the assembly types
lattices = {}
# Instantiate the UO2 Lattice
lattices['UO2 Assembly'] = openmc.RectLattice(name='UO2 Assembly')
lattices['UO2 Assembly'].dimension = [17, 17]
lattices['UO2 Assembly'].lower_left = [-10.71, -10.71]
lattices['UO2 Assembly'].pitch = [1.26, 1.26]
u = universes['uo2']
g = universes['guide_tube']
f = universes['fiss_chamber']
lattices['UO2 Assembly'].universes = \
[[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, u, u, g, u, u, g, u, u, g, u, u, u, u, u],
[u, u, u, g, u, u, u, u, u, u, u, u, u, g, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, g, u, u, g, u, u, g, u, u, g, u, u, g, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, g, u, u, g, u, u, f, u, u, g, u, u, g, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, g, u, u, g, u, u, g, u, u, g, u, u, g, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, g, u, u, u, u, u, u, u, u, u, g, u, u, u],
[u, u, u, u, u, g, u, u, g, u, u, g, u, u, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u],
[u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u]]
# Create a containing cell and universe
cells['UO2 Assembly'] = openmc.Cell(name='UO2 Assembly')
cells['UO2 Assembly'].fill = lattices['UO2 Assembly']
universes['UO2 Assembly'] = openmc.Universe(name='UO2 Assembly')
universes['UO2 Assembly'].add_cell(cells['UO2 Assembly'])
# Instantiate the MOX Lattice
lattices['MOX Assembly'] = openmc.RectLattice(name='MOX Assembly')
lattices['MOX Assembly'].dimension = [17, 17]
lattices['MOX Assembly'].lower_left = [-10.71, -10.71]
lattices['MOX Assembly'].pitch = [1.26, 1.26]
m = universes['mox43']
n = universes['mox7']
o = universes['mox87']
g = universes['guide_tube']
f = universes['fiss_chamber']
lattices['MOX Assembly'].universes = \
[[m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m],
[m, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, m],
[m, n, n, n, n, g, n, n, g, n, n, g, n, n, n, n, m],
[m, n, n, g, n, o, o, o, o, o, o, o, n, g, n, n, m],
[m, n, n, n, o, o, o, o, o, o, o, o, o, n, n, n, m],
[m, n, g, o, o, g, o, o, g, o, o, g, o, o, g, n, m],
[m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
[m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
[m, n, g, o, o, g, o, o, f, o, o, g, o, o, g, n, m],
[m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
[m, n, n, o, o, o, o, o, o, o, o, o, o, o, n, n, m],
[m, n, g, o, o, g, o, o, g, o, o, g, o, o, g, n, m],
[m, n, n, n, o, o, o, o, o, o, o, o, o, n, n, n, m],
[m, n, n, g, n, o, o, o, o, o, o, o, n, g, n, n, m],
[m, n, n, n, n, g, n, n, g, n, n, g, n, n, n, n, m],
[m, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, m],
[m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m, m]]
# Create a containing cell and universe
cells['MOX Assembly'] = openmc.Cell(name='MOX Assembly')
cells['MOX Assembly'].fill = lattices['MOX Assembly']
universes['MOX Assembly'] = openmc.Universe(name='MOX Assembly')
universes['MOX Assembly'].add_cell(cells['MOX Assembly'])
# Instantiate the reflector Lattice
lattices['Reflector Assembly'] = openmc.RectLattice(name='Reflector Assembly')
lattices['Reflector Assembly'].dimension = [1,1]
lattices['Reflector Assembly'].lower_left = [-10.71, -10.71]
lattices['Reflector Assembly'].pitch = [21.42, 21.42]
lattices['Reflector Assembly'].universes = [[universes['water']]]
# Create a containing cell and universe
cells['Reflector Assembly'] = openmc.Cell(name='Reflector Assembly')
cells['Reflector Assembly'].fill = lattices['Reflector Assembly']
universes['Reflector Assembly'] = openmc.Universe(name='Reflector Assembly')
universes['Reflector Assembly'].add_cell(cells['Reflector Assembly'])
Let's now create the core layout in a 3x3 lattice where each lattice position is one of the assemblies we just defined.
After that we can create the final cell to contain the entire core.
lattices['Core'] = openmc.RectLattice(name='3x3 core lattice')
lattices['Core'].dimension= [3, 3]
lattices['Core'].lower_left = [-32.13, -32.13]
lattices['Core'].pitch = [21.42, 21.42]
r = universes['Reflector Assembly']
u = universes['UO2 Assembly']
m = universes['MOX Assembly']
lattices['Core'].universes = [[u, m, r],
[m, u, r],
[r, r, r]]
# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-32.13, boundary_type='reflective')
max_x = openmc.XPlane(x0=+32.13, boundary_type='vacuum')
min_y = openmc.YPlane(y0=-32.13, boundary_type='vacuum')
max_y = openmc.YPlane(y0=+32.13, boundary_type='reflective')
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = lattices['Core']
# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y
# Create root Universe
root_universe = openmc.Universe(name='root universe', universe_id=0)
root_universe.add_cell(root_cell)
Before we commit to the geometry, we should view it using the Python API's plotting capability
root_universe.plot(origin=(0., 0., 0.), width=(3 * 21.42, 3 * 21.42), pixels=(500, 500),
color_by='material')
OK, it looks pretty good, let's go ahead and write the file
# Create Geometry and set root Universe
geometry = openmc.Geometry(root_universe)
# Export to "geometry.xml"
geometry.export_to_xml()
We can now create the tally file information. The tallies will be set up to give us the pin powers in this notebook. We will do this with a mesh filter, with one mesh cell per pin.
tallies_file = openmc.Tallies()
# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
mesh.dimension = [17 * 2, 17 * 2]
mesh.lower_left = [-32.13, -10.71]
mesh.upper_right = [+10.71, +32.13]
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']
# Add tally to collection
tallies_file.append(tally)
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
With the geometry and materials finished, we now just need to define simulation parameters for the settings.xml
file. Note the use of the energy_mode
attribute of our settings_file
object. This is used to tell OpenMC that we intend to run in multi-group mode instead of the default continuous-energy mode. If we didn't specify this but our cross sections file was not a continuous-energy data set, then OpenMC would complain.
This will be a relatively coarse calculation with only 500,000 active histories. A benchmark-fidelity run would of course require many more!
# OpenMC simulation parameters
batches = 150
inactive = 50
particles = 5000
# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
# Tell OpenMC this is a multi-group problem
settings_file.energy_mode = 'multi-group'
# Set the verbosity to 6 so we dont see output for every batch
settings_file.verbosity = 6
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-32.13, -10.71, -1e50, 10.71, 32.13, 1e50]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.source.Source(space=uniform_dist)
# Tell OpenMC we want to run in eigenvalue mode
settings_file.run_mode = 'eigenvalue'
# Export to "settings.xml"
settings_file.export_to_xml()
Let's go ahead and execute the simulation! You'll notice that the output for multi-group mode is exactly the same as for continuous-energy. The differences are all under the hood.
# Run OpenMC
openmc.run()
Results Visualization¶
Now that we have run the simulation, let's look at the fission rate and flux tallies that we tallied.
# Load the last statepoint file and keff value
sp = openmc.StatePoint('statepoint.' + str(batches) + '.h5')
# Get the OpenMC pin power tally data
mesh_tally = sp.get_tally(name='mesh tally')
fission_rates = mesh_tally.get_values(scores=['fission'])
# Reshape array to 2D for plotting
fission_rates.shape = mesh.dimension
# Normalize to the average pin power
fission_rates /= np.mean(fission_rates)
# Force zeros to be NaNs so their values are not included when matplotlib calculates
# the color scale
fission_rates[fission_rates == 0.] = np.nan
# Plot the pin powers and the fluxes
plt.figure()
plt.imshow(fission_rates, interpolation='none', cmap='jet', origin='lower')
plt.colorbar()
plt.title('Pin Powers')
plt.show()
There we have it! We have just successfully run the C5G7 benchmark model!
Multi-Group Mode Part II: MGXS Library Generation With OpenMC¶
The previous Notebook in this series used multi-group mode to perform a calculation with previously defined cross sections. However, in many circumstances the multi-group data is not given and one must instead generate the cross sections for the specific application (or at least verify the use of cross sections from another application).
This Notebook illustrates the use of the openmc.mgxs.Library class specifically for the calculation of MGXS to be used in OpenMC's multi-group mode. This example notebook is therefore very similar to the MGXS Part III notebook, except OpenMC is used as the multi-group solver instead of OpenMOC.
During this process, this notebook will illustrate the following features:
- Calculation of multi-group cross sections for a fuel assembly
- Automated creation and storage of MGXS with openmc.mgxs.Library
- Steady-state pin-by-pin fission rates comparison between continuous-energy and multi-group OpenMC.
- Modification of the scattering data in the library to show the flexibility of the multi-group solver
Generate Input Files¶
import matplotlib.pyplot as plt
import numpy as np
import os
import openmc
%matplotlib inline
We will begin by creating three materials for the fuel, water, and cladding of the fuel pins.
# 1.6% enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_element('U', 1., enrichment=1.6)
fuel.add_element('O', 2.)
# zircaloy
zircaloy = openmc.Material(name='Zircaloy')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_element('Zr', 1.)
# borated water
water = openmc.Material(name='Borated Water')
water.set_density('g/cm3', 0.740582)
water.add_element('H', 4.9457e-2)
water.add_element('O', 2.4732e-2)
water.add_element('B', 8.0042e-6)
With our three materials, we can now create a Materials object that can be exported to an actual XML file.
# Instantiate a Materials object
materials_file = openmc.Materials((fuel, zircaloy, water))
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. This problem will be a square array of fuel pins and control rod guide tubes for which we can use OpenMC's lattice/universe feature. The basic universe will have three regions for the fuel, the clad, and the surrounding coolant. The first step is to create the bounding surfaces for fuel and clad, as well as the outer bounding surfaces of the problem.
# Create cylinders for the fuel and clad
# The x0 and y0 parameters (0. and 0.) are the default values for an
# openmc.ZCylinder object. We could therefore leave them out to no effect
fuel_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.39218)
clad_outer_radius = openmc.ZCylinder(x0=0.0, y0=0.0, R=0.45720)
# Create boundary planes to surround the geometry
min_x = openmc.XPlane(x0=-10.71, boundary_type='reflective')
max_x = openmc.XPlane(x0=+10.71, boundary_type='reflective')
min_y = openmc.YPlane(y0=-10.71, boundary_type='reflective')
max_y = openmc.YPlane(y0=+10.71, boundary_type='reflective')
min_z = openmc.ZPlane(z0=-10., boundary_type='reflective')
max_z = openmc.ZPlane(z0=+10., boundary_type='reflective')
With the surfaces defined, we can now construct a fuel pin cell from cells that are defined by intersections of half-spaces created by the surfaces.
# 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.
# 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.
# 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.
# 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.
# Create root Cell
root_cell = openmc.Cell(name='root cell')
root_cell.fill = assembly
# Add boundary planes
root_cell.region = +min_x & -max_x & +min_y & -max_y & +min_z & -max_z
# Create root Universe
root_universe = openmc.Universe(name='root universe', universe_id=0)
root_universe.add_cell(root_cell)
Before proceeding lets check the geometry.
root_universe.plot(origin=(0., 0., 0.), width=(21.42, 21.42), pixels=(500, 500), color_by='material')
Looks good!
We now must create a geometry that is assigned a root universe and export it to XML.
# Create Geometry and set root universe
geometry = openmc.Geometry(root_universe)
# Export to "geometry.xml"
geometry.export_to_xml()
With the geometry and materials finished, we now just need to define simulation parameters.
# OpenMC simulation parameters
batches = 600
inactive = 50
particles = 2000
# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}
settings_file.run_mode = 'eigenvalue'
settings_file.verbosity = 4
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [-10.71, -10.71, -10, 10.71, 10.71, 10.]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings_file.export_to_xml()
Create an MGXS Library¶
Now we are ready to generate multi-group cross sections! First, let's define a 2-group structure using the built-in EnergyGroups class.
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups([0., 0.625, 20.0e6])
Next, we will instantiate an openmc.mgxs.Library for the energy groups with our the fuel assembly geometry.
# Initialize a 2-group MGXS Library for OpenMC
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = groups
Now, we must specify to the Library which types of cross sections to compute. OpenMC's multi-group mode can accept isotropic flux-weighted cross sections or angle-dependent cross sections, as well as supporting anisotropic scattering represented by either Legendre polynomials, histogram, or tabular angular distributions. We will create the following multi-group cross sections needed to run an OpenMC simulation to verify the accuracy of our cross sections: "total", "absorption", "nu-fission", '"fission", "nu-scatter matrix", "multiplicity matrix", and "chi".
The "multiplicity matrix" type is a relatively rare cross section type. This data is needed to provide OpenMC's multi-group mode with additional information needed to accurately treat scattering multiplication (i.e., (n,xn) reactions)), including how this multiplication varies depending on both incoming and outgoing neutron energies.
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
'nu-scatter matrix', 'multiplicity matrix', 'chi']
Now we must specify the type of domain over which we would like the Library
to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library
supports "material", "cell", "universe", and "mesh" domain types. In this simple example, we wish to compute multi-group cross sections only for each material and therefore will use a "material" domain type.
NOTE: By default, the Library
class will instantiate MGXS
objects for each and every domain (material, cell, universe, or mesh) in the geometry of interest. However, one may specify a subset of these domains to the Library.domains
property.
# Specify a "cell" domain type for the cross section tally filters
mgxs_lib.domain_type = "material"
# Specify the cell domains over which to compute multi-group cross sections
mgxs_lib.domains = geometry.get_all_materials().values()
We will instruct the library to not compute cross sections on a nuclide-by-nuclide basis, and instead to focus on generating material-specific macroscopic cross sections.
NOTE: The default value of the by_nuclide
parameter is False
, so the following step is not necessary but is included for illustrative purposes.
# 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.
# Set the Legendre order to 3 for P3 scattering
mgxs_lib.legendre_order = 3
Now that the Library
has been setup let's verify that it contains the types of cross sections which meet the needs of OpenMC's multi-group solver. Note that this step is done automatically when writing the Multi-Group Library file later in the process (as part of mgxs_lib.write_mg_library()
), but it is a good practice to also run this before spending all the time running OpenMC to generate the cross sections.
If no error is raised, then we have a good set of data.
# Check the library - if no errors are raised, then the library is satisfactory.
mgxs_lib.check_library_for_openmc_mgxs()
Great, now we can use the Library
to construct the tallies needed to compute all of the requested multi-group cross sections in each domain.
# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()
The tallies can now be exported to a "tallies.xml" input file for OpenMC.
NOTE: At this point the Library
has constructed nearly 100 distinct Tally objects. The overhead to tally in OpenMC scales as O(N) for N tallies, which can become a bottleneck for large tally datasets. To compensate for this, the Python API's Tally
, Filter
and Tallies
classes allow for the smart merging of tallies when possible. The Library
class supports this runtime optimization with the use of the optional merge
parameter (False
by default) for the Library.add_to_tallies_file(...)
method, as shown below.
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)
In addition, we instantiate a fission rate mesh tally that we will eventually use to compare with the corresponding multi-group results.
# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.upper_right = [+10.71, +10.71]
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']
# Add tally to collection
tallies_file.append(tally, merge=True)
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
Time to run the calculation and get our results!
# Run OpenMC
openmc.run()
To make sure the results we need are available after running the multi-group calculation, we will now rename the statepoint and summary files.
# Move the statepoint File
ce_spfile = './statepoint_ce.h5'
os.rename('statepoint.' + str(batches) + '.h5', ce_spfile)
# Move the Summary file
ce_sumfile = './summary_ce.h5'
os.rename('summary.h5', ce_sumfile)
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. Let's begin by loading the StatePoint file.
# Load the statepoint file
sp = openmc.StatePoint(ce_spfile, autolink=False)
# Load the summary file in its new location
su = openmc.Summary(ce_sumfile)
sp.link_with_summary(su)
The statepoint is now ready to be analyzed by the Library
. We simply have to load the tallies from the statepoint into the Library
and our MGXS
objects will compute the cross sections for us under-the-hood.
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)
The next step will be to prepare the input for OpenMC to use our newly created multi-group data.
Multi-Group OpenMC Calculation¶
We will now use the Library
to produce a multi-group cross section data set for use by the OpenMC multi-group solver.
Note that since this simulation included so few histories, it is reasonable to expect some data has not had any scores, and thus we could see division by zero errors. This will show up as a runtime warning in the following step. The Library
class is designed to gracefully handle these scenarios.
# Create a MGXS File which can then be written to disk
mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=['fuel', 'zircaloy', 'water'])
# Write the file to disk using the default filename of "mgxs.h5"
mgxs_file.export_to_hdf5()
OpenMC's multi-group mode uses the same input files as does the continuous-energy mode (materials, geometry, settings, plots, and tallies file). Differences would include the use of a flag to tell the code to use multi-group transport, a location of the multi-group library file, and any changes needed in the materials.xml and geometry.xml files to re-define materials as necessary. The materials and geometry file changes could be necessary if materials or their nuclide/element/macroscopic constituents need to be renamed.
In this example we have created macroscopic cross sections (by material), and thus we will need to change the material definitions accordingly.
First we will create the new materials.xml file.
# Re-define our materials to use the multi-group macroscopic data
# instead of the continuous-energy data.
# 1.6% enriched fuel UO2
fuel_mg = openmc.Material(name='UO2')
fuel_mg.add_macroscopic('fuel')
# cladding
zircaloy_mg = openmc.Material(name='Clad')
zircaloy_mg.add_macroscopic('zircaloy')
# moderator
water_mg = openmc.Material(name='Water')
water_mg.add_macroscopic('water')
# Finally, instantiate our Materials object
materials_file = openmc.Materials((fuel_mg, zircaloy_mg, water_mg))
# Set the location of the cross sections file
materials_file.cross_sections = 'mgxs.h5'
# Export to "materials.xml"
materials_file.export_to_xml()
No geometry file neeeds to be written as the continuous-energy file is correctly defined for the multi-group case as well.
Next, we can make the changes we need to the simulation parameters. These changes are limited to telling OpenMC to run a multi-group vice contrinuous-energy calculation.
# Set the energy mode
settings_file.energy_mode = 'multi-group'
# Export to "settings.xml"
settings_file.export_to_xml()
Lets clear the tallies file so it doesn't include tallies for re-generating a multi-group library, but then put back in a tally for the fission mesh.
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
# Add fission and flux mesh to tally for plotting using the same mesh we've already defined
mesh_tally = openmc.Tally(name='mesh tally')
mesh_tally.filters = [openmc.MeshFilter(mesh)]
mesh_tally.scores = ['fission']
tallies_file.add_tally(mesh_tally)
# Export to "tallies.xml"
tallies_file.export_to_xml()
Before running the calculation let's visually compare a subset of the newly-generated multi-group cross section data to the continuous-energy data. We will do this using the cross section plotting functionality built-in to the OpenMC Python API.
# First lets plot the fuel data
# We will first add the continuous-energy data
fig = openmc.plot_xs(fuel, ['total'])
# We will now add in the corresponding multi-group data and show the result
openmc.plot_xs(fuel_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()
# Then repeat for the zircaloy data
fig = openmc.plot_xs(zircaloy, ['total'])
openmc.plot_xs(zircaloy_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()
# And finally repeat for the water data
fig = openmc.plot_xs(water, ['total'])
openmc.plot_xs(water_mg, ['total'], plot_CE=False, mg_cross_sections='mgxs.h5', axis=fig.axes[0])
fig.axes[0].legend().set_visible(False)
plt.show()
plt.close()
At this point, the problem is set up and we can run the multi-group calculation.
# Run the Multi-Group OpenMC Simulation
openmc.run()
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.
# Move the StatePoint File
mg_spfile = './statepoint_mg.h5'
os.rename('statepoint.' + str(batches) + '.h5', mg_spfile)
# Move the Summary file
mg_sumfile = './summary_mg.h5'
os.rename('summary.h5', mg_sumfile)
# Rename and then load the last statepoint file and keff value
mgsp = openmc.StatePoint(mg_spfile, autolink=False)
# Load the summary file in its new location
mgsu = openmc.Summary(mg_sumfile)
mgsp.link_with_summary(mgsu)
# Get keff
mg_keff = mgsp.k_combined
Next, we can load the continuous-energy eigenvalue for comparison.
ce_keff = sp.k_combined
Lets compare the two eigenvalues, including their bias
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))
This shows a small but nontrivial pcm bias between the two methods. Some degree of mismatch is expected simply to the very few histories being used in these example problems. An additional mismatch is always inherent in the practical application of multi-group theory due to the high degree of approximations inherent in that method.
Pin Power Visualizations¶
Next we will visualize the pin power results obtained from both the Continuous-Energy and Multi-Group OpenMC calculations.
First, we extract volume-integrated fission rates from the Multi-Group calculation's mesh fission rate tally for each pin cell in the fuel assembly.
# Get the OpenMC fission rate mesh tally data
mg_mesh_tally = mgsp.get_tally(name='mesh tally')
mg_fission_rates = mg_mesh_tally.get_values(scores=['fission'])
# Reshape array to 2D for plotting
mg_fission_rates.shape = (17,17)
# Normalize to the average pin power
mg_fission_rates /= np.mean(mg_fission_rates)
We can now do the same for the Continuous-Energy results.
# 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.
# 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')
These figures really indicate that more histories are probably necessary when trying to achieve a fully converged solution, but hey, this is good enough for our example!
Scattering Anisotropy Treatments¶
We will next show how we can work with the scattering angular distributions. OpenMC's MG solver has the capability to use group-to-group angular distributions which are represented as any of the following: a truncated Legendre series of up to the 10th order, a histogram distribution, and a tabular distribution. Any combination of these representations can be used by OpenMC during the transport process, so long as all constituents of a given material use the same representation. This means it is possible to have water represented by a tabular distribution and fuel represented by a Legendre if so desired.
Note: To have the highest runtime performance OpenMC natively converts Legendre series to a tabular distribution before the transport begins. This default functionality can be turned off with the tabular_legendre
element of the settings.xml
file (or for the Python API, the openmc.Settings.tabular_legendre
attribute).
This section will examine the following:
- Re-run the MG-mode calculation with P0 scattering everywhere using the
openmc.Settings.max_order
attribute - Re-run the problem with only the water represented with P3 scattering and P0 scattering for the remaining materials using the Python API's ability to convert between formats.
Global P0 Scattering¶
First we begin by re-running with P0 scattering (i.e., isotropic) everywhere. If a global maximum order is requested, the most effective way to do this is to use the max_order
attribute of our openmc.Settings
object.
# Set the maximum scattering order to 0 (i.e., isotropic scattering)
settings_file.max_order = 0
# Export to "settings.xml"
settings_file.export_to_xml()
Now we can re-run OpenMC to obtain our results
# Run the Multi-Group OpenMC Simulation
openmc.run()
And then get the eigenvalue differences from the Continuous-Energy and P3 MG solution
# Move the statepoint File
mgp0_spfile = './statepoint_mg_p0.h5'
os.rename('statepoint.' + str(batches) + '.h5', mgp0_spfile)
# Move the Summary file
mgp0_sumfile = './summary_mg_p0.h5'
os.rename('summary.h5', mgp0_sumfile)
# Load the last statepoint file and keff value
mgsp_p0 = openmc.StatePoint(mgp0_spfile, autolink=False)
# Get keff
mg_p0_keff = mgsp_p0.k_combined
bias_p0 = 1.0E5 * (ce_keff[0] - mg_p0_keff[0])
print('P3 bias [pcm]: {0:1.1f}'.format(bias))
print('P0 bias [pcm]: {0:1.1f}'.format(bias_p0))
Mixed Scattering Representations¶
OpenMC's Multi-Group mode also includes a feature where not every data in the library is required to have the same scattering treatment. For example, we could represent the water with P3 scattering, and the fuel and cladding with P0 scattering. This series will show how this can be done.
First we will convert the data to P0 scattering, unless its water, then we will leave that as P3 data.
# Convert the zircaloy and fuel data to P0 scattering
for i, xsdata in enumerate(mgxs_file.xsdatas):
if xsdata.name != 'water':
mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('legendre', 0)
We can also use whatever scattering format that we want for the materials in the library. As an example, we will take this P0 data and convert zircaloy to a histogram anisotropic scattering format and the fuel to a tabular anisotropic scattering format
# Convert the formats as discussed
for i, xsdata in enumerate(mgxs_file.xsdatas):
if xsdata.name == 'zircaloy':
mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('histogram', 2)
elif xsdata.name == 'fuel':
mgxs_file.xsdatas[i] = xsdata.convert_scatter_format('tabular', 2)
mgxs_file.export_to_hdf5('mgxs.h5')
Finally we will re-set our max_order
parameter of our openmc.Settings
object to our maximum order so that OpenMC will use whatever scattering data is available in the library.
After we do this we can re-run the simulation.
settings_file.max_order = None
# Export to "settings.xml"
settings_file.export_to_xml()
# Run the Multi-Group OpenMC Simulation
openmc.run()
For a final step we can again obtain the eigenvalue differences from this case and compare with the same from the P3 MG solution
# Load the last statepoint file and keff value
mgsp_mixed = openmc.StatePoint('./statepoint.' + str(batches) + '.h5')
mg_mixed_keff = mgsp_mixed.k_combined
bias_mixed = 1.0E5 * (ce_keff[0] - mg_mixed_keff[0])
print('P3 bias [pcm]: {0:1.1f}'.format(bias))
print('Mixed Scattering bias [pcm]: {0:1.1f}'.format(bias_mixed))
Our tests in this section showed the flexibility of data formatting within OpenMC's multi-group mode: every material can be represented with its own format with the approximations that make the most sense. Now, as you'll see above, the runtimes from our P3, P0, and mixed cases are not significantly different and therefore this might not be a useful strategy for multi-group Monte Carlo. However, this capability provides a useful benchmark for the accuracy hit one may expect due to these scattering approximations before implementing this generality in a deterministic solver where the runtime savings are more significant.
NOTE: The biases obtained above with P3, P0, and mixed representations do not necessarily reflect the inherent accuracies of the options. These cases were not run with a sufficient number of histories to truly differentiate methods improvement from statistical noise.
Multi-Group Mode Part III: Advanced Feature Showcase¶
This Notebook illustrates the use of the the more advanced features of OpenMC's multi-group mode and the openmc.mgxs.Library class. During this process, this notebook will illustrate the following features:
- Calculation of multi-group cross sections for a simplified BWR 8x8 assembly with isotropic and angle-dependent MGXS.
- Automated creation and storage of MGXS with openmc.mgxs.Library
- Fission rate comparison between continuous-energy and the two multi-group OpenMC cases.
To avoid focusing on unimportant details, the BWR assembly in this notebook is greatly simplified. The descriptions which follow will point out some areas of simplification.
Generate Input Files¶
import os
import matplotlib.pyplot as plt
import numpy as np
import openmc
%matplotlib inline
We will be running a rodded 8x8 assembly with Gadolinia fuel pins. Let's create all the elemental data we would need for this case.
# Instantiate some elements
elements = {}
for elem in ['H', 'O', 'U', 'Zr', 'Gd', 'B', 'C', 'Fe']:
elements[elem] = openmc.Element(elem)
With the elements we defined, we will now create the materials we will use later.
Material Definition Simplifications:
- This model will be run at room temperature so the NNDC ENDF-B/VII.1 data set can be used but the water density will be representative of a module with around 20% voiding. This water density will be non-physically used in all regions of the problem.
- Steel is composed of more than just iron, but we will only treat it as such here.
materials = {}
# Fuel
materials['Fuel'] = openmc.Material(name='Fuel')
materials['Fuel'].set_density('g/cm3', 10.32)
materials['Fuel'].add_element(elements['O'], 2)
materials['Fuel'].add_element(elements['U'], 1, enrichment=3.)
# Gadolinia bearing fuel
materials['Gad'] = openmc.Material(name='Gad')
materials['Gad'].set_density('g/cm3', 10.23)
materials['Gad'].add_element(elements['O'], 2)
materials['Gad'].add_element(elements['U'], 1, enrichment=3.)
materials['Gad'].add_element(elements['Gd'], .02)
# Zircaloy
materials['Zirc2'] = openmc.Material(name='Zirc2')
materials['Zirc2'].set_density('g/cm3', 6.55)
materials['Zirc2'].add_element(elements['Zr'], 1)
# Boiling Water
materials['Water'] = openmc.Material(name='Water')
materials['Water'].set_density('g/cm3', 0.6)
materials['Water'].add_element(elements['H'], 2)
materials['Water'].add_element(elements['O'], 1)
# Boron Carbide for the Control Rods
materials['B4C'] = openmc.Material(name='B4C')
materials['B4C'].set_density('g/cm3', 0.7 * 2.52)
materials['B4C'].add_element(elements['B'], 4)
materials['B4C'].add_element(elements['C'], 1)
# Steel
materials['Steel'] = openmc.Material(name='Steel')
materials['Steel'].set_density('g/cm3', 7.75)
materials['Steel'].add_element(elements['Fe'], 1)
We can now create a Materials object that can be exported to an actual XML file.
# Instantiate a Materials object
materials_file = openmc.Materials(materials.values())
# Export to "materials.xml"
materials_file.export_to_xml()
Now let's move on to the geometry. The first step is to define some constants which will be used to set our dimensions and then we can start creating the surfaces and regions for the problem, the 8x8 lattice, the rods and the control blade.
Before proceeding let's discuss some simplifications made to the problem geometry:
- To enable the use of an equal-width mesh for running the multi-group calculations, the intra-assembly gap was increased to the same size as the pitch of the 8x8 fuel lattice
- The can is neglected
- The pin-in-water geometry for the control blade is ignored and instead the blade is a solid block of B4C
- Rounded corners are ignored
- There is no cladding for the water rod
# Set constants for the problem and assembly dimensions
fuel_rad = 0.53213
clad_rad = 0.61341
Np = 8
pin_pitch = 1.6256
length = float(Np + 2) * pin_pitch
assembly_width = length - 2. * pin_pitch
rod_thick = 0.47752 / 2. + 0.14224
rod_span = 7. * pin_pitch
surfaces = {}
# Create boundary planes to surround the geometry
surfaces['Global x-'] = openmc.XPlane(x0=0., boundary_type='reflective')
surfaces['Global x+'] = openmc.XPlane(x0=length, boundary_type='reflective')
surfaces['Global y-'] = openmc.YPlane(y0=0., boundary_type='reflective')
surfaces['Global y+'] = openmc.YPlane(y0=length, boundary_type='reflective')
# Create cylinders for the fuel and clad
surfaces['Fuel Radius'] = openmc.ZCylinder(R=fuel_rad)
surfaces['Clad Radius'] = openmc.ZCylinder(R=clad_rad)
surfaces['Assembly x-'] = openmc.XPlane(x0=pin_pitch)
surfaces['Assembly x+'] = openmc.XPlane(x0=length - pin_pitch)
surfaces['Assembly y-'] = openmc.YPlane(y0=pin_pitch)
surfaces['Assembly y+'] = openmc.YPlane(y0=length - pin_pitch)
# Set surfaces for the control blades
surfaces['Top Blade y-'] = openmc.YPlane(y0=length - rod_thick)
surfaces['Top Blade x-'] = openmc.XPlane(x0=pin_pitch)
surfaces['Top Blade x+'] = openmc.XPlane(x0=rod_span)
surfaces['Left Blade x+'] = openmc.XPlane(x0=rod_thick)
surfaces['Left Blade y-'] = openmc.YPlane(y0=length - rod_span)
surfaces['Left Blade y+'] = openmc.YPlane(y0=9. * pin_pitch)
With the surfaces defined, we can now construct regions with these surfaces before we use those to create cells
# Set regions for geometry building
regions = {}
regions['Global'] = \
(+surfaces['Global x-'] & -surfaces['Global x+'] &
+surfaces['Global y-'] & -surfaces['Global y+'])
regions['Assembly'] = \
(+surfaces['Assembly x-'] & -surfaces['Assembly x+'] &
+surfaces['Assembly y-'] & -surfaces['Assembly y+'])
regions['Fuel'] = -surfaces['Fuel Radius']
regions['Clad'] = +surfaces['Fuel Radius'] & -surfaces['Clad Radius']
regions['Water'] = +surfaces['Clad Radius']
regions['Top Blade'] = \
(+surfaces['Top Blade y-'] & -surfaces['Global y+']) & \
(+surfaces['Top Blade x-'] & -surfaces['Top Blade x+'])
regions['Top Steel'] = \
(+surfaces['Global x-'] & -surfaces['Top Blade x-']) & \
(+surfaces['Top Blade y-'] & -surfaces['Global y+'])
regions['Left Blade'] = \
(+surfaces['Left Blade y-'] & -surfaces['Left Blade y+']) & \
(+surfaces['Global x-'] & -surfaces['Left Blade x+'])
regions['Left Steel'] = \
(+surfaces['Left Blade y+'] & -surfaces['Top Blade y-']) & \
(+surfaces['Global x-'] & -surfaces['Left Blade x+'])
regions['Corner Blade'] = \
regions['Left Steel'] | regions['Top Steel']
regions['Water Fill'] = \
regions['Global'] & ~regions['Assembly'] & \
~regions['Top Blade'] & ~regions['Left Blade'] &\
~regions['Corner Blade']
We will begin building the 8x8 assembly. To do that we will have to build the cells and universe for each pin type (fuel, gadolinia-fuel, and water).
universes = {}
cells = {}
for name, mat, in zip(['Fuel Pin', 'Gd Pin'],
[materials['Fuel'], materials['Gad']]):
universes[name] = openmc.Universe(name=name)
cells[name] = openmc.Cell(name=name)
cells[name].fill = mat
cells[name].region = regions['Fuel']
universes[name].add_cell(cells[name])
cells[name + ' Clad'] = openmc.Cell(name=name + ' Clad')
cells[name + ' Clad'].fill = materials['Zirc2']
cells[name + ' Clad'].region = regions['Clad']
universes[name].add_cell(cells[name + ' Clad'])
cells[name + ' Water'] = openmc.Cell(name=name + ' Water')
cells[name + ' Water'].fill = materials['Water']
cells[name + ' Water'].region = regions['Water']
universes[name].add_cell(cells[name + ' Water'])
universes['Hole'] = openmc.Universe(name='Hole')
cells['Hole'] = openmc.Cell(name='Hole')
cells['Hole'].fill = materials['Water']
universes['Hole'].add_cell(cells['Hole'])
Let's use this pin information to create our 8x8 assembly.
# Create fuel assembly Lattice
universes['Assembly'] = openmc.RectLattice(name='Assembly')
universes['Assembly'].pitch = (pin_pitch, pin_pitch)
universes['Assembly'].lower_left = [pin_pitch, pin_pitch]
f = universes['Fuel Pin']
g = universes['Gd Pin']
h = universes['Hole']
lattices = [[f, f, f, f, f, f, f, f],
[f, f, f, f, f, f, f, f],
[f, f, f, g, f, g, f, f],
[f, f, g, h, h, f, g, f],
[f, f, f, h, h, f, f, f],
[f, f, g, f, f, f, g, f],
[f, f, f, g, f, g, f, f],
[f, f, f, f, f, f, f, f]]
# Store the array of lattice universes
universes['Assembly'].universes = lattices
cells['Assembly'] = openmc.Cell(name='Assembly')
cells['Assembly'].fill = universes['Assembly']
cells['Assembly'].region = regions['Assembly']
So far we have the rods and water within the assembly , but we still need the control blade and the water which fills the rest of the space. We will create those cells now
# The top portion of the blade, poisoned with B4C
cells['Top Blade'] = openmc.Cell(name='Top Blade')
cells['Top Blade'].fill = materials['B4C']
cells['Top Blade'].region = regions['Top Blade']
# The left portion of the blade, poisoned with B4C
cells['Left Blade'] = openmc.Cell(name='Left Blade')
cells['Left Blade'].fill = materials['B4C']
cells['Left Blade'].region = regions['Left Blade']
# The top-left corner portion of the blade, with no poison
cells['Corner Blade'] = openmc.Cell(name='Corner Blade')
cells['Corner Blade'].fill = materials['Steel']
cells['Corner Blade'].region = regions['Corner Blade']
# Water surrounding all other cells and our assembly
cells['Water Fill'] = openmc.Cell(name='Water Fill')
cells['Water Fill'].fill = materials['Water']
cells['Water Fill'].region = regions['Water Fill']
OpenMC requires that there is a "root" universe. Let us create our root universe and fill it with the cells just defined.
# Create root Universe
universes['Root'] = openmc.Universe(name='root universe', universe_id=0)
universes['Root'].add_cells([cells['Assembly'], cells['Top Blade'],
cells['Corner Blade'], cells['Left Blade'],
cells['Water Fill']])
What do you do after you create your model? Check it! We will use the plotting capabilities of the Python API to do this for us.
When doing so, we will coloring by material with fuel being red, gadolinia-fuel as yellow, zirc cladding as a light grey, water as blue, B4C as black and steel as a darker gray.
universes['Root'].plot(origin=(length / 2., length / 2., 0.),
pixels=(500, 500), width=(length, length),
color_by='material',
colors={materials['Fuel']: (1., 0., 0.),
materials['Gad']: (1., 1., 0.),
materials['Zirc2']: (0.5, 0.5, 0.5),
materials['Water']: (0.0, 0.0, 1.0),
materials['B4C']: (0.0, 0.0, 0.0),
materials['Steel']: (0.4, 0.4, 0.4)})
Looks pretty good to us!
We now must create a geometry that is assigned a root universe and export it to XML.
# Create Geometry and set root universe
geometry = openmc.Geometry(universes['Root'])
# Export to "geometry.xml"
geometry.export_to_xml()
With the geometry and materials finished, we now just need to define simulation parameters, including how to run the model and what we want to learn from the model (i.e., define the tallies). We will start with our simulation parameters in the next block.
This will include setting the run strategy, telling OpenMC not to bother creating a tallies.out
file, and limiting the verbosity of our output to just the header and results to not clog up our notebook with results from each batch.
# OpenMC simulation parameters
batches = 1000
inactive = 20
particles = 1000
# Instantiate a Settings object
settings_file = openmc.Settings()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
settings_file.output = {'tallies': False}
settings_file.verbosity = 4
# Create an initial uniform spatial source distribution over fissionable zones
bounds = [pin_pitch, pin_pitch, 10, length - pin_pitch, length - pin_pitch, 10]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True)
settings_file.source = openmc.source.Source(space=uniform_dist)
# Export to "settings.xml"
settings_file.export_to_xml()
Create an MGXS Library¶
Now we are ready to generate multi-group cross sections! First, let's define a 2-group structure using the built-in EnergyGroups class.
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625, 20.0e6])
Next, we will instantiate an openmc.mgxs.Library for the energy groups with our the problem geometry. This library will use the default setting of isotropically-weighting the multi-group cross sections.
# Initialize a 2-group Isotropic MGXS Library for OpenMC
iso_mgxs_lib = openmc.mgxs.Library(geometry)
iso_mgxs_lib.energy_groups = groups
Now, we must specify to the Library which types of cross sections to compute. OpenMC's multi-group mode can accept isotropic flux-weighted cross sections or angle-dependent cross sections, as well as supporting anisotropic scattering represented by either Legendre polynomials, histogram, or tabular angular distributions.
Just like before, we will create the following multi-group cross sections needed to run an OpenMC simulation to verify the accuracy of our cross sections: "total", "absorption", "nu-fission", '"fission", "nu-scatter matrix", "multiplicity matrix", and "chi". "multiplicity matrix" is needed to provide OpenMC's multi-group mode with additional information needed to accurately treat scattering multiplication (i.e., (n,xn) reactions)) explicitly.
# Specify multi-group cross section types to compute
iso_mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
'nu-scatter matrix', 'multiplicity matrix', 'chi']
Now we must specify the type of domain over which we would like the Library
to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library
supports "material" "cell", "universe", and "mesh" domain types.
For the sake of example we will use a mesh to gather our cross sections. This mesh will be set up so there is one mesh bin for every pin cell.
# Instantiate a tally Mesh
mesh = openmc.Mesh()
mesh.type = 'regular'
mesh.dimension = [10, 10]
mesh.lower_left = [0., 0.]
mesh.upper_right = [length, length]
# Specify a "mesh" domain type for the cross section tally filters
iso_mgxs_lib.domain_type = "mesh"
# Specify the mesh over which to compute multi-group cross sections
iso_mgxs_lib.domains = [mesh]
Now we will set the scattering treatment that we wish to use.
In the mg-mode-part-ii notebook, the cross sections were generated with a typical P3 scattering expansion in mind. Now, however, we will use a more advanced technique: OpenMC will directly provide us a histogram of the change-in-angle (i.e., $\mu$) distribution.
Where as in the mg-mode-part-ii notebook, all that was required was to set the legendre_order
attribute of mgxs_lib
, here we have only slightly more work: we have to tell the Library that we want to use a histogram distribution (as it is not the default), and then tell it the number of bins.
For this problem we will use 11 bins.
# Set the scattering format to histogram and then define the number of bins
# Avoid a warning that corrections don't make sense with histogram data
iso_mgxs_lib.correction = None
# Set the histogram data
iso_mgxs_lib.scatter_format = 'histogram'
iso_mgxs_lib.histogram_bins = 11
Ok, we made our isotropic library with histogram-scattering!
Now why don't we go ahead and create a library to do the same, but with angle-dependent MGXS. That is, we will avoid making the isotropic flux weighting approximation and instead just store a cross section for every polar and azimuthal angle pair.
To do this with the Python API and OpenMC, all we have to do is set the number of polar and azimuthal bins. Here we only need to set the number of bins, the API will convert all of angular space into equal-width bins for us.
Since this problem is symmetric in the z-direction, we only need to concern ourselves with the azimuthal variation here. We will use eight angles.
Ok, we will repeat all the above steps for a new library object, but will also set the number of azimuthal bins at the end.
# Let's repeat all of the above for an angular MGXS library so we can gather
# that in the same continuous-energy calculation
angle_mgxs_lib = openmc.mgxs.Library(geometry)
angle_mgxs_lib.energy_groups = groups
angle_mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission',
'nu-scatter matrix', 'multiplicity matrix', 'chi']
angle_mgxs_lib.domain_type = "mesh"
angle_mgxs_lib.domains = [mesh]
angle_mgxs_lib.correction = None
angle_mgxs_lib.scatter_format = 'histogram'
angle_mgxs_lib.histogram_bins = 11
# Set the angular bins to 8
angle_mgxs_lib.num_azimuthal = 8
Now that our libraries have been setup, let's make sure they contain the types of cross sections which meet the needs of OpenMC's multi-group solver. Note that this step is done automatically when writing the Multi-Group Library file later in the process (as part of the mgxs_lib.write_mg_library()
), but it is a good practice to also run this before spending all the time running OpenMC to generate the cross sections.
# Check the libraries - if no errors are raised, then the library is satisfactory.
iso_mgxs_lib.check_library_for_openmc_mgxs()
angle_mgxs_lib.check_library_for_openmc_mgxs()
Lastly, we use our two Library
objects to construct the tallies needed to compute all of the requested multi-group cross sections in each domain.
We expect a warning here telling us that the default Legendre order is not meaningful since we are using histogram scattering.
# Construct all tallies needed for the multi-group cross section library
iso_mgxs_lib.build_library()
angle_mgxs_lib.build_library()
The tallies within the libraries can now be exported to a "tallies.xml" input file for OpenMC.
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
iso_mgxs_lib.add_to_tallies_file(tallies_file, merge=True)
angle_mgxs_lib.add_to_tallies_file(tallies_file, merge=True)
In addition, we instantiate a fission rate mesh tally for eventual comparison of results.
# Instantiate tally Filter
mesh_filter = openmc.MeshFilter(mesh)
# Instantiate the Tally
tally = openmc.Tally(name='mesh tally')
tally.filters = [mesh_filter]
tally.scores = ['fission']
# Add tally to collection
tallies_file.append(tally, merge=True)
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
Time to run the calculation and get our results!
# Run OpenMC
openmc.run()
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.
# Move the StatePoint File
ce_spfile = './statepoint_ce.h5'
os.rename('statepoint.' + str(batches) + '.h5', ce_spfile)
# Move the Summary file
ce_sumfile = './summary_ce.h5'
os.rename('summary.h5', ce_sumfile)
Tally Data Processing¶
Our simulation ran successfully and created statepoint and summary output files. Let's begin by loading the StatePoint file, but not automatically linking the summary file.
# Load the statepoint file, but not the summary file, as it is a different filename than expected.
sp = openmc.StatePoint(ce_spfile, autolink=False)
In addition to the statepoint file, our simulation also created a summary file which encapsulates information about the materials and geometry. This is necessary for the openmc.Library
to properly process the tally data. We first create a Summary
object and link it with the statepoint. Normally this would not need to be performed, but since we have renamed our summary file to avoid conflicts with the Multi-Group calculation's summary file, we will load this in explicitly.
su = openmc.Summary(ce_sumfile)
sp.link_with_summary(su)
The statepoint is now ready to be analyzed. To create our libraries we simply have to load the tallies from the statepoint into each Library
and our MGXS
objects will compute the cross sections for us under-the-hood.
# Initialize MGXS Library with OpenMC statepoint data
iso_mgxs_lib.load_from_statepoint(sp)
angle_mgxs_lib.load_from_statepoint(sp)
The next step will be to prepare the input for OpenMC to use our newly created multi-group data.
Isotropic Multi-Group OpenMC Calculation¶
We will now use the Library
to produce the isotropic multi-group cross section data set for use by the OpenMC multi-group solver.
If the model to be run in multi-group mode is the same as the continuous-energy mode, the openmc.mgxs.Library
class has the ability to directly create the multi-group geometry, materials, and multi-group library for us.
Note that this feature is only useful if the MG model is intended to replicate the CE geometry - it is not useful if the CE library is not the same geometry (like it would be for generating MGXS from a generic spectral region).
This method creates and assigns the materials automatically, including creating a geometry which is equivalent to our mesh cells for which the cross sections were derived.
# Allow the API to create our Library, materials, and geometry file
iso_mgxs_file, materials_file, geometry_file = iso_mgxs_lib.create_mg_mode()
# Tell the materials file what we want to call the multi-group library
materials_file.cross_sections = 'mgxs.h5'
# Write our newly-created files to disk
iso_mgxs_file.export_to_hdf5('mgxs.h5')
materials_file.export_to_xml()
geometry_file.export_to_xml()
Next, we can make the changes we need to the settings file. These changes are limited to telling OpenMC to run a multi-group calculation and provide the location of our multi-group cross section file.
# Set the energy mode
settings_file.energy_mode = 'multi-group'
# Export to "settings.xml"
settings_file.export_to_xml()
Let's clear up the tallies file so it doesn't include all the extra tallies for re-generating a multi-group library
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
# Add our fission rate mesh tally
tallies_file.add_tally(tally)
# Export to "tallies.xml"
tallies_file.export_to_xml()
Before running the calculation let's look at our meshed model. It might not be interesting, but let's take a look anyways.
geometry_file.root_universe.plot(origin=(length / 2., length / 2., 0.),
pixels=(300, 300), width=(length, length),
color_by='material')
So, we see a 10x10 grid with a different color for every material, sounds good!
At this point, the problem is set up and we can run the multi-group calculation.
# Execute the Isotropic MG OpenMC Run
openmc.run()
Before we go the angle-dependent case, let's save the StatePoint and Summary files so they don't get over-written
# Move the StatePoint File
iso_mg_spfile = './statepoint_mg_iso.h5'
os.rename('statepoint.' + str(batches) + '.h5', iso_mg_spfile)
# Move the Summary file
iso_mg_sumfile = './summary_mg_iso.h5'
os.rename('summary.h5', iso_mg_sumfile)
Angle-Dependent Multi-Group OpenMC Calculation¶
Let's now run the calculation with the angle-dependent multi-group cross sections. This process will be the exact same as above, except this time we will use the angle-dependent Library as our starting point.
We do not need to re-write the materials, geometry, or tallies file to disk since they are the same as for the isotropic case.
# Let's repeat for the angle-dependent case
angle_mgxs_lib.load_from_statepoint(sp)
angle_mgxs_file, materials_file, geometry_file = angle_mgxs_lib.create_mg_mode()
angle_mgxs_file.export_to_hdf5()
At this point, the problem is set up and we can run the multi-group calculation.
# Execute the angle-dependent OpenMC Run
openmc.run()
Results Comparison¶
In this section we will compare the eigenvalues and fission rate distributions of the continuous-energy, isotropic multi-group and angle-dependent multi-group cases.
We will begin by loading the multi-group statepoint files, first the isotropic, then angle-dependent. The angle-dependent was not renamed, so we can autolink its summary.
# Load the isotropic statepoint file
iso_mgsp = openmc.StatePoint(iso_mg_spfile, autolink=False)
iso_mgsum = openmc.Summary(iso_mg_sumfile)
iso_mgsp.link_with_summary(iso_mgsum)
# Load the angle-dependent statepoint file
angle_mgsp = openmc.StatePoint('statepoint.' + str(batches) + '.h5')
Eigenvalue Comparison¶
Next, we can load the eigenvalues for comparison and do that comparison
ce_keff = sp.k_combined
iso_mg_keff = iso_mgsp.k_combined
angle_mg_keff = angle_mgsp.k_combined
# Find eigenvalue bias
iso_bias = 1.0E5 * (ce_keff[0] - iso_mg_keff[0])
angle_bias = 1.0E5 * (ce_keff[0] - angle_mg_keff[0])
Let's compare the eigenvalues in units of pcm
print('Isotropic to CE Bias [pcm]: {0:1.1f}'.format(iso_bias))
print('Angle to CE Bias [pcm]: {0:1.1f}'.format(angle_bias))
We see a large reduction in error by switching to the usage of angle-dependent multi-group cross sections!
Of course, this rodded and partially voided BWR problem was chosen specifically to exacerbate the angular variation of the reaction rates (and thus cross sections). Such improvements should not be expected in every case, especially if localized absorbers are not present.
It is important to note that both eigenvalues can be improved by the application of finer geometric or energetic discretizations, but this shows that the angle discretization may be a factor for consideration.
Fission Rate Distribution Comparison¶
Next we will visualize the mesh tally results obtained from our three cases.
This will be performed by first obtaining the one-group fission rate tally information from our state point files. After we have this information we will re-shape the data to match the original mesh laydown. We will then normalize, and finally create side-by-side plots of all.
sp_files = [sp, iso_mgsp, angle_mgsp]
titles = ['Continuous-Energy', 'Isotropic Multi-Group',
'Angle-Dependent Multi-Group']
fiss_rates = []
fig = plt.figure(figsize=(12, 6))
for i, (case, title) in enumerate(zip(sp_files, titles)):
# Get our mesh tally information
mesh_tally = case.get_tally(name='mesh tally')
fiss_rates.append(mesh_tally.get_values(scores=['fission']))
# Reshape the array
fiss_rates[-1].shape = mesh.dimension
# Normalize the fission rates
fiss_rates[-1] /= np.mean(fiss_rates[-1])
# Set 0s to NaNs so they show as white
fiss_rates[-1][fiss_rates[-1] == 0.] = np.nan
fig = plt.subplot(1, len(titles), i + 1)
# Plot only the fueled regions
plt.imshow(fiss_rates[-1][1:-1, 1:-1], cmap='jet', origin='lower',
vmin=0.4, vmax=4.)
plt.title(title + '\nFission Rates')
With this colormap, dark blue is the lowest power and dark red is the highest power.
We see general agreement between the fission rate distributions, but it looks like there may be less of a gradient near the rods in the continuous-energy and angle-dependent MGXS cases than in the isotropic MGXS case.
To better see the differences, let's plot ratios of the fission powers for our two multi-group cases compared to the continuous-energy case t
# Calculate and plot the ratios of MG to CE for each of the 2 MG cases
ratios = []
fig, axes = plt.subplots(figsize=(12, 6), nrows=1, ncols=2)
for i, (case, title, axis) in enumerate(zip(sp_files[1:], titles[1:], axes.flat)):
# Get our ratio relative to the CE (in fiss_ratios[0])
ratios.append(np.divide(fiss_rates[i + 1], fiss_rates[0]))
# Plot only the fueled regions
im = axis.imshow(ratios[-1][1:-1, 1:-1], cmap='bwr', origin='lower',
vmin = 0.9, vmax = 1.1)
axis.set_title(title + '\nFission Rates Relative\nto Continuous-Energy')
# Add a color bar
fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
fig.colorbar(im, cax=cbar_ax)
With this ratio its clear that the errors are significantly worse in the isotropic case. These errors are conveniently located right where the most anisotropy is espected: by the control blades and by the Gd-bearing pins!
Release Notes for OpenMC 0.9.0¶
This release of OpenMC is the first release to use a new native HDF5 cross
section format rather than ACE format cross sections. Other significant new
features include a nuclear data interface in the Python API (openmc.data
)
a stochastic volume calculation capability, a random sphere packing algorithm
that can handle packing fractions up to 60%, and a new XML parser with
significantly better performance than the parser used previously.
Caution
With the new cross section format, the default energy units are now electronvolts (eV) rather than megaelectronvolts (MeV)! If you are specifying an energy filter for a tally, make sure you use units of eV now.
The Python API continues to improve over time; several backwards incompatible changes were made in the API which users of previous versions should take note of:
Each type of tally filter is now specified with a separate class. For example:
energy_filter = openmc.EnergyFilter([0.0, 0.625, 4.0, 1.0e6, 20.0e6])
Several attributes of the
Plot
class have changed (color
->color_by
andcol_spec
>colors
).Plot.colors
now accepts a dictionary mappingCell
orMaterial
instances to RGB 3-tuples or string colors names, e.g.:plot.colors = { fuel: 'yellow', water: 'blue' }
make_hexagon_region
is nowget_hexagonal_prism()
Several changes in
Settings
attributes:weight
is now set asSettings.cutoff['weight']
- Shannon entropy is now specified by passing a
openmc.Mesh
toSettings.entropy_mesh
- Uniform fission site method is now specified by passing a
openmc.Mesh
toSettings.ufs_mesh
- All
sourcepoint_*
options are now specified in aSettings.sourcepoint
dictionary - Resonance scattering method is now specified as a dictionary in
Settings.resonance_scattering
- Multipole is now turned on by setting
Settings.temperature['multipole'] = True
- The
output_path
attribute is nowSettings.output['path']
All the
openmc.mgxs.Nu*
classes are gone. Instead, anu
argument was added to the constructor of the corresponding classes.
System Requirements¶
There are no special requirements for running the OpenMC code. As of this release, OpenMC has been tested on a variety of Linux distributions and Mac OS X. Numerous users have reported working builds on Microsoft Windows, but your mileage may vary. Memory requirements will vary depending on the size of the problem at hand (mostly on the number of nuclides and tallies in the problem).
New Features¶
- Stochastic volume calculations
- Multi-delayed group cross section generation
- Ability to calculate multi-group cross sections over meshes
- Temperature interpolation on cross section data
- Nuclear data interface in Python API,
openmc.data
- Allow cutoff energy via
Settings.cutoff
- Ability to define fuel by enrichment (see
Material.add_element()
) - Random sphere packing for TRISO particle generation,
openmc.model.pack_trisos()
- Critical eigenvalue search,
openmc.search_for_keff()
- Model container,
openmc.model.Model
- In-line plotting in Jupyter,
openmc.plot_inline()
- Energy function tally filters,
openmc.EnergyFunctionFilter
- Replaced FoX XML parser with pugixml
- Cell/material instance counting,
Geometry.determine_paths()
- Differential tallies (see
openmc.TallyDerivative
) - Consistent multi-group scattering matrices
- Improved documentation and new Jupyter notebooks
- OpenMOC compatibility module,
openmc.openmoc_compatible
Bug Fixes¶
- c5df6c: Fix mesh filter max iterator check
- 1cfa39: Reject external source only if 95% of sites are rejected
- 335359: Fix bug in plotting meshlines
- 17c678: Make sure system_clock uses high-resolution timer
- 23ec0b: Fix use of S(a,b) with multipole data
- 7eefb7: Fix several bugs in tally module
- 7880d4: Allow plotting calculation with no boundary conditions
- ad2d9f: Fix filter weight missing when scoring all nuclides
- 59fdca: Fix use of source files for fixed source calculations
- 9eff5b: Fix thermal scattering bugs
- 7848a9: Fix combined k-eff estimator producing NaN
- f139ce: Fix printing bug for tallies with AggregateNuclide
- b8ddfa: Bugfix for short tracks near tally mesh edges
- ec3cfb: Fix inconsistency in filter weights
- 5e9b06: Fix XML representation for verbosity
- c39990: Fix bug tallying reaction rates with multipole on
- c6b67e: Fix fissionable source sampling bug
- 489540: Check for void materials in tracklength tallies
- f0214f: Fixes/improvements to the ARES algorithm
Contributors¶
This release contains new contributions from the following people:
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:
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:
The particle’s properties are initialized from a source site previously sampled.
Based on the particle’s coordinates, the current cell in which the particle resides is determined.
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.
The distance to the nearest boundary of the particle’s cell is determined based on the bounding surfaces to the cell.
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)\).
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.
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.
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).
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
By subtracting the right-hand term from both sides of equation (1), we can then write the surface equation for the sphere:
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.
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.
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 | 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:
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
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
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
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
Expanding equation (7) and rearranging terms, we obtain
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\):
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
Let us define \(\bar{x} = x - x_0\), \(\bar{y} = y - y_0\), and \(\bar{z} = z - z_0\). We then have
Expanding equation (11) and rearranging terms, we obtain
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\):
If the discriminant \(k^2 - c < 0\), this means that both solutions to the quadratic are complex. In physical terms, this means that the ray along which the particle is traveling does not make any intersections with the sphere.
If we do have intersections and \(c < 0\), this means that the particle is inside the sphere. Thus, one solution should be positive and one should be negative. The positive distance will occur when the sign on the square root of the discriminant is positive. If we have intersections but \(c > 0\) this means that the particle is outside the sphere. The solutions to the quadratic will then be either both positive or both negative. If they are both positive, the smaller (closer) one will be the solution with a negative sign on the square root of the discriminant.
Cone Parallel to an Axis¶
The equation for a cone parallel to, for example, the x-axis is \((y - y_0)^2 + (z - z_0)^2 = R^2(x - x_0)^2\). Thus, we need to solve \((y + dv - y_0)^2 + (z + dw - z_0)^2 = R^2(x + du - x_0)^2\). Let us define \(\bar{x} = x - x_0\), \(\bar{y} = y - y_0\), and \(\bar{z} = z - z_0\). We then have
Expanding equation (14) and rearranging terms, we obtain
Defining the terms
we then have the simple quadratic equation \(ad^2 + 2kd + c = 0\) which can be solved as described in Cylinder Parallel to an Axis.
General Quadric¶
The equation for a general quadric surface is \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\). Thus, we need to solve the equation
Expanding equation (17) and rearranging terms, we obtain
Defining the terms
we then have the simple quadratic equation \(ad^2 + 2kd + c = 0\) which can be solved as described in Cylinder Parallel to an Axis.
Finding a Cell Given a Point¶
Another basic algorithm is to determine which cell contains a given point in the global coordinate system, i.e. if the particle’s position is \((x,y,z)\), what cell is it currently in. This is done in the following manner in OpenMC. With the possibility of multiple levels of coordinates, we must perform a recursive search for the cell. First, we start in the highest (most global) universe, which we call the base universe, and loop over each cell within that universe. For each cell, we check whether the specified point is inside the cell using the algorithm described in Finding a Lattice Tile. If the cell is filled with a normal material, the search is done and we have identified the cell containing the point. If the cell is filled with another universe, we then search all cells within that universe to see if any of them contain the specified point. If the cell is filled with a lattice, the position within the lattice is determined, and then whatever universe fills that lattice position is recursively searched. The search ends once a cell containing a normal material is found that contains the specified point.
Finding a Lattice Tile¶
If a particle is inside a lattice, its position inside the lattice must be determined before assigning it to a cell. Throughout this section, the volumetric units of the lattice will be referred to as “tiles”. Tiles are identified by thier indices, and the process of discovering which tile contains the particle is referred to as “indexing”.
Rectilinear Lattice Indexing¶
Indices are assigned to tiles in a rectilinear lattice based on the tile’s position along the \(x\), \(y\), and \(z\) axes. Figure 7 maps the indices for a 2D lattice. The indices, (1, 1), map to the lower-left tile. (5, 1) and (5, 5) map to the lower-right and upper-right tiles, respectively.
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:
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.
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:
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.
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
In order to determine if a point is inside the cell, we would substitute its coordinates into equation (23). If the inequalities are satisfied, than the point is indeed inside the cell.
Handling Surface Crossings¶
A particle will cross a surface if the distance to the nearest surface is closer than the distance sampled to the next collision. A number of things happen when a particle hits a surface. First, we need to check if a non-transmissive boundary condition has been applied to the surface. If a vacuum boundary condition has been applied, the particle is killed and any surface current tallies are scored to as needed. If a reflective boundary condition has been applied to the surface, surface current tallies are scored to and then the particle’s direction is changed according to the procedure in Reflective Boundary Conditions.
Next, we need to determine what cell is beyond the surface in the direction of travel of the particle so that we can evaluate cross sections based on its material properties. At initialization, a list of neighboring cells is created for each surface in the problem as described in Building Neighbor Lists. The algorithm outlined in Finding a Cell Given a Point is used to find a cell containing the particle with one minor modification; rather than searching all cells in the base universe, only the list of neighboring cells is searched. If this search is unsuccessful, then a search is done over every cell in the base universe.
Building Neighbor Lists¶
After the geometry has been loaded and stored in memory from an input file, OpenMC builds a list for each surface containing any cells that are bounded by that surface in order to speed up processing of surface crossings. The algorithm to build these lists is as follows. First, we loop over all cells in the geometry and count up how many times each surface appears in a specification as bounding a negative half-space and bounding a positive half-space. Two arrays are then allocated for each surface, one that lists each cell that contains the negative half-space of the surface and one that lists each cell that contains the positive half-space of the surface. Another loop is performed over all cells and the neighbor lists are populated for each surface.
Reflective Boundary Conditions¶
If the velocity of a particle is \(\mathbf{v}\) and it crosses a surface of the form \(f(x,y,z) = 0\) with a reflective boundary condition, it can be shown based on geometric arguments that the velocity vector will then become
where \(\hat{\mathbf{n}}\) is a unit vector normal to the surface at the point of the surface crossing. The rationale for this can be understood by noting that \((\mathbf{v} \cdot \hat{\mathbf{n}}) \hat{\mathbf{n}}\) is the projection of the velocity vector onto the normal vector. By subtracting two times this projection, the velocity is reflected with respect to the surface normal. Since the magnitude of the velocity of the particle will not change as it undergoes reflection, we can work with the direction of the particle instead, simplifying equation (24) to
where \(\mathbf{v} = || \mathbf{v} || \mathbf{\Omega}\). The direction of the surface normal will be the gradient of the surface at the point of crossing, i.e. \(\mathbf{n} = \nabla f(x,y,z)\). Substituting this into equation (25), we get
If we write the initial and final directions in terms of their vector components, \(\mathbf{\Omega} = (u,v,w)\) and \(\mathbf{\Omega'} = (u', v', w')\), this allows us to represent equation (25) as a series of equations:
One can then use equation (27) to develop equations for transforming a particle’s direction given the equation of the surface.
Plane Perpendicular to an Axis¶
For a plane that is perpendicular to an axis, the rule for reflection is almost so simple that no derivation is needed at all. Nevertheless, we will proceed with the derivation to confirm that the rules of geometry agree with our intuition. The gradient of the surface \(f(x,y,z) = x - x_0 = 0\) is simply \(\nabla f = (1, 0, 0)\). Note that this vector is already normalized, i.e. \(|| \nabla f || = 1\). The second two equations in (27) tell us that \(v\) and \(w\) do not change and the first tell us that
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
Substituting equation (29) into equation (27) gives us the form of the solution. For example, the x-component of the reflected direction will be
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
where we have introduced the constants \(\bar{y}\) and \(\bar{z}\). Taking the square of the norm of the gradient, we find that
This implies that
Substituting equations (33) and (31) into equation (27) gives us the form of the solution. In this case, the x-component will not change. The y- and z-components of the reflected direction will be
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
where we have introduced the constants \(\bar{x}, \bar{y}, \bar{z}\). Taking the square of the norm of the gradient, we find that
This implies that
Substituting equations (37) and (35) into equation (27) gives us the form of the solution:
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
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
This implies that
Substituting equations (41) and (39) into equation (27) gives us the form of the solution:
General Quadric¶
A general quadric surface has the form \(f(x,y,z) = Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\). Thus, the gradient to the surface is
Cross Section Representations¶
Continuous-Energy Data¶
The data governing the interaction of neutrons with various nuclei for continous-energy problems are represented using the ACE format which is used by MCNP and Serpent. ACE-format data can be generated with the NJOY nuclear data processing system which converts raw ENDF/B data into linearly-interpolable data as required by most Monte Carlo codes. The use of a standard cross section format allows for a direct comparison of OpenMC with other codes since the same cross section libraries can be used.
The ACE format contains continuous-energy cross sections for the following types of reactions: elastic scattering, fission (or first-chance fission, second-chance fission, etc.), inelastic scattering, \((n,xn)\), \((n,\gamma)\), and various other absorption reactions. For those reactions with one or more neutrons in the exit channel, secondary angle and energy distributions may be provided. In addition, fissionable nuclides have total, prompt, and/or delayed \(\nu\) as a function of energy and neutron precursor distributions. Many nuclides also have probability tables to be used for accurate treatment of self-shielding in the unresolved resonance range. For bound scatterers, separate tables with \(S(\alpha,\beta,T)\) scattering law data can be used.
Energy Grid Methods¶
The method by which continuous energy cross sections for each nuclide in a problem are stored as a function of energy can have a substantial effect on the performance of a Monte Carlo simulation. Since the ACE format is based on linearly-interpolable cross sections, each nuclide has cross sections tabulated over a wide range of energies. Some nuclides may only have a few points tabulated (e.g. H-1) whereas other nuclides may have hundreds or thousands of points tabulated (e.g. U-238).
At each collision, it is necessary to sample the probability of having a particular type of interaction whether it be elastic scattering, \((n,2n)\), level inelastic scattering, etc. This requires looking up the microscopic cross sections for these reactions for each nuclide within the target material. Since each nuclide has a unique energy grid, it would be necessary to search for the appropriate index for each nuclide at every collision. This can become a very time-consuming process, especially if there are many nuclides in a problem as there would be for burnup calculations. Thus, there is a strong motive to implement a method of reducing the number of energy grid searches in order to speed up the calculation.
Logarithmic Mapping¶
To speed up energy grid searches, OpenMC uses a logarithmic mapping technique to limit the range of energies that must be searched for each nuclide. The entire energy range is divided up into equal-lethargy segments, and the bounding energies of each segment are mapped to bounding indices on each of the nuclide energy grids. By default, OpenMC uses 8000 equal-lethargy segments as recommended by Brown.
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:
Assuming free-gas thermal motion, cross sections in the multipole form can be analytically Doppler broadened to give the form:
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:
The \(W_i\) integral simplifies down to an analytic form. We define the Faddeeva function, \(W\) as:
Through this, the integral transforms as follows:
There are freely available algorithms to evaluate the Faddeeva function. For many nuclides, the Faddeeva function needs to be evaluated thousands of times to calculate a cross section. To mitigate that computational cost, the WMP method only evaluates poles within a certain energy “window” around the incident neutron energy and accounts for the effect of resonances outside that window with a polynomial fit. This polynomial fit is then broadened exactly. This exact broadening can make up for the removal of the \(C\) integral, as typically at low energies, only curve fits are used.
Note that the implementation of WMP in OpenMC currently assumes that inelastic scattering does not occur in the resolved resonance region. This is usually, but not always the case. Future library versions may eliminate this issue.
The data format used by OpenMC to represent windowed multipole data is specified in Windowed Multipole Library Format.
Temperature Treatment¶
At the beginning of a simulation, OpenMC collects a list of all temperatures that are present in a model. It then uses this list to determine what cross sections to load. The data that is loaded depends on what temperature method has been selected. There are three methods available:
Nearest: | Cross sections are loaded only if they are within a specified tolerance of the actual temperatures in the model. |
---|---|
Interpolation: | Cross sections are loaded at temperatures that bound the actual temperatures in the model. During transport, cross sections for each material are calculated using statistical linear-linear interpolation between bounding temperature. Suppose cross sections are available at temperatures \(T_1, T_2, ..., T_n\) and a material is assigned a temperature \(T\) where \(T_i < T < T_{i+1}\). Statistical interpolation is applied as follows: a uniformly-distributed random number of the unit interval, \(\xi\), is sampled. If \(\xi < (T - T_i)/(T_{i+1} - T_i)\), then cross sections at temperature \(T_{i+1}\) are used. Otherwise, cross sections at \(T_i\) are used. This procedure is applied for pointwise cross sections in the resolved resonance range, unresolved resonance probability tables, and \(S(\alpha,\beta)\) thermal scattering tables. |
Multipole: | Resolved resonance cross sections are calculated on-the-fly using techniques/data described in Windowed Multipole Representation. Cross section data is loaded for a single temperature and is used in the unresolved resonance and fast energy ranges. |
Multi-Group Data¶
The data governing the interaction of particles with various nuclei or materials are represented using a multi-group library format specific to the OpenMC code. The format is described in the MGXS Library Specification. The data itself can be prepared via traditional paths or directly from a continuous-energy OpenMC calculation by use of the Python API as is shown in the Multi-Group Mode Part I: Introduction example notebook. This multi-group library consists of meta-data (such as the energy group structure) and multiple xsdata objects which contains the required microscopic or macroscopic multi-group data.
At a minimum, the library must contain the absorption cross section (\(\sigma_{a,g}\)) and a scattering matrix. If the problem is an eigenvalue problem then all fissionable materials must also contain either a fission production matrix cross section (\(\nu\sigma_{f,g\rightarrow g'}\)), or both the fission spectrum data (\(\chi_{g'}\)) and a fission production cross section (\(\nu\sigma_{f,g}\)), or, . The library must also contain the fission cross section (\(\sigma_{f,g}\)) or the fission energy release cross section (\(\kappa\sigma_{f,g}\)) if the associated tallies are required by the model using the library.
After a scattering collision, the outgoing particle experiences a change in both energy and angle. The probability of a particle resulting in a given outgoing energy group (g’) given a certain incoming energy group (g) is provided by the scattering matrix data. The angular information can be expressed either via Legendre expansion of the particle’s change-in-angle (\(\mu\)), a tabular representation of the probability distribution function of \(\mu\), or a histogram representation of the same PDF. The formats used to represent these are described in the MGXS Library Specification.
Unlike the continuous-energy mode, the multi-group mode does not explicitly track particles produced from scattering multiplication (i.e., \((n,xn)\)) reactions. These are instead accounted for by adjusting the weight of the particle after the collision such that the correct total weight is maintained. The weight adjustment factor is optionally provided by the multiplicity data which is required to be provided in the form of a group-wise matrix. This data is provided as a group-wise matrix since the probability of producing multiple particles in a scattering reaction depends on both the incoming energy, g, and the sampled outgoing energy, g’. This data represents the average number of particles emitted from a scattering reaction, given a scattering reaction has occurred:
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:
The above method is the same as is usually done with most deterministic solvers. Note that this method is less accurate than using the scattering multiplication weight adjustment since simply reducing the absorption cross section does not include any information about the outgoing energy of the particles produced in these reactions.
All of the data discussed in this section can be provided to the code independent of the particle’s direction of motion (i.e., isotropic), or the data can be provided as a tabular distribution of the polar and azimuthal particle direction angles. The isotropic representation is the most commonly used, however inaccuracies are to be expected especially near material interfaces where a material has a very large cross sections relative to the other material (as can be expected in the resonance range). The angular representation can be used to minimize this error.
Finally, the above options for representing the physics do not have to be consistent across the problem. The number of groups and the structure, however, does have to be consistent across the data sets. That is to say that each microscopic or macroscopic data set does not have to apply the same scattering expansion, treatment of multiplicity or angular representation of the cross sections. This allows flexibility for the model to use highly anisotropic scattering information in the water while the fuel can be simulated with linear or even isotropic scattering.
Random Number Generation¶
In order to sample probability distributions, one must be able to produce random numbers. The standard technique to do this is to generate numbers on the interval \([0,1)\) from a deterministic sequence that has properties that make it appear to be random, e.g. being uniformly distributed and not exhibiting correlation between successive terms. Since the numbers produced this way are not truly “random” in a strict sense, they are typically referred to as pseudorandom numbers, and the techniques used to generate them are pseudorandom number generators (PRNGs). Numbers sampled on the unit interval can then be transformed for the purpose of sampling other continuous or discrete probability distributions.
Linear Congruential Generators¶
There are a great number of algorithms for generating random numbers. One of the simplest and commonly used algorithms is called a linear congruential generator. We start with a random number seed \(\xi_0\) and a sequence of random numbers can then be generated using the following recurrence relation:
where \(g\), \(c\), and \(M\) are constants. The choice of these constants will have a profound effect on the quality and performance of the generator, so they should not be chosen arbitrarily. As Donald Knuth stated in his seminal work The Art of Computer Programming, “random numbers should not be generated with a method chosen at random. Some theory should be used.” Typically, \(M\) is chosen to be a power of two as this enables \(x \mod M\) to be performed using the bitwise AND operator with a bit mask. The constants for the linear congruential generator used by default in OpenMC are \(g = 2806196910506780709\), \(c = 1\), and \(M = 2^{63}\) (see L’Ecuyer).
Skip-ahead Capability¶
One of the important capabilities for a random number generator is to be able to skip ahead in the sequence of random numbers. Without this capability, it would be very difficult to maintain reproducibility in a parallel calculation. If we want to skip ahead \(N\) random numbers and \(N\) is large, the cost of sampling \(N\) random numbers to get to that position may be prohibitively expensive. Fortunately, algorithms have been developed that allow us to skip ahead in \(O(\log_2 N)\) operations instead of \(O(N)\). One algorithm to do so is described in a paper by Brown. This algorithm relies on the following relationship:
Note that (2) has the same general form as eqref{eq:lcg}, so the idea is to determine the new multiplicative and additive constants in \(O(\log_2 N)\) operations.
References
Physics¶
There are limited differences between physics treatments used in the continuous-energy and multi-group modes. If distinctions are necessary, each of the following sections will provide an explanation of the differences. Otherwise, replacing any references of the particle’s energy (E) with references to the particle’s energy group (g) will suffice.
Sampling Distance to Next Collision¶
As a particle travels through a homogeneous material, the probability distribution function for the distance to its next collision \(\ell\) is
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
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:
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
\((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
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
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:
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
At low energies, elastic scattering will be isotropic in the center-of-mass system, but for higher energies, there may be p-wave and higher order scattering that leads to anisotropic scattering. Thus, in general, we need to sample a cosine of the scattering angle which we will refer to as \(\mu\). For elastic scattering, the secondary angle distribution is always given in the center-of-mass system and is sampled according to the procedure outlined in Sampling Angular Distributions. After the cosine of the angle of scattering has been sampled, we need to determine the neutron’s new direction \(\mathbf{\Omega}'_n\) in the center-of-mass system. This is done with the procedure in Transforming a Particle’s Coordinates. The new direction is multiplied by the speed of the neutron in the center-of-mass system to obtain the new velocity vector in the center-of-mass:
Finally, we transform the velocity in the center-of-mass system back to lab coordinates:
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
However, equation (11) is only valid if the target was at rest. When the target nucleus does have thermal motion, the cosine of the scattering angle can be determined by simply taking the dot product of the neutron’s initial and final direction in the lab system.
Inelastic Scattering¶
Note that the multi-group mode makes no distinction between elastic or inelastic scattering reactions. The spceific multi-group scattering implementation is discussed in the Multi-Group Scattering section.
The major algorithms for inelastic scattering were described in previous sections. First, a scattering cosine is sampled using the algorithms in Sampling Angular Distributions. Then an outgoing energy is sampled using the algorithms in Sampling Energy Distributions. If the outgoing energy and scattering cosine were given in the center-of-mass system, they are transformed to laboratory coordinates using the algorithm described in Transforming a Particle’s Coordinates. Finally, the direction of the particle is changed also using the procedure in Transforming a Particle’s Coordinates.
Although inelastic scattering leaves the target nucleus in an excited state, no secondary photons from nuclear de-excitation are tracked in OpenMC.
\((n,xn)\) Reactions¶
Note that the multi-group mode makes no distinction between elastic or inelastic scattering reactions. The specific multi-group scattering implementation is discussed in the Multi-Group Scattering section.
These types of reactions are just treated as inelastic scattering and as such are subject to the same procedure as described in Inelastic Scattering. For reactions with integral multiplicity, e.g., \((n,2n)\), an appropriate number of secondary neutrons are created. For reactions that have a multiplicity given as a function of the incoming neutron energy (which occasionally occurs for MT=5), the weight of the outgoing neutron is multiplied by the multiplicity.
Multi-Group Scattering¶
In multi-group mode, a scattering collision requires that the outgoing energy group of the simulated particle be selected from a probability distribution, the change-in-angle selected from a probability distribution according to the outgoing energy group, and finally the particle’s weight adjusted again according to the outgoing energy group.
The first step in selecting an outgoing energy group for a particle in a given incoming energy group is to select a random number (\(\xi\)) between 0 and 1. This number is then compared to the cumulative distribution function produced from the outgoing group (g’) data for the given incoming group (g):
If the scattering data is represented as a Legendre expansion, then the value of \(\Sigma_{s,g \rightarrow g'}\) above is the 0th order forthe given group transfer. If the data is provided as tabular or histogram data, then \(\Sigma_{s,g \rightarrow g'}\) is the sum of all bins of data for a given g and g’ pair.
Now that the outgoing energy is known the change-in-angle, \(\mu\) can be determined. If the data is provided as a Legendre expansion, this is done by rejection sampling of the probability distribution represented by the Legendre series. For efficiency, the selected values of the PDF (\(f(\mu)\)) are chosen to be between 0 and the maximum value of \(f(\mu)\) in the domain of -1 to 1. Note that this sampling scheme automatically forces negative values of the \(f(\mu)\) probability distribution function to be treated as zero probabilities.
If the angular data is instead provided as a tabular representation, then the value of \(\mu\) is selected as described in the Tabular Angular Distribution section with a linear-linear interpolation scheme.
If the angular data is provided as a histogram representation, then the value of \(\mu\) is selected in a similar fashion to that described for the selection of the outgoing energy (since the energy group representation is simply a histogram representation) except the CDF is composed of the angular bins and not the energy groups. However, since we are interested in a specific value of \(\mu\) instead of a group, then an angle selected from a uniform distribution within from the chosen angular bin.
The final step in the scattering treatment is to adjust the weight of the neutron to account for any production of neutrons due to \((n,xn)\) reactions. This data is obtained from the multiplicity data provided in the multi-group cross section library for the material of interest. The scaled value will default to 1.0 if no value is provided in the library.
Fission¶
While fission is normally considered an absorption reaction, as far as it concerns a Monte Carlo simulation it actually bears more similarities to inelastic scattering since fission results in secondary neutrons in the exit channel. Other absorption reactions like \((n,\gamma)\) or \((n,\alpha)\), on the contrary, produce no neutrons. There are a few other idiosyncrasies in treating fission. In an eigenvalue calculation, secondary neutrons from fission are only “banked” for use in the next generation rather than being tracked as secondary neutrons from elastic and inelastic scattering would be. On top of this, fission is sometimes broken into first-chance fission, second-chance fission, etc. The nuclear data file either lists the partial fission reactions with secondary energy distributions for each one, or a total fission reaction with a single secondary energy distribution.
When a fission reaction is sampled in OpenMC (either total fission or, if data exists, first- or second-chance fission), the following algorithm is used to create and store fission sites for the following generation. First, the average number of prompt and delayed neutrons must be determined to decide whether the secondary neutrons will be prompt or delayed. This is important because delayed neutrons have a markedly different spectrum from prompt neutrons, one that has a lower average energy of emission. The total number of neutrons emitted \(\nu_t\) is given as a function of incident energy in the ENDF format. Two representations exist for \(\nu_t\). The first is a polynomial of order \(N\) with coefficients \(c_0,c_1,\dots,c_N\). If \(\nu_t\) has this format, we can evaluate it at incoming energy \(E\) by using the equation
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
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
where \(w\) is the statistical weight and \(k_{eff}\) is the effective multiplication factor from the previous generation. The number of neutrons produced is biased in this manner so that the expected number of fission neutrons produced is the number of source particles that we started with in the generation. Since \(\nu\) is not an integer, we use the following procedure to obtain an integral number of fission neutrons to produce. If \(\xi > \nu - \lfloor \nu \rfloor\), then we produce \(\lfloor \nu \rfloor\) neutrons. Otherwise, we produce \(\lfloor \nu \rfloor + 1\) neutrons. Then, for each fission site produced, we sample the outgoing angle and energy according to the algorithms given in Sampling Angular Distributions and Sampling Energy Distributions respectively. If the neutron is to be born delayed, then there is an extra step of sampling a delayed neutron precursor group since they each have an associated secondary energy distribution.
The sampled outgoing angle and energy of fission neutrons along with the position of the collision site are stored in an array called the fission bank. In a subsequent generation, these fission bank sites are used as starting source sites.
The above description is similar for the multi-group mode except the data are provided as group-wise data instead of in a continuous-energy format. In this case, the outgoing energy of the fission neutrons are represented as histograms by way of either the nu-fission matrix or chi vector.
Secondary Angle-Energy Distributions¶
Note that this section is specific to continuous-energy mode since the multi-group scattering process has already been described including the secondary energy and angle sampling.
For a reaction with secondary products, it is necessary to determine the outgoing angle and energy of the products. For any reaction other than elastic and level inelastic scattering, the outgoing energy must be determined based on tabulated or parameterized data. The ENDF-6 Format specifies a variety of ways that the secondary energy distribution can be represented. ENDF File 5 contains uncorrelated energy distribution whereas ENDF File 6 contains correlated energy-angle distributions. The ACE format specifies its own representations based loosely on the formats given in ENDF-6. OpenMC’s HDF5 nuclear data files use a combination of ENDF and ACE distributions; in this section, we will describe how the outgoing angle and energy of secondary particles are sampled.
One of the subtleties in the nuclear data format is the fact that a single reaction product can have multiple angle-energy distributions. This is mainly useful for reactions with multiple products of the same type in the exit channel such as \((n,2n)\) or \((n,3n)\). In these types of reactions, each neutron is emitted corresponding to a different excitation level of the compound nucleus, and thus in general the neutrons will originate from different energy distributions. If multiple angle-energy distributions are present, they are assigned incoming-energy-dependent probabilities that can then be used to randomly select one.
Once a distribution has been selected, the procedure for determining the outgoing angle and energy will depend on the type of the distribution.
Product Angle-Energy Distributions¶
If the secondary distribution for a product was given in file 6 in ENDF, the angle and energy are correlated with one another and cannot be sampled separately. Several representations exist in ENDF/ACE for correlated angle-energy distributions.
N-Body Phase Space Distribution¶
Reactions in which there are more than two products of similar masses are sometimes best treated by using what’s known as an N-body phase distribution. This distribution has the following probability density function for outgoing energy and angle of the \(i\)-th particle in the center-of-mass system:
where \(n\) is the number of outgoing particles, \(C_n\) is a normalization constant, \(E_i^{max}\) is the maximum center-of-mass energy for particle \(i\), and \(E'\) is the outgoing energy. We see in equation (47) that the angle is simply isotropic in the center-of-mass system. The algorithm for sampling the outgoing energy is based on algorithms R28, C45, and C64 in the Monte Carlo Sampler. First we calculate the maximum energy in the center-of-mass using the following equation:
where \(A_p\) is the total mass of the outgoing particles in neutron masses, \(A\) is the mass of the original target nucleus in neutron masses, and \(Q\) is the Q-value of the reaction. Next we sample a value \(x\) from a Maxwell distribution with a nuclear temperature of one using the algorithm outlined in Maxwell Fission Spectrum. We then need to determine a value \(y\) that will depend on how many outgoing particles there are. For \(n = 3\), we simply sample another Maxwell distribution with unity nuclear temperature. For \(n = 4\), we use the equation
where \(\xi_i\) are random numbers sampled on the interval \([0,1)\). For \(n = 5\), we use the equation
After \(x\) and \(y\) have been determined, the outgoing energy is then calculated as
There are two important notes to make regarding the N-body phase space distribution. First, the documentation (and code) for MCNP5-1.60 has a mistake in the algorithm for \(n = 4\). That being said, there are no existing nuclear data evaluations which use an N-body phase space distribution with \(n = 4\), so the error would not affect any calculations. In the ENDF/B-VII.1 nuclear data evaluation, only one reaction uses an N-body phase space distribution at all, the \((n,2n)\) reaction with H-2.
Transforming a Particle’s Coordinates¶
Since all the multi-group data exists in the laboratory frame of reference, this section does not apply to the multi-group mode.
Once the cosine of the scattering angle \(\mu\) has been sampled either from a angle distribution or a correlated angle-energy distribution, we are still left with the task of transforming the particle’s coordinates. If the outgoing energy and scattering cosine were given in the center-of-mass system, then we first need to transform these into the laboratory system. The relationship between the outgoing energy in center-of-mass and laboratory is
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
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
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
where \(v_n\) is the magnitude of the velocity of the neutron, \(\bar{\sigma}\) is an effective cross section, \(T\) is the temperature of the target material, \(\mathbf{v}_T\) is the velocity of the target nucleus, \(v_r = || \mathbf{v}_n - \mathbf{v}_T ||\) is the magnitude of the relative velocity, \(\sigma\) is the cross section at 0 K, and \(M (\mathbf{v}_T)\) is the probability distribution for the target nucleus velocity at temperature \(T\) (a Maxwellian). In a Monte Carlo code, one must account for the effect of the thermal motion on both the integrated cross section as well as secondary angle and energy distributions. For integrated cross sections, it is possible to calculate thermally-averaged cross sections by applying a kernel Doppler broadening algorithm to data at 0 K (or some temperature lower than the desired temperature). The most ubiquitous algorithm for this purpose is the SIGMA1 method developed by Red Cullen and subsequently refined by others. This method is used in the NJOY and PREPRO data processing codes.
The effect of thermal motion on secondary angle and energy distributions can be accounted for on-the-fly in a Monte Carlo simulation. We must first qualify where it is actually used however. All threshold reactions are treated as being independent of temperature, and therefore they are not Doppler broadened in NJOY and no special procedure is used to adjust the secondary angle and energy distributions. The only non-threshold reactions with secondary neutrons are elastic scattering and fission. For fission, it is assumed that the neutrons are emitted isotropically (this is not strictly true, but is nevertheless a good approximation). This leaves only elastic scattering that needs a special thermal treatment for secondary distributions.
Fortunately, it is possible to directly sample the velocity of the target nuclide and then use it directly in the kinematic calculations. However, this calculation is a bit more nuanced than it might seem at first glance. One might be tempted to simply sample a Maxwellian distribution for the velocity of the target nuclide. Careful inspection of equation (55) however tells us that target velocities that produce relative velocities which correspond to high cross sections will have a greater contribution to the effective reaction rate. This is most important when the velocity of the incoming neutron is close to a resonance. For example, if the neutron’s velocity corresponds to a trough in a resonance elastic scattering cross section, a very small target velocity can cause the relative velocity to correspond to the peak of the resonance, thus making a disproportionate contribution to the reaction rate. The conclusion is that if we are to sample a target velocity in the Monte Carlo code, it must be done in such a way that preserves the thermally-averaged reaction rate as per equation (55).
The method by which most Monte Carlo codes sample the target velocity for use in elastic scattering kinematics is outlined in detail by [Gelbard]. The derivation here largely follows that of Gelbard. Let us first write the reaction rate as a function of the velocity of the target nucleus:
where \(R\) is the reaction rate. Note that this is just the right-hand side of equation (55). Based on the discussion above, we want to construct a probability distribution function for sampling the target velocity to preserve the reaction rate – this is different from the overall probability distribution function for the target velocity, \(M ( \mathbf{v}_T )\). This probability distribution function can be found by integrating equation (56) to obtain a normalization factor:
Let us call the normalization factor in the denominator of equation (57) \(C\).
Constant Cross Section Model¶
It is often assumed that \(\sigma (v_r)\) is constant over the range of relative velocities of interest. This is a good assumption for almost all cases since the elastic scattering cross section varies slowly with velocity for light nuclei, and for heavy nuclei where large variations can occur due to resonance scattering, the moderating effect is rather small. Nonetheless, this assumption may cause incorrect answers in systems with low-lying resonances that can cause a significant amount of up-scatter that would be ignored by this assumption (e.g. U-238 in commercial light-water reactors). We will revisit this assumption later in Energy-Dependent Cross Section Model. For now, continuing with the assumption, we write \(\sigma (v_r) = \sigma_s\) which simplifies (57) to
The Maxwellian distribution in velocity is
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
Let us define the Maxwellian distribution in speed as
To simplify things a bit, we’ll define a parameter
Substituting equation (62) into equation (61), we obtain
Now, changing variables in equation (58) by using the result from equation (61), our new probability distribution function is
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
Thus, we can infer that
Inserting equation (66) into (64), we obtain
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:
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
and accepting it with probability
The reason for dividing and multiplying the terms by \(v_n + v_T\) is to ensure that the first term is bounded. In general, \(|| \mathbf{v}_n - \mathbf{v}_T ||\) can take on arbitrarily large values, but if we divide it by its maximum value \(v_n + v_T\), then it ensures that the function will be bounded. We now must come up with a sampling scheme for equation (69). To determine \(q(v_T)\), we need to integrate \(f_2\) in equation (68). Doing so we find that
Thus, we need to sample the probability distribution function
Now, let us do a change of variables with the following definitions
Substituting equation (73) into equation (72) along with \(dx = \beta dv_T\) and doing some crafty rearranging of terms yields
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
then we sample the probability distribution \(2x^3 e^{-x^2}\) for \(x\) using rule C49 in the Monte Carlo Sampler which we can then use to determine the speed of the target nucleus \(v_T\) from equation (73). Otherwise, we sample the probability distribution \(\frac{4}{\sqrt{\pi}} x^2 e^{-x^2}\) for \(x\) using rule C61 in the Monte Carlo Sampler.
With a target speed sampled, we must then decide whether to accept it based on the probability in equation (70). The cosine can be sampled isotropically as \(\mu = 2\xi_2 - 1\) where \(\xi_2\) is a random number on the unit interval. Since the maximum value of \(f_1(v_T, \mu)\) is \(4\sigma_s / \sqrt{\pi} C'\), we then sample another random number \(\xi_3\) and accept the sampled target speed and cosine if
If is not accepted, then we repeat the process and resample a target speed and cosine until a combination is found that satisfies equation (76).
Energy-Dependent Cross Section Model¶
As was noted earlier, assuming that the elastic scattering cross section is constant in (56) is not strictly correct, especially when low-lying resonances are present in the cross sections for heavy nuclides. To correctly account for energy dependence of the scattering cross section entails performing another rejection step. The most common method is to sample \(\mu\) and \(v_T\) as in the constant cross section approximation and then perform a rejection on the ratio of the 0 K elastic scattering cross section at the relative velocity to the maximum 0 K elastic scattering cross section over the range of velocities considered:
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
where \(\sigma_c\) is the effective bound coherent scattering cross section, \(W\) is the effective Debye-Waller coefficient, \(E_i\) are the energies of the Bragg edges, and \(f_i\) are related to crystallographic structure factors. Since the functional form of the cross section is just 1/E and the proportionality constant changes only at Bragg edges, the proportionality constants are stored and then the cross section can be calculated analytically based on equation (78).
Outgoing Angle for Coherent Elastic Scattering¶
Another aspect of using S(\(\alpha,\beta,T\)) tables is determining the outgoing energy and angle of the neutron after scattering. For incoherent and coherent elastic scattering, the energy of the neutron does not actually change, but the angle does change. For coherent elastic scattering, the angle will depend on which Bragg edge scattered the neutron. The probability that edge \(i\) will scatter then neutron is given by
After a Bragg edge has been sampled, the cosine of the angle of scattering is given analytically by
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
where the interpolation factor is defined as
Outgoing Energy and Angle for Inelastic Scattering¶
Each S(\(\alpha,\beta,T\)) table provides a correlated angle-energy secondary distribution for neutron thermal inelastic scattering. There are three representations used in the ACE thermal scattering data: equiprobable discrete outgoing energies, non-uniform yet still discrete outgoing energies, and continuous outgoing energies with corresponding probability and cumulative distribution functions provided in tabular format. These three representations all represent the angular distribution in a common format, using a series of discrete equiprobable outgoing cosines.
Equi-Probable Outgoing Energies¶
If the thermal data was processed with \(iwt = 1\) in NJOY, then the outgoing energy spectra is represented in the ACE data as a set of discrete and equiprobable outgoing energies. The procedure to determine the outgoing energy and angle is as such. First, the interpolation factor is determined from equation (82). Then, an outgoing energy bin is sampled from a uniform distribution and then interpolated between values corresponding to neighboring incoming energies:
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:
where \(\mu_{i,j,k}\) is the k-th outgoing cosine corresponding to the j-th outgoing energy and the i-th incoming energy.
Skewed Equi-Probable Outgoing Energies¶
If the thermal data was processed with \(iwt=0\) in NJOY, then the outgoing energy spectra is represented in the ACE data according to the following: the first and last outgoing energies have a relative probability of 1, the second and second-to-last energies have a relative probability of 4, and all other energies have a relative probability of 10. The procedure to determine the outgoing energy and angle is similar to the method discussed above, except that the sampled probability distribution is now skewed accordingly.
Continuous Outgoing Energies¶
If the thermal data was processed with \(iwt=2\) in NJOY, then the outgoing energy spectra is represented by a continuous outgoing energy spectra in tabular form with linear-linear interpolation. The sampling of the outgoing energy portion of this format is very similar to Correlated Energy and Angle Distribution, but the sampling of the correlated angle is performed as it was in the other two representations discussed in this sub-section. In the Law 61 algorithm, we found an interpolation factor \(f\), statistically sampled an incoming energy bin \(\ell\), and sampled an outgoing energy bin \(j\) based on the tabulated cumulative distribution function. Once the outgoing energy has been determined with equation (34), we then need to decide which angular distribution data to use. Like the linear-linear interpolation case in Law 61, the angular distribution closest to the sampled value of the cumulative distribution function for the outgoing energy is utilized. The actual algorithm utilized to sample the outgoing angle is shown in equation (84).
Unresolved Resonance Region Probability Tables¶
Note that unresolved resonance treatments are only applicable to continuous-energy transport.
In the unresolved resonance energy range, resonances may be so closely spaced that it is not possible for experimental measurements to resolve all resonances. To properly account for self-shielding in this energy range, OpenMC uses the probability table method. For most thermal reactors, the use of probability tables will not significantly affect problem results. However, for some fast reactors and other problems with an appreciable flux spectrum in the unresolved resonance range, not using probability tables may lead to incorrect results.
Probability tables in the ACE format are generated from the UNRESR module in NJOY following the method of Levitt. A similar method employed for the RACER and MC21 Monte Carlo codes is described in a paper by Sutton and Brown. For the discussion here, we will focus only on use of the probability table table as it appears in the ACE format.
Each probability table for a nuclide contains the following information at a number of incoming energies within the unresolved resonance range:
- Cumulative probabilities for cross section bands;
- Total cross section (or factor) in each band;
- Elastic scattering cross section (or factor) in each band;
- Fission cross section (or factor) in each band;
- \((n,\gamma)\) cross section (or factor) in each band; and
- Neutron heating number (or factor) in each band.
It should be noted that unresolved resonance probability tables affect only integrated cross sections and no extra data need be given for secondary angle/energy distributions. Secondary distributions for elastic and inelastic scattering would be specified whether or not probability tables were present.
The procedure for determining cross sections in the unresolved range using probability tables is as follows. First, the bounding incoming energies are determined, i.e. find \(i\) such that \(E_i < E < E_{i+1}\). We then sample a cross section band \(j\) using the cumulative probabilities for table \(i\). This allows us to then calculate the elastic, fission, and capture cross sections from the probability tables interpolating between neighboring incoming energies. If interpolation is specified, then the cross sections are calculated as
where \(\sigma_{i,j}\) is the j-th band cross section corresponding to the i-th incoming neutron energy and \(f\) is the interpolation factor defined in the same manner as (82). If logarithmic interpolation is specified, the cross sections are calculated as
where the interpolation factor is now defined as
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.
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
fission sites, where \(w\) is the weight of the neutron before being adjusted. One should note this is just the expected number of neutrons produced per collision rather than the expected number of neutrons produced given that fission has already occurred.
Additionally, since survival biasing can reduce the weight of the neutron to very low values, it is always used in conjunction with a weight cutoff and Russian rouletting. Two user adjustable parameters \(w_c\) and \(w_s\) are given which are the weight below which neutrons should undergo Russian roulette and the weight should they survive Russian roulette. The algorithm for Russian rouletting is as follows. After a collision if \(w < w_c\), then the neutron is killed with probability \(1 - w/w_s\). If it survives, the weight is set equal to \(w_s\). One can confirm that the average weight following Russian roulette is simply \(w\), so the game can be considered “fair”. By default, the cutoff weight in OpenMC is \(w_c = 0.25\) and the survival weight is \(w_s = 1.0\). These parameters vary from one Monte Carlo code to another.
References
[Gelbard] | Ely M. Gelbard, “Epithermal Scattering in VIM,” FRA-TM-123, Argonne National Laboratory (1979). |
[Squires] | G. L. Squires, Introduction to the Theory of Thermal Neutron Scattering, Cambridge University Press (1978). |
[Williams] | M. M. R. Williams, The Slowing Down and Thermalization of Neutrons, North-Holland Publishing Co., Amsterdam (1966). Note: This book can be obtained for free from the OECD. |
Tallies¶
Note that the methods discussed in this section are written specifically for continuous-energy mode but equivalent apply to the multi-group mode if the particle’s energy is replaced with the particle’s group
Filters and Scores¶
The tally capability in OpenMC takes a similar philosophy as that employed in the MC21 Monte Carlo code to give maximum flexibility in specifying tallies while still maintaining scalability. Any tally in a Monte Carlo simulation can be written in the following form:
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
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:
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:
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
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
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
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
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:
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\)
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:
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
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:
This estimator is biased because its expected value is actually not equal to the population variance:
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:
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:
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
We can combine this result with equation (14) to come up with an unbiased estimator for the variance of the sample mean:
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
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
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
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
Thus, inverting the cumulative distribution function, we find the \(x\) percentile of the standard Cauchy distribution to be
For two degrees of freedom, the cumulative distribution function is the second-degree polynomial
Solving for \(x\), we find the \(x\) percentile to be
For degrees of freedom greater than two, it is not possible to obtain an analytical formula for the inverse of the cumulative distribution function. We must resort to either numerically solving for the inverse or to an approximation. Approximations for percentiles of the t-distribution have been found with high levels of accuracy. OpenMC uses the following approximation:
where \(z_x\) is the \(x\) percentile of the standard normal distribution. In order to determine an arbitrary percentile of the standard normal distribution, we use an unpublished rational approximation. After using the rational approximation, one iteration of Newton’s method is applied to improve the estimate of the percentile.
References
Eigenvalue Calculations¶
An eigenvalue calculation, also referred to as a criticality calculation, is a transport simulation wherein the source of neutrons includes a fissionable material. Some common eigenvalue calculations include the simulation of nuclear reactors, spent fuel pools, nuclear weapons, and other fissile systems. The reason they are called eigenvalue calculations is that the transport equation becomes an eigenvalue equation if a fissionable source is present since then the source of neutrons will depend on the flux of neutrons itself. Eigenvalue simulations using Monte Carlo methods are becoming increasingly common with the advent of high-performance computing.
This section will explore the theory behind and implementation of eigenvalue calculations in a Monte Carlo code.
Method of Successive Generations¶
The method used to converge on the fission source distribution in an eigenvalue calculation, known as the method of successive generations, was first introduced by [Lieberoth]. In this method, a finite number of neutron histories, \(N\), are tracked through their lifetime iteratively. If fission occurs, rather than tracking the resulting fission neutrons, the spatial coordinates of the fission site, the sampled outgoing energy and direction of the fission neutron, and the weight of the neutron are stored for use in the subsequent generation. In OpenMC, the array used for storing the fission site information is called the fission bank. At the end of each fission generation, \(N\) source sites for the next generation must be randomly sampled from the \(M\) fission sites that were stored to ensure that the neutron population does not grow exponentially. The sampled source sites are stored in an array called the source bank and can be retrieved during the subsequent generation.
It’s important to recognize that in the method of successive generations, we must start with some assumption on how the fission source sites are distributed since the distribution is not known a priori. Typically, a user will make a guess as to what the distribution is – this guess could be a uniform distribution over some region of the geometry or simply a point source. Fortunately, regardless of the choice of initial source distribution, the method is guaranteed to converge to the true source distribution. Until the source distribution converges, tallies should not be scored to since they will otherwise include contributions from an unconverged source distribution.
The method by which the fission source iterations are parallelized can have a large impact on the achievable parallel scaling. This topic is discussed at length in Fission Bank Algorithms.
Source Convergence Issues¶
Diagnosing Convergence with Shannon Entropy¶
As discussed earlier, it is necessary to converge both \(k_{eff}\) and the source distribution before any tallies can begin. Moreover, the convergence rate of the source distribution is in general slower than that of \(k_{eff}\). One should thus examine not only the convergence of \(k_{eff}\) but also the convergence of the source distribution in order to make decisions on when to start active batches.
However, the representation of the source distribution makes it a bit more difficult to analyze its convergence. Since \(k_{eff}\) is a scalar quantity, it is easy to simply look at a line plot of \(k_{eff}\) versus the number of batches and this should give the user some idea about whether it has converged. On the other hand, the source distribution at any given batch is a finite set of coordinates in Euclidean space. In order to analyze the convergence, we would either need to use a method for assessing convergence of an N-dimensional quantity or transform our set of coordinates into a scalar metric. The latter approach has been developed considerably over the last decade and a method now commonly used in Monte Carlo eigenvalue calculations is to use a metric called the Shannon entropy, a concept borrowed from information theory.
To compute the Shannon entropy of the source distribution, we first need to discretize the source distribution rather than having a set of coordinates in Euclidean space. This can be done by superimposing a structured mesh over the geometry (containing at least all fissionable materials). Then, the fraction of source sites that are present in each mesh element is counted:
The Shannon entropy is then computed as
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
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
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..

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

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:
- The beginning of a cycle where each node has \(N/p\) source sites;
- 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.

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
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
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
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
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,
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):
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
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
Lastly, the stochastic and true eigenvalues can be written as integrals over all phase space of the stochastic and true source distributions, respectively, as
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
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
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
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:
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
Integrating over all space and simplifying, we can obtain an expression for the eigenvalue on the \(i\)-th node:
It is easy to show from this expression that the stochastic realization of the global eigenvalue is merely the average of these local eigenvalues:
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
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
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:
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
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\):
Since we know the variance of \(\hat{k}_i\), we can use the error propagation law to determine the variance of \(M_i\):
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
Through a similar analysis, one can show that the variance of \(\sum_{i=1}^j M_i\) is
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
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:
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:
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)\):
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.

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:
and
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:
where
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
- cell-to-boundary coupling
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
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,
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,
and
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,
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
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
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
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,
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.

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.
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 |
To solve the eigenvalue problem with these matrices, different source iteration and linear solvers can be used. The most common source iteration solver used is standard power iteration as described in [Gill]. To accelerate these source iterations, a Wielandt shift scheme can be used as discussed in [Park]. PETSc solvers were first implemented to perform the linear solution in parallel that occurs once per source iteration. When using PETSc, different types of parallel linear solvers and preconditioners can be used. By default, OpenMC uses an incomplete LU preconditioner and a GMRES Krylov solver. After some initial studies of parallelization with PETSc, it was observed that because CMFD matrices are very sparse, solution times do not scale well. An additional Gauss-Seidel linear solver with Chebyshev acceleration was added that is similar to the one used for CMFD in CASMO [Rhodes] and [Smith]. This solver was implemented with a custom section for two energy groups. Because energy group is the inner most index, a block diagonal is formed when using more than one group. For two groups, it is easy to invert this diagonal analytically inside the Gauss-Seidel iterative solver. For more than two groups, this analytic inversion can still be performed, but with more computational effort. A standard Gauss-Seidel solver is used for more than two groups.
Besides a power iteration, a Jacobian-free Newton-Krylov method was also implemented to obtain eigenvalue and multigroup fluxes as described in [Gill] and [Knoll]. This method is not the primary one used, but has gotten recent attention due to its coupling advantages to other physics such as thermal hydraulics. Once multigroup fluxes are obtained, a normalized fission source is calculated in the code using eq. (16) directly.
The next step in the process is to compute weight adjustment factors. These are calculated by taking the ratio of the expected number of neutrons from the CMFD source distribution to the current number of neutrons in each mesh. It is straightforward to compute the CMFD number of neutrons because it is the product between the total starting initial weight of neutrons and the CMFD normalized fission source distribution. To compute the number of neutrons from the current MC source, OpenMC sums the statistical weights of neutrons from the source bank on a given spatial and energy mesh. Once weight adjustment factors were calculated, each neutron’s statistical weight in the source bank was modified according to its location and energy. Examples of CMFD simulations using OpenMC can be found in [HermanThesis].
References
[BEAVRS] | Nick Horelik, Bryan Herman. Benchmark for Evaluation And Verification of Reactor Simulations. Massachusetts Institute of Technology, http://crpg.mit.edu/pub/beavrs , 2013. |
[Gill] | (1, 2) Daniel F. Gill. Newton-Krylov methods for the solution of the k-eigenvalue problem in multigroup neutronics calculations. Ph.D. thesis, Pennsylvania State University, 2010. |
[Hebert] | Alain Hebert. Applied reactor physics. Presses Internationales Polytechnique, Montreal, 2009. |
[Herman] | Bryan R. Herman, Benoit Forget, Kord Smith, and Brian N. Aviles. Improved diffusion coefficients generated from Monte Carlo codes. In Proceedings of M&C 2013, Sun Valley, ID, USA, May 5 - 9, 2013. |
[HermanThesis] | Bryan R. Herman. Monte Carlo and Thermal Hydraulic Coupling using Low-Order Nonlinear Diffusion Acceleration. Sc.D. thesis, Massachusetts Institute of Technology, 2014. |
[Knoll] | D.A. Knoll, H. Park, and C. Newman. Acceleration of k-eigenvalue/criticality calculations using the Jacobian-free Newton-Krylov method. Nuclear Science and Engineering, 167:133–140, 2011. |
[Park] | H. Park, D.A. Knoll, and C.K. Newman. Nonlinear acceleration of transport criticality problems. Nuclear Science and Engineering, 172:52–65, 2012. |
[Rhodes] | Joel Rhodes and Malte Edenius. CASMO-4 — A Fuel Assembly Burnup Program. User’s Manual. Studsvik of America, ssp-09/443-u rev 0, proprietary edition, 2001. |
[Smith] | Kord S Smith and Joel D Rhodes III. Full-core, 2-D, LWR core calculations with CASMO-4E. In Proceedings of PHYSOR 2002, Seoul, Korea, October 7 - 10, 2002. |
User’s Guide¶
Welcome to the OpenMC User’s Guide! This tutorial will guide you through the essential aspects of using OpenMC to perform simulations.
A Beginner’s Guide to OpenMC¶
What does OpenMC do?¶
In a nutshell, OpenMC simulates neutral particles (presently only neutrons) moving stochastically through an arbitrarily defined model that represents an real-world experimental setup. The experiment could be as simple as a sphere of metal or as complicated as a full-scale nuclear reactor. This is what’s known as Monte Carlo simulation. In the case of a nuclear reactor model, neutrons are especially important because they are the particles that induce fission in isotopes of uranium and other elements. Knowing the behavior of neutrons allows one to determine how often and where fission occurs. The amount of energy released is then directly proportional to the fission reaction rate since most heat is produced by fission. By simulating many neutrons (millions or billions), it is possible to determine the average behavior of these neutrons (or the behavior of the energy produced, or any other quantity one is interested in) very accurately.
Using Monte Carlo methods to determine the average behavior of various physical quantities in a system is quite different from other means of solving the same problem. The other class of methods for determining the behavior of neutrons and reactions rates is so-called deterministic methods. In these methods, the starting point is not randomly simulating particles but rather writing an equation that describes the average behavior of the particles. The equation that describes the average behavior of neutrons is called the neutron transport equation. This equation is a seven-dimensional equation (three for space, three for velocity, and one for time) and is very difficult to solve directly. For all but the simplest problems, it is necessary to make some sort of discretization. As an example, we can divide up all space into small sections which are homogeneous and then solve the equation on those small sections. After these discretizations and various approximations, one can arrive at forms that are suitable for solution on a computer. Among these are discrete ordinates, method of characteristics, finite-difference diffusion, and nodal methods.
So why choose Monte Carlo over deterministic methods? Each method has its pros and cons. Let us first take a look at few of the salient pros and cons of deterministic methods:
- Pro: Depending on what method is used, solution can be determined very quickly.
- Pro: The solution is a global solution, i.e. we know the average behavior everywhere.
- Pro: Once the problem is converged, the solution is known.
- Con: If the model is complex, it is necessary to do sophisticated mesh generation.
- Con: It is necessary to generate multi-group cross sections which requires knowing the solution a priori.
Now let’s look at the pros and cons of Monte Carlo methods:
- Pro: No mesh generation is required to build geometry. By using constructive solid geometry, it’s possible to build arbitrarily complex models with curved surfaces.
- Pro: Monte Carlo methods can be used with either continuous-energy or multi-group cross sections.
- Pro: Running simulations in parallel is conceptually very simple.
- Con: Because they rely on repeated random sampling, they are computationally very expensive.
- Con: A simulation doesn’t automatically give you the global solution everywhere – you have to specifically ask for those quantities you want.
- Con: Even after the problem is converged, it is necessary to simulate many particles to reduce stochastic uncertainty.
Because fewer approximations are made in solving a problem by the Monte Carlo method, it is often seen as a “gold standard” which can be used as a benchmark for a solution of the same problem by deterministic means. However, it comes at the expense of a potentially longer simulation.
How does it work?¶
In order to do anything, the code first needs to have a model of some problem of interest. This could be a nuclear reactor or any other physical system with fissioning material. You, as the code user, will need to describe the model so that the code can do something with it. A basic model consists of a few things:
- A description of the geometry – the problem must be split up into regions of homogeneous material composition.
- For each different material in the problem, a description of what nuclides are in the material and at what density.
- Various parameters telling the code how many particles to simulate and what options to use.
- A list of different physical quantities that the code should return at the end of the simulation. In a Monte Carlo simulation, if you don’t ask for anything, it will not give you any answers (other than a few default quantities).
What do I need to know?¶
If you are starting to work with OpenMC, there are a few things you should be familiar with. Whether you plan on working in Linux, macOS, or Windows, you should be comfortable working in a command line environment. There are many resources online for learning command line environments. If you are using Linux or Mac OS X (also Unix-derived), this tutorial will help you get acquainted with commonly-used commands.
To reap the full benefits of OpenMC, you should also have basic proficiency in the use of Python, as OpenMC includes a rich Python API that offers many usability improvements over dealing with raw XML input files.
OpenMC uses a version control software called git to keep track of changes to the code, document bugs and issues, and other development tasks. While you don’t necessarily have to have git installed in order to download and run OpenMC, it makes it much easier to receive updates if you do have it installed and have a basic understanding of how it works. There are a list of good git tutorials at the git documentation website. The OpenMC source code and documentation are hosted at GitHub. In order to receive updates to the code directly, submit bug reports, and perform other development tasks, you may want to sign up for a free account on GitHub. Once you have an account, you can follow these instructions on how to set up your computer for using GitHub.
If you are new to nuclear engineering, you may want to review the NRC’s Reactor Concepts Manual. This manual describes the basics of nuclear power for electricity generation, the fission process, and the overall systems in a pressurized or boiling water reactor. Another resource that is a bit more technical than the Reactor Concepts Manual but still at an elementary level is the DOE Fundamentals Handbook on Nuclear Physics and Reactor Theory Volume I and Volume II. You may also find it helpful to review the following terms:
Installation and Configuration¶
Installing on Linux/Mac with conda-forge¶
Conda is an open source package management system and environment management system for installing multiple versions of software packages and their dependencies and switching easily between them. conda-forge is a community-led conda channel of installable packages. For instructions on installing conda, please consult their documentation.
Once you have conda installed on your system, add the conda-forge channel to your configuration with:
conda config --add channels conda-forge
Once the conda-forge channel has been enabled, OpenMC can then be installed with:
conda install openmc
It is possible to list all of the versions of OpenMC available on your platform with:
conda search openmc --channel conda-forge
Installing on Ubuntu with PPA¶
For users with Ubuntu 15.04 or later, a binary package for OpenMC is available through a Personal Package Archive (PPA) and can be installed through the APT package manager. First, add the following PPA to the repository sources:
sudo apt-add-repository ppa:paulromano/staging
Next, resynchronize the package index files:
sudo apt update
Now OpenMC should be recognized within the repository and can be installed:
sudo apt install openmc
Binary packages from this PPA may exist for earlier versions of Ubuntu, but they are no longer supported.
Building from Source¶
Prerequisites¶
Required
A Fortran compiler such as gfortran
In order to compile OpenMC, you will need to have a Fortran compiler installed on your machine. Since a number of Fortran 2003/2008 features are used in the code, it is recommended that you use the latest version of whatever compiler you choose. For gfortran, it is necessary to use version 4.8.0 or above.
If you are using Debian or a Debian derivative such as Ubuntu, you can install the gfortran compiler using the following command:
sudo apt install gfortranA C/C++ compiler such as gcc
OpenMC includes two libraries written in C and C++, respectively. These libraries have been tested to work with a wide variety of compilers. If you are using a Debian-based distribution, you can install the g++ compiler using the following command:
sudo apt install g++CMake cross-platform build system
The compiling and linking of source files is handled by CMake in a platform-independent manner. If you are using Debian or a Debian derivative such as Ubuntu, you can install CMake using the following command:
sudo apt install cmakeHDF5 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-parallelYou may omit
--enable-parallel
if you want to compile HDF5 in serial.Important
OpenMC uses various parts of the HDF5 Fortran 2003 API; as such you must include
--enable-fortran2003
or else OpenMC will not be able to compile.On Debian derivatives, HDF5 and/or parallel HDF5 can be installed through the APT package manager:
sudo apt install libhdf5-dev hdf5-helpersNote that the exact package names may vary depending on your particular distribution and version.
Optional
An MPI implementation for distributed-memory parallel runs
To compile with support for parallel runs on a distributed-memory architecture, you will need to have a valid implementation of MPI installed on your machine. The code has been tested and is known to work with the latest versions of both OpenMPI and MPICH. OpenMPI and/or MPICH can be installed on Debian derivatives with:
sudo apt install mpich libmpich-dev sudo apt install openmpi-bin libopenmpi-devgit version control software for obtaining source code
Obtaining the Source¶
All OpenMC source code is hosted on GitHub. You can download the source code directly from GitHub or, if you have the git version control software installed on your computer, you can use git to obtain the source code. The latter method has the benefit that it is easy to receive updates directly from the GitHub repository. GitHub has a good set of instructions for how to set up git to work with GitHub since this involves setting up ssh keys. With git installed and setup, the following command will download the full source code from the GitHub repository:
git clone https://github.com/mit-crpg/openmc.git
By default, the cloned repository will be set to the development branch. To switch to the source of the latest stable release, run the following commands:
cd openmc
git checkout master
Build Configuration¶
Compiling OpenMC with CMake is carried out in two steps. First, cmake
is run
to determine the compiler, whether optional packages (MPI, HDF5) are available,
to generate a list of dependencies between source files so that they may be
compiled in the correct order, and to generate a normal Makefile. The Makefile
is then used by make
to actually carry out the compile and linking
commands. A typical out-of-source build would thus look something like the
following
mkdir build && cd build
cmake ..
make
Note that first a build directory is created as a subdirectory of the source directory. The Makefile in the top-level directory will automatically perform an out-of-source build with default options.
CMakeLists.txt Options¶
The following options are available in the CMakeLists.txt file:
- debug
- Enables debugging when compiling. The flags added are dependent on which compiler is used.
- profile
- Enables profiling using the GNU profiler, gprof.
- optimize
- Enables high-optimization using compiler-dependent flags. For gfortran and Intel Fortran, this compiles with -O3.
- openmp
- Enables shared-memory parallelism using the OpenMP API. The Fortran compiler being used must support OpenMP. (Default: on)
- coverage
- Compile and link code instrumented for coverage analysis. This is typically used in conjunction with gcov.
- maxcoord
- Maximum number of nested coordinate levels in geometry. Defaults to 10.
To set any of these options (e.g. turning on debug mode), the following form should be used:
cmake -Ddebug=on /path/to/openmc
Compiling with MPI¶
To compile with MPI, set the FC
and CC
environment variables
to the path to the MPI Fortran and C wrappers, respectively. For example, in a
bash shell:
export FC=mpif90
export CC=mpicc
cmake /path/to/openmc
Note that in many shells, environment variables can be set for a single command, i.e.
FC=mpif90 CC=mpicc cmake /path/to/openmc
Selecting HDF5 Installation¶
CMakeLists.txt searches for the h5fc
or h5pfc
HDF5 Fortran wrapper on
your PATH environment variable and subsequently uses it to determine library
locations and compile flags. If you have multiple installations of HDF5 or one
that does not appear on your PATH, you can set the HDF5_ROOT environment
variable to the root directory of the HDF5 installation, e.g.
export HDF5_ROOT=/opt/hdf5/1.8.15
cmake /path/to/openmc
This will cause CMake to search first in /opt/hdf5/1.8.15/bin for h5fc
/
h5pfc
before it searches elsewhere. As noted above, an environment variable
can typically be set for a single command, i.e.
HDF5_ROOT=/opt/hdf5/1.8.15 cmake /path/to/openmc
Compiling on Linux and Mac OS X¶
To compile OpenMC on Linux or Max OS X, run the following commands from within the root directory of the source code:
mkdir build && cd build
cmake ..
make
make install
This will build an executable named openmc
and install it (by default in
/usr/local/bin). If you do not have administrative privileges, you can install
OpenMC locally by specifying an install prefix when running cmake:
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local ..
The CMAKE_INSTALL_PREFIX
variable can be changed to any path for which you
have write-access.
Compiling on Windows 10¶
Recent versions of Windows 10 include a subsystem for Linux that allows one to
run Bash within Ubuntu running in Windows. First, follow the installation guide
here to get
Bash on Ubuntu on Windows setup. Once you are within bash, obtain the necessary
prerequisites via apt
. Finally, follow the
instructions for compiling on linux.
Compiling for the Intel Xeon Phi¶
For the second generation Knights Landing architecture, nothing special is required to compile OpenMC. You may wish to experiment with compiler flags that control generation of vector instructions to see what configuration gives optimal performance for your target problem.
For the first generation Knights Corner architecture, it is necessary to
cross-compile OpenMC. If you are using the Intel Fortran compiler, it is
necessary to specify that all objects be compiled with the -mmic
flag as
follows:
mkdir build && cd build
FC=ifort CC=icc FFLAGS=-mmic cmake -Dopenmp=on ..
make
Note that unless an HDF5 build for the Intel Xeon Phi (Knights Corner) is already on your target machine, you will need to cross-compile HDF5 for the Xeon Phi. An example script to build zlib and HDF5 provides several necessary workarounds.
Testing Build¶
If you have ENDF/B-VII.1 cross sections from NNDC you can test your build. Make sure the OPENMC_CROSS_SECTIONS environmental variable is set to the cross_sections.xml file in the data/nndc directory. There are two ways to run tests. The first is to use the Makefile present in the source directory and run the following:
make test
If you want more options for testing you can use ctest command. For example, if we wanted to run only the plot tests with 4 processors, we run:
cd build
ctest -j 4 -R plot
If you want to run the full test suite with different build options please refer to our OpenMC Test Suite documentation.
Python Prerequisites¶
OpenMC’s Python API works with either Python 2.7 or Python 3.2+. In addition to Python itself, the API relies on a number of third-party packages. All prerequisites can be installed using conda (recommended), pip, or through the package manager in most Linux distributions.
Required
- six
- The Python API works with both Python 2.7+ and 3.2+. To do so, the six compatibility library is used.
- NumPy
- NumPy is used extensively within the Python API for its powerful N-dimensional array.
- h5py
- h5py provides Python bindings to the HDF5 library. Since OpenMC outputs various HDF5 files, h5py is needed to provide access to data within these files from Python.
Optional
- SciPy
- SciPy’s special functions, sparse matrices, and spatial data structures are used for several optional features in the API.
- pandas
- Pandas is used to generate tally DataFrames as demonstrated in Pandas Dataframes example notebook.
- Matplotlib
- Matplotlib is used to providing plotting functionality in the API like the
Universe.plot()
method and theopenmc.plot_xs()
function. - uncertainties
- Uncertainties are optionally used for decay data in the
openmc.data
module. - Cython
- Cython is used for resonance reconstruction for ENDF data converted to
openmc.data.IncidentNeutron
. - vtk
- The Python VTK bindings are needed to convert voxel and track files to VTK format.
- silomesh
- The silomesh package is needed to convert voxel and track files to SILO format.
- lxml
- lxml is used for the openmc-validate-xml script.
Configuring Input Validation with GNU Emacs nXML mode¶
The GNU Emacs text editor has a built-in mode that extends functionality for
editing XML files. One of the features in nXML mode is the ability to perform
real-time validation of XML files against a RELAX NG schema. The OpenMC
source contains RELAX NG schemas for each type of user input file. In order for
nXML mode to know about these schemas, you need to tell emacs where to find a
“locating files” description. Adding the following lines to your ~/.emacs
file will enable real-time validation of XML input files:
(require 'rng-loc)
(add-to-list 'rng-schema-locating-files "~/openmc/schemas.xml")
Make sure to replace the last string on the second line with the path to the schemas.xml file in your own OpenMC source directory.
Cross Section Configuration¶
In order to run a simulation with OpenMC, you will need cross section data for each nuclide or material in your problem. OpenMC can be run in continuous-energy or multi-group mode.
In continuous-energy mode, OpenMC uses a native HDF5 format (see Nuclear Data File Format) to store all nuclear data. If you have ACE format data that was produced with NJOY, such as that distributed with MCNP or Serpent, it can be converted to the HDF5 format using the openmc-ace-to-hdf5 script (or using the Python API). Several sources provide openly available ACE data as described below and can be easily converted using the provided scripts. The TALYS-based evaluated nuclear data library, TENDL, is also available in ACE format. In addition to tabulated cross sections in the HDF5 files, OpenMC relies on windowed multipole data to perform on-the-fly Doppler broadening.
In multi-group mode, OpenMC utilizes an HDF5-based library format which can be used to describe nuclide- or material-specific quantities.
Environment Variables¶
When openmc is run, it will look for several environment
variables that indicate where cross sections can be found. While the location of
cross sections can also be indicated through the openmc.Materials
class
(or in the materials.xml file), if you always use the same
set of cross section data, it is often easier to just set an environment
variable that will be picked up by default every time OpenMC is run. The
following environment variables are used:
OPENMC_CROSS_SECTIONS
- Indicates the path to the cross_sections.xml
summary file that is used to locate HDF5 format cross section libraries if the
user has not specified
Materials.cross_sections
(equivalently, the <cross_sections> Element in materials.xml). OPENMC_MULTIPOLE_LIBRARY
- Indicates the path to a directory containing windowed multipole data if the
user has not specified
Materials.multipole_library
(equivalently, the <multipole_library> Element in materials.xml) OPENMC_MG_CROSS_SECTIONS
- Indicates the path to the an HDF5 file that contains
multi-group cross sections if the user has not specified
Materials.cross_sections
(equivalently, the <cross_sections> Element in materials.xml).
To set these environment variables persistently, export them from your shell
profile (.profile
or .bashrc
in bash).
Continuous-Energy Cross Sections¶
Using ENDF/B-VII.1 Cross Sections from NNDC¶
The NNDC provides ACE data from the ENDF/B-VII.1 neutron and thermal scattering sublibraries at room temperature processed using NJOY. To use this data with OpenMC, the openmc-get-nndc-data script can be used to automatically download and extract the ACE data, fix any deficiencies, and create an HDF5 library:
openmc-get-nndc-data
At this point, you should set the OPENMC_CROSS_SECTIONS
environment
variable to the absolute path of the file nndc_hdf5/cross_sections.xml
. This
cross section set is used by the test suite.
Using JEFF Cross Sections from OECD/NEA¶
The NEA provides processed ACE data from the JEFF library. To use this data with OpenMC, the openmc-get-jeff-data script can be used to automatically download and extract the ACE data, fix any deficiencies, and create an HDF5 library.
openmc-get-jeff-data
At this point, you should set the OPENMC_CROSS_SECTIONS
environment
variable to the absolute path of the file jeff-3.2-hdf5/cross_sections.xml
.
Using Cross Sections from MCNP¶
OpenMC provides two scripts (openmc-convert-mcnp70-data and openmc-convert-mcnp71-data)
that will automatically convert ENDF/B-VII.0 and ENDF/B-VII.1 ACE data that is
provided with MCNP5 or MCNP6. To convert the ENDF/B-VII.0 ACE files
(endf70[a-k]
and endf70sab
) into the native HDF5 format, run the
following:
openmc-convert-mcnp70-data /path/to/mcnpdata/
where /path/to/mcnpdata
is the directory containing the endf70[a-k]
files.
To convert the ENDF/B-VII.1 ACE files (the endf71x and ENDF71SaB libraries), use the following script:
openmc-convert-mcnp71-data /path/to/mcnpdata
where /path/to/mcnpdata
is the directory containing the endf71x
and
ENDF71SaB
directories.
Using Other Cross Sections¶
If you have a library of ACE format cross sections other than those listed above that you need to convert to OpenMC’s HDF5 format, the openmc-ace-to-hdf5 script can be used. There are four different ways you can specify ACE libraries that are to be converted:
- List each ACE library as a positional argument. This is very useful in conjunction with the usual shell utilities (ls, find, etc.).
- Use the
--xml
option to specify a pre-v0.9 cross_sections.xml file. - Use the
--xsdir
option to specify a MCNP xsdir file. - Use the
--xsdata
option to specify a Serpent xsdata file.
The script does not use any extra information from cross_sections.xml/ xsdir/
xsdata files to determine whether the nuclide is metastable. Instead, the
--metastable
argument can be used to specify whether the ZAID naming
convention follows the NNDC data convention (1000*Z + A + 300 + 100*m), or the
MCNP data convention (essentially the same as NNDC, except that the first
metastable state of Am242 is 95242 and the ground state is 95642).
Manually Creating a Library from ACE files¶
The scripts described above use the openmc.data
module in the Python API
to convert ACE data and create a cross_sections.xml
file. For those who prefer to use the API directly, the
openmc.data.IncidentNeutron
and openmc.data.ThermalScattering
classes can be used to read ACE data and convert it to HDF5. For
continuous-energy incident neutron data, use the
IncidentNeutron.from_ace()
class method to read in an existing ACE file
and the IncidentNeutron.export_to_hdf5()
method to write the data to an
HDF5 file.
u235 = openmc.data.IncidentNeutron.from_ace('92235.710nc')
u235.export_to_hdf5('U235.h5')
If you have multiple ACE files for the same nuclide at different temperatures,
you can use the IncidentNeutron.add_temperature_from_ace()
method to
append cross sections to an existing IncidentNeutron
instance:
u235 = openmc.data.IncidentNeutron.from_ace('92235.710nc')
for suffix in [711, 712, 713, 714, 715, 716]:
u235.add_temperature_from_ace('92235.{}nc'.format(suffix))
u235.export_to_hdf5('U235.h5')
Similar methods exist for thermal scattering data:
light_water = openmc.data.ThermalScattering.from_ace('lwtr.20t')
for suffix in range(21, 28):
light_water.add_temperature_from_ace('lwtr.{}t'.format(suffix))
light_water.export_to_hdf5('lwtr.h5')
Once you have created corresponding HDF5 files for each of your ACE files, you
can create a library and export it to XML using the
openmc.data.DataLibrary
class:
library = openmc.data.DataLibrary()
library.register_file('U235.h5')
library.register_file('lwtr.h5')
...
library.export_to_xml()
At this point, you will have a cross_sections.xml
file that you can use in
OpenMC.
Hint
The IncidentNeutron
class allows you to view/modify cross
sections, secondary angle/energy distributions, probability tables,
etc. For a more thorough overview of the capabilities of this class,
see the Nuclear Data example notebook.
Manually Creating a Library from ENDF files¶
If you need to create a nuclear data library and you do not already have
suitable ACE files or you need to further customize the data (for example,
adding more temperatures), the IncidentNeutron.from_njoy()
and
ThermalScattering.from_njoy()
methods can be used to create data instances
by directly running NJOY. Both methods require that you pass the name of ENDF
file(s) that are passed on to NJOY. For example, to generate data for Zr-92:
zr92 = openmc.data.IncidentNeutron.from_njoy('n-040_Zr_092.endf')
By default, data is produced at room temperature, 293.6 K. You can also specify a list of temperatures that you want data at:
zr92 = openmc.data.IncidentNeutron.from_njoy(
'n-040_Zr_092.endf', temperatures=[300., 600., 1000.])
The IncidentNeutron.from_njoy()
method assumes you have an executable
named njoy
available on your path. If you want to explicitly name the
executable, the njoy_exec
optional argument can be used. Additionally, the
stdout
argument can be used to show the progress of the NJOY run.
Once you have instances of IncidentNeutron
and
ThermalScattering
, a library can be created by using the
export_to_hdf5()
methods and the DataLibrary
class as described in
Manually Creating a Library from ACE files.
Enabling Resonance Scattering Treatments¶
In order for OpenMC to correctly treat elastic scattering in heavy nuclides
where low-lying resonances might be present (see
Energy-Dependent Cross Section Model), the elastic scattering cross section at 0 K
must be present. To add the 0 K elastic scattering cross section to existing
IncidentNeutron
instance, you can use the
IncidentNeutron.add_elastic_0K_from_endf()
method which requires an ENDF
file for the nuclide you are modifying:
u238 = openmc.data.IncidentNeutron.from_hdf5('U238.h5')
u238.add_elastic_0K_from_endf('n-092_U_238.endf')
u238.export_to_hdf5('U238_with_0K.h5')
With 0 K elastic scattering data present, you can turn on a resonance scattering
method using Settings.resonance_scattering
.
Note
The process of reconstructing resonances and generating tabulated 0 K
cross sections can be computationally expensive, especially for
nuclides like U-238 where thousands of resonances are present. Thus,
running the IncidentNeutron.add_elastic_0K_from_endf()
method
may take several minutes to complete.
Windowed Multipole Data¶
OpenMC is capable of using windowed multipole data for on-the-fly Doppler
broadening. While such data is not yet available for all nuclides, an
experimental multipole library is available that contains data for 70
nuclides. To obtain this library, you can run openmc-get-multipole-data which
will download and extract it into a wmp
directory. Once the library has been
downloaded, set the OPENMC_MULTIPOLE_LIBRARY
environment variable (or
the Materials.multipole_library
attribute) to the wmp
directory.
Multi-Group Cross Sections¶
Multi-group cross section libraries are generally tailored to the specific
calculation to be performed. Therefore, at this point in time, OpenMC is not
distributed with any pre-existing multi-group cross section libraries.
However, if obtained or generated their own library, the user
should set the OPENMC_MG_CROSS_SECTIONS
environment variable
to the absolute path of the file library expected to used most frequently.
For an example of how to create a multi-group library, see Multi-Group Mode Part I: Introduction.
Basics of Using OpenMC¶
Creating a Model¶
When you build and install OpenMC, you will have an openmc
executable on your system. When you run openmc
, the first thing it will do
is look for a set of XML files that describe the model you want to
simulation. Three of these files are required and another three are optional, as
described below.
Required
- Materials Specification – materials.xml
- This file describes what materials are present in the problem and what they are composed of. Additionally, it indicates where OpenMC should look for a cross section library.
- Geometry Specification – geometry.xml
- This file describes how the materials defined in
materials.xml
occupy regions of space. Physical volumes are defined using constructive solid geometry, described in detail in Defining Geometry. - Settings Specification – settings.xml
- This file indicates what mode OpenMC should be run in, how many particles to simulate, the source definition, and a whole host of miscellaneous options.
Optional
- Tallies Specification – tallies.xml
- This file describes what physical quantities should be tallied during the simulation (fluxes, reaction rates, currents, etc.).
- Geometry Plotting Specification – plots.xml
- This file gives specifications for producing slice or voxel plots of the geometry.
- CMFD Specification – cmfd.xml
- This file specifies execution parameters for coarse mesh finite difference (CMFD) acceleration.
eXtensible Markup Language (XML)¶
Unlike many other Monte Carlo codes which use an arbitrary-format ASCII file with “cards” to specify a particular geometry, materials, and associated run settings, the input files for OpenMC are structured in a set of XML files. XML, which stands for eXtensible Markup Language, is a simple format that allows data to be exchanged efficiently between different programs and interfaces.
Anyone who has ever seen webpages written in HTML will be familiar with the structure of XML whereby “tags” enclosed in angle brackets denote that a particular piece of data will follow. Let us examine the follow example:
<person>
<firstname>John</firstname>
<lastname>Smith</lastname>
<age>27</age>
<occupation>Health Physicist</occupation>
</person>
Here we see that the first tag indicates that the following data will describe a person. The nested tags firstname, lastname, age, and occupation indicate characteristics about the person being described.
In much the same way, OpenMC input uses XML tags to describe the geometry, the materials, and settings for a Monte Carlo simulation. Note that because the XML files have a well-defined structure, they can be validated using the openmc-validate-xml script or using Emacs nXML mode.
Creating Input Files¶
The most rudimentary option for creating input files is to simply write them from scratch using the XML format specifications. This approach will feel familiar to users of other Monte Carlo codes such as MCNP and Serpent, with the added bonus that the XML formats feel much more “readable”. Alternatively, input files can be generated using OpenMC’s Python API, which is introduced in the following section.
Python API¶
OpenMC’s Python API defines a set of functions and classes
that roughly correspond to elements in the XML files. For example, the
openmc.Cell
Python class directly corresponds to the
<cell> Element in XML. Each XML file itself also has a corresponding class:
openmc.Geometry
for geometry.xml
, openmc.Materials
for
materials.xml
, openmc.Settings
for settings.xml
, and so on. To
create a model then, one creates instances of these classes and then uses the
export_to_xml()
method, e.g. Geometry.export_to_xml()
. Most scripts
that generate a full model will look something like the following:
# Create materials
materials = openmc.Materials()
...
materials.export_to_xml()
# Create geometry
geometry = openmc.Geometry()
...
geometry.export_to_xml()
# Assign simulation settings
settings = openmc.Settings()
...
settings.export_to_xml()
One a model has been created and exported to XML, a simulation can be run either
by calling openmc directly from a shell or by using the
openmc.run()
function from Python.
Identifying Objects¶
In the XML user input files, each object (cell, surface, tally, etc.) has to be uniquely identified by a positive integer (ID) in the same manner as MCNP and Serpent. In the Python API, integer IDs can be assigned but it is not strictly required. When IDs are not explicitly assigned to instances of the OpenMC Python classes, they will be automatically assigned.
Viewing and Analyzing Results¶
After a simulation has been completed by running openmc, you will have several output files that were created:
tallies.out
- An ASCII file showing the mean and standard deviation of the mean for any user-defined tallies.
summary.h5
- An HDF5 file with a complete description of the geometry and materials used in the simulation.
statepoint.#.h5
- An HDF5 file with the complete results of the simulation, including tallies as well as the final source distribution. This file can be used both to view/analyze results as well as restart a simulation if desired.
For a simple simulation with few tallies, looking at the tallies.out
file
might be sufficient. For anything more complicated (plotting results, finding a
subset of results, etc.), you will likely find it easier to work with the
statepoint file directly using the openmc.StatePoint
class. For more
details on working with statepoints, see Working with State Points.
Physical Units¶
Unless specified otherwise, all length quantities are assumed to be in units of centimeters, all energy quantities are assumed to be in electronvolts, and all time quantities are assumed to be in seconds.
Measure | Default unit | Symbol |
---|---|---|
length | centimeter | cm |
energy | electronvolt | eV |
time | second | s |
ERSN-OpenMC Graphical User Interface¶
A third-party Java-based user-friendly graphical user interface for creating XML input files called ERSN-OpenMC is developed and maintained by members of the Radiation and Nuclear Systems Group at the Faculty of Sciences Tetouan, Morocco. The GUI also allows one to automatically download prerequisites for installing and running OpenMC.
Material Compositions¶
Materials in OpenMC are defined as a set of nuclides/elements at specified
densities and are created using the openmc.Material
class. Once a
material has been instantiated, nuclides can be added with
Material.add_nuclide()
and elements can be added with
Material.add_element()
. Densities can be specified using atom fractions or
weight fractions. For example, to create a material and add Gd152 at 0.5 atom
percent, you’d run:
mat = openmc.Material()
mat.add_nuclide('Gd152', 0.5, 'ao')
The third argument to Material.add_nuclide()
can also be ‘wo’ for weight
percent. The densities specified for each nuclide/element are relative and are
renormalized based on the total density of the material. The total density is
set using the Material.set_density()
method. The density can be specified
in gram per cubic centimeter (‘g/cm3’), atom per barn-cm (‘atom/b-cm’), or
kilogram per cubic meter (‘kg/m3’), e.g.,
mat.set_density('g/cm3', 4.5)
Natural Elements¶
The Material.add_element()
method works exactly the same as
Material.add_nuclide()
, except that instead of specifying a single isotope
of an element, you specify the element itself. For example,
mat.add_element('C', 1.0)
Internally, OpenMC stores data on the atomic masses and natural abundances of all known isotopes and then uses this data to determine what isotopes should be added to the material. When the material is later exported to XML for use by the openmc executable, you’ll see that any natural elements are expanded to the naturally-occurring isotopes.
Often, cross section libraries don’t actually have all naturally-occurring
isotopes for a given element. For example, in ENDF/B-VII.1, cross section
evaluations are given for O16 and O17 but not for O18. If OpenMC is aware of
what cross sections you will be using (either through the
Materials.cross_sections
attribute or the
OPENMC_CROSS_SECTIONS
environment variable), it will attempt to only
put isotopes in your model for which you have cross section data. In the case of
oxygen in ENDF/B-VII.1, the abundance of O18 would end up being lumped with O16.
Thermal Scattering Data¶
If you have a moderating material in your model like water or graphite, you
should assign thermal scattering data (so-called \(S(\alpha,\beta)\)) using
the Material.add_s_alpha_beta()
method. For example, to model light water,
you would need to add hydrogen and oxygen to a material and then assign the
c_H_in_H2O
thermal scattering data:
water = openmc.Material()
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
water.add_s_alpha_beta('c_H_in_H2O')
water.set_density('g/cm3', 1.0)
Naming Conventions¶
OpenMC uses the GND naming convention for nuclides, metastable states, and compounds:
Nuclides: | SymA where “A” is the mass number (e.g., Fe56 ) |
---|---|
Elements: | Sym0 (e.g., Fe0 or C0 ) |
Excited states: | SymA_eN (e.g., V51_e1 for the first excited state of
Vanadium-51.) This is only used in decay data. |
Metastable states: | |
SymA_mN (e.g., Am242_m1 for the first excited state
of Americium-242). |
|
Compounds: | c_String_Describing_Material (e.g., c_H_in_H2O ). Used for
thermal scattering data. |
Important
The element syntax, e.g., C0
, is only used when the cross
section evaluation is an elemental evaluation, like carbon in
ENDF/B-VII.1! If you are adding an element via
Material.add_element()
, just use Sym
.
Temperature¶
Some Monte Carlo codes define temperature implicitly through the cross section data, which is itself given only at a particular temperature. In OpenMC, the material definition is decoupled from the specification of temperature. Instead, temperatures are assigned to cells directly. Alternatively, a default temperature can be assigned to a material that is to be applied to any cell where the material is used. In the absence of any cell or material temperature specification, a global default temperature can be set that is applied to all cells and materials. Anytime a material temperature is specified, it will override the global default temperature. Similarly, anytime a cell temperatures is specified, it will override the material or global default temperature. All temperatures should be given in units of Kelvin.
To assign a default material temperature, one should use the temperature
attribute, e.g.,
hot_fuel = openmc.Material()
hot_fuel.temperature = 1200.0 # temperature in Kelvin
Warning
MCNP users should be aware that OpenMC does not use the concept of
cross section suffixes like “71c” or “80c”. Temperatures in Kelvin
should be assigned directly per material or per cell using the
Material.temperature
or Cell.temperature
attributes, respectively.
Material Collections¶
The openmc executable expects to find a materials.xml
file
when it is run. To create this file, one needs to instantiate the
openmc.Materials
class and add materials to it. The Materials
class acts like a list (in fact, it is a subclass of Python’s built-in
list
class), so materials can be added by passing a list to the
constructor, using methods like append()
, or through the operator
+=
. Once materials have been added to the collection, it can be exported
using the Materials.export_to_xml()
method.
materials = openmc.Materials()
materials.append(water)
materials += [uo2, zircaloy]
materials.export_to_xml()
# This is equivalent
materials = openmc.Materials([water, uo2, zircaloy])
materials.export_to_xml()
Cross Sections¶
OpenMC uses a file called cross_sections.xml to
indicate where cross section data can be found on the filesystem. This file
serves the same role that xsdir
does for MCNP or xsdata
does for
Serpent. Information on how to generate a cross section listing file can be
found in Manually Creating a Library from ACE files. Once you have a cross sections file that has
been generated, you can tell OpenMC to use this file either by setting
Materials.cross_sections
or by setting the
OPENMC_CROSS_SECTIONS
environment variable to the path of the
cross_sections.xml
file. The former approach would look like:
materials.cross_sections = '/path/to/cross_sections.xml'
Defining Geometry¶
Surfaces and Regions¶
The geometry of a model in OpenMC is defined using constructive solid geometry (CSG), also sometimes referred to as combinatorial geometry. CSG allows a user to create complex regions using Boolean operators (intersection, union, and complement) on simpler regions. In order to define a region that we can assign to a cell, we must first define surfaces which bound the region. A surface is a locus of zeros of a function of Cartesian coordinates \(x,y,z\), e.g.
- A plane perpendicular to the \(x\) axis: \(x - x_0 = 0\)
- A cylinder perpendicular to the \(z\) axis: \((x - x_0)^2 + (y - y_0)^2 - R^2 = 0\)
- A sphere: \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\)
Defining a surface alone is not sufficient to specify a volume – in order to define an actual volume, one must reference the half-space of a surface. A surface half-space is the region whose points satisfy a positive of negative inequality of the surface equation. For example, for a sphere of radius one centered at the origin, the surface equation is \(f(x,y,z) = x^2 + y^2 + z^2 - 1 = 0\). Thus, we say that the negative half-space of the sphere, is defined as the collection of points satisfying \(f(x,y,z) < 0\), which one can reason is the inside of the sphere. Conversely, the positive half-space of the sphere would correspond to all points outside of the sphere, satisfying \(f(x,y,z) > 0\).
In the Python API, surfaces are created via subclasses of
openmc.Surface
. The available surface types and their corresponding
classes are listed in the following table.
Surface | Equation | Class |
---|---|---|
Plane perpendicular to \(x\)-axis | \(x - x_0 = 0\) | openmc.XPlane |
Plane perpendicular to \(y\)-axis | \(y - y_0 = 0\) | openmc.YPlane |
Plane perpendicular to \(z\)-axis | \(z - z_0 = 0\) | openmc.ZPlane |
Arbitrary plane | \(Ax + By + Cz = D\) | openmc.Plane |
Infinite cylinder parallel to \(x\)-axis | \((y-y_0)^2 + (z-z_0)^2 - R^2 = 0\) | openmc.XCylinder |
Infinite cylinder parallel to \(y\)-axis | \((x-x_0)^2 + (z-z_0)^2 - R^2 = 0\) | openmc.YCylinder |
Infinite cylinder parallel to \(z\)-axis | \((x-x_0)^2 + (y-y_0)^2 - R^2 = 0\) | openmc.ZCylinder |
Sphere | \((x-x_0)^2 + (y-y_0)^2 + (z-z_0)^2 - R^2 = 0\) | openmc.Sphere |
Cone parallel to the \(x\)-axis | \((y-y_0)^2 + (z-z_0)^2 - R^2(x-x_0)^2 = 0\) | openmc.XCone |
Cone parallel to the \(y\)-axis | \((x-x_0)^2 + (z-z_0)^2 - R^2(y-y_0)^2 = 0\) | openmc.YCone |
Cone parallel to the \(z\)-axis | \((x-x_0)^2 + (y-y_0)^2 - R^2(z-z_0)^2 = 0\) | openmc.ZCone |
General quadric surface | \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\) | openmc.Quadric |
Each surface is characterized by several parameters. As one example, the parameters for a sphere are the \(x,y,z\) coordinates of the center of the sphere and the radius of the sphere. All of these parameters can be set either as optional keyword arguments to the class constructor or via attributes:
sphere = openmc.Sphere(R=10.0)
# This is equivalent
sphere = openmc.Sphere()
sphere.r = 10.0
Once a surface has been created, half-spaces can be obtained by applying the
unary -
or +
operators, corresponding to the negative and positive
half-spaces, respectively. For example:
>>> sphere = openmc.Sphere(R=10.0)
>>> inside_sphere = -sphere
>>> outside_sphere = +sphere
>>> type(inside_sphere)
<class 'openmc.surface.Halfspace'>
Instances of openmc.Halfspace
can be combined together using the
Boolean operators &
(intersection), |
(union), and ~
(complement):
>>> inside_sphere = -openmc.Sphere()
>>> above_plane = +openmc.ZPlane()
>>> northern_hemisphere = inside_sphere & above_plane
>>> type(northern_hemisphere)
<class 'openmc.region.Intersection'>
For many regions, a bounding-box can be determined automatically:
>>> northern_hemisphere.bounding_box
(array([-1., -1., 0.]), array([1., 1., 1.]))
While a bounding box can be determined for regions involving half-spaces of
spheres, cylinders, and axis-aligned planes, it generally cannot be determined
if the region involves cones, non-axis-aligned planes, or other exotic
second-order surfaces. For example, the openmc.get_hexagonal_prism()
function returns the interior region of a hexagonal prism; because it is bounded
by a openmc.Plane
, trying to get its bounding box won’t work:
>>> hex = openmc.get_hexagonal_prism()
>>> hex.bounding_box
(array([-0.8660254, -inf, -inf]),
array([ 0.8660254, inf, inf]))
Boundary Conditions¶
When a surface is created, by default particles that pass through the surface
will consider it to be transmissive, i.e., they pass through the surface
freely. If your model does not extend to infinity in all spatial dimensions, you
may want to specify different behavior for particles passing through a
surface. To specify a vacuum boundary condition, simply change the
Surface.boundary_type
attribute to ‘vacuum’:
outer_surface = openmc.Sphere(R=100.0, boundary_type='vacuum')
# This is equivalent
outer_surface = openmc.Sphere(R=100.0)
outer_surface.boundary_type = 'vacuum'
Reflective and periodic boundary conditions can be set with the strings ‘reflective’ and ‘periodic’. Vacuum and reflective boundary conditions can be applied to any type of surface. Periodic boundary conditions can only be applied to pairs of axis-aligned planar surfaces.
Cells¶
Once you have a material created and a region of space defined, you need to
define a cell that assigns the material to the region. Cells are created using
the openmc.Cell
class:
fuel = openmc.Cell(fill=uo2, region=pellet)
# This is equivalent
fuel = openmc.Cell()
fuel.fill = uo2
fuel.region = pellet
In this example, an instance of openmc.Material
is assigned to the
Cell.fill
attribute. One can also fill a cell with a universe or lattice.
The classes Halfspace
, Intersection
, Union
, and
Complement
and all instances of openmc.Region
and can be
assigned to the Cell.region
attribute.
Universes¶
Similar to MCNP and Serpent, OpenMC is capable of using universes, collections
of cells that can be used as repeatable units of geometry. At a minimum, there
must be one “root” universe present in the model. To define a universe, an
instance of openmc.Universe
is created and then cells can be added
using the Universe.add_cells()
or Universe.add_cell()
methods. Alternatively, a list of cells can be specified in the constructor:
universe = openmc.Universe(cells=[cell1, cell2, cell3])
# This is equivalent
universe = openmc.Universe()
universe.add_cells([cell1, cell2])
universe.add_cell(cell3)
Universes are generally used in three ways:
- To be assigned to a
Geometry
object (see Exporting a Geometry Model), - To be assigned as the fill for a cell via the
Cell.fill
attribute, and - To be used in a regular arrangement of universes in a lattice.
Once a universe is constructed, it can actually be used to determine what cell
or material is found at a given location by using the Universe.find()
method, which returns a list of universes, cells, and lattices which are
traversed to find a given point. The last element of that list would contain the
lowest-level cell at that location:
>>> universe.find((0., 0., 0.))[-1]
Cell
ID = 10000
Name = cell 1
Fill = Material 10000
Region = -10000
Rotation = None
Temperature = None
Translation = None
As you are building a geometry, it is also possible to display a plot of single
universe using the Universe.plot()
method. This method requires that you
have matplotlib installed.
Lattices¶
Many particle transport models involve repeated structures that occur in a
regular pattern such as a rectangular or hexagonal lattice. In such a case, it
would be cumbersome to have to define the boundaries of each of the cells to be
filled with a universe. OpenMC provides a means to define lattice structures
through the openmc.RectLattice
and openmc.HexLattice
classes.
Rectangular Lattices¶
A rectangular lattice defines a two-dimensional or three-dimensional array of universes that are filled into rectangular prisms (lattice elements) each of which has the same width, length, and height. To completely define a rectangular lattice, one needs to specify
- The coordinates of the lower-left corner of the lattice
(
RectLattice.lower_left
), - The pitch of the lattice, i.e., the distance between the center of adjacent
lattice elements (
RectLattice.pitch
), - What universes should fill each lattice element
(
RectLattice.universes
), and - A universe that is used to fill any lattice position outside the well-defined
portion of the lattice (
RectLattice.outer
).
For example, to create a 3x3 lattice centered at the origin in which each
lattice element is 5cm by 5cm and is filled by a universe u
, one could run:
lattice = openmc.RectLattice()
lattice.lower_left = (-7.5, -7.5)
lattice.pitch = (5.0, 5.0)
lattice.universes = [[u, u, u],
[u, u, u],
[u, u, u]]
Note that because this is a two-dimensional lattice, the lower-left coordinates
and pitch only need to specify the \(x,y\) values. The order that the
universes appear is such that the first row corresponds to lattice elements with
the highest \(y\) -value. Note that the RectLattice.universes
attribute expects a doubly-nested iterable of type openmc.Universe
—
this can be normal Python lists, as shown above, or a NumPy array can be used as
well:
lattice.universes = np.tile(u, (3, 3))
For a three-dimensional lattice, the \(x,y,z\) coordinates of the lower-left
coordinate need to be given and the pitch should also give dimensions for all
three axes. For example, to make a 3x3x3 lattice where the bottom layer is
universe u
, the middle layer is universe q
and the top layer is universe
z
would look like:
lat3d = openmc.RectLattice()
lat3d.lower_left = (-7.5, -7.5, -7.5)
lat3d.pitch = (5.0, 5.0, 5.0)
lat3d.universes = [
[[u, u, u],
[u, u, u],
[u, u, u]],
[[q, q, q],
[q, q, q],
[q, q, q]],
[[z, z, z],
[z, z, z]
[z, z, z]]]
Again, using NumPy can make things easier:
lat3d.universes = np.empty((3, 3, 3), dtype=openmc.Universe)
lat3d.universes[0, ...] = u
lat3d.universes[1, ...] = q
lat3d.universes[2, ...] = z
Finally, it’s possible to specify that lattice positions that aren’t normally
without the bounds of the lattice be filled with an “outer” universe. This
allows one to create a truly infinite lattice if desired. An outer universe is
set with the RectLattice.outer
attribute.
Hexagonal Lattices¶
OpenMC also allows creation of 2D and 3D hexagonal lattices. Creating a hexagonal lattice is similar to creating a rectangular lattice with a few differences:
- The center of the lattice must be specified (
HexLattice.center
). - For a 2D hexagonal lattice, a single value for the pitch should be specified, although it still needs to appear in a list. For a 3D hexagonal lattice, the pitch in the radial and axial directions should be given.
- For a hexagonal lattice, the
HexLattice.universes
attribute cannot be given as a NumPy array for reasons explained below. - As with rectangular lattices, the
HexLattice.outer
attribute will specify an outer universe.
For a 2D hexagonal lattice, the HexLattice.universes
attribute should be
set to a two-dimensional list of universes filling each lattice element. Each
sub-list corresponds to one ring of universes and is ordered from the outermost
ring to the innermost ring. The universes within each sub-list are ordered from
the “top” (position with greatest y value) and proceed in a clockwise fashion
around the ring. The HexLattice.show_indices()
static method can be used
to help figure out how to place universes:
>>> print(openmc.HexLattice.show_indices(3))
(0, 0)
(0,11) (0, 1)
(0,10) (1, 0) (0, 2)
(1, 5) (1, 1)
(0, 9) (2, 0) (0, 3)
(1, 4) (1, 2)
(0, 8) (1, 3) (0, 4)
(0, 7) (0, 5)
(0, 6)
Note that by default, hexagonal lattices are positioned such that each lattice
element has two faces that are parallel to the \(y\) axis. As one example,
to create a three-ring lattice centered at the origin with a pitch of 10 cm
where all the lattice elements centered along the \(y\) axis are filled with
universe u
and the remainder are filled with universe q
, the following
code would work:
hexlat = openmc.HexLattice()
hexlat.center = (0, 0)
hexlat.pitch = [10]
outer_ring = [u, q, q, q, q, q, u, q, q, q, q, q]
middle_ring = [u, q, q, u, q, q]
inner_ring = [u]
hexlat.universes = [outer_ring, middle_ring, inner_ring]
If you need to create a hexagonal boundary (composed of six planar surfaces) for
a hexagonal lattice, openmc.get_hexagonal_prism()
can be used.
Exporting a Geometry Model¶
Once you have finished building your geometry by creating surfaces, cell, and,
if needed, lattices, the last step is to create an instance of
openmc.Geometry
and export it to an XML file that the
openmc executable can read using the
Geometry.export_to_xml()
method. This can be done as follows:
geom = openmc.Geometry(root_univ)
geom.export_to_xml()
# This is equivalent
geom = openmc.Geometry()
geom.root_universe = root_univ
geom.export_to_xml()
Execution Settings¶
Once you have created the materials and geometry for your simulation, the last
step to have a complete model is to specify execution settings through the
openmc.Settings
class. At a minimum, you need to specify a source
distribution and how many particles to run. Many other execution settings can be set using the
openmc.Settings
object, but they are generally optional.
Run Modes¶
The Settings.run_mode
attribute controls what run mode is used when
openmc is executed. There are five different run modes that can
be specified:
- ‘eigenvalue’
- Runs a \(k\) eigenvalue simulation. See Eigenvalue Calculations for a
full description of eigenvalue calculations. In this mode, the
Settings.source
specifies a starting source that is only used for the first fission generation. - ‘fixed source’
- Runs a fixed-source calculation with a specified external source, specified in
the
Settings.source
attribute. - ‘volume’
- Runs a stochastic volume calculation.
- ‘plot’
- Generates slice or voxel plots (see Geometry Visualization).
- ‘particle_restart’
- Simulate a single source particle using a particle restart file.
So, for example, to specify that OpenMC should be run in fixed source mode, you
would need to instantiate a openmc.Settings
object and assign the
Settings.run_mode
attribute:
settings = openmc.Settings()
settings.run_mode = 'fixed source'
If you don’t specify a run mode, the default run mode is ‘eigenvalue’.
Number of Particles¶
For a fixed source simulation, the total number of source particle histories
simulated is broken up into a number of batches, each corresponding to a
realization of the tally random variables. Thus, you
need to specify both the number of batches (Settings.batches
) as well as
the number of particles per batch (Settings.particles
).
For a \(k\) eigenvalue simulation, particles are grouped into fission
generations, as described in Eigenvalue Calculations. Successive fission
generations can be combined into a batch for statistical purposes. By default, a
batch will consist of only a single fission generation, but this can be changed
with the Settings.generations_per_batch
attribute. For problems with a
high dominance ratio, using multiple generations per batch can help reduce
underprediction of variance, thereby leading to more accurate confidence
intervals. Tallies should not be scored to until the source distribution
converges, as described in Method of Successive Generations, which may take
many generations. To specify the number of batches that should be discarded
before tallies begin to accumulate, use the Settings.inactive
attribute.
The following example shows how one would simulate 10000 particles per generation, using 10 generations per batch, 150 total batches, and discarding 5 batches. Thus, a total of 145 active batches (or 1450 generations) will be used for accumulating tallies.
settings.particles = 10000
settings.generations_per_batch = 10
settings.batches = 150
settings.inactive = 5
External Source Distributions¶
External source distributions can be specified through the
Settings.source
attribute. If you have a single external source, you can
create an instance of openmc.Source
and use it to set the
Settings.source
attribute. If you have multiple external sources with
varying source strengths, Settings.source
should be set to a list of
openmc.Source
objects.
The openmc.Source
class has three main attributes that one can set:
Source.space
, which defines the spatial distribution,
Source.angle
, which defines the angular distribution, and
Source.energy
, which defines the energy distribution.
The spatial distribution can be set equal to a sub-class of
openmc.stats.Spatial
; common choices are openmc.stats.Point
or
openmc.stats.Box
. To independently specify distributions in the
\(x\), \(y\), and \(z\) coordinates, you can use
openmc.stats.CartesianIndependent
.
The angular distribution can be set equal to a sub-class of
openmc.stats.UnitSphere
such as openmc.stats.Isotropic
,
openmc.stats.Monodirectional
, or
openmc.stats.PolarAzimuthal
. By default, if no angular distribution is
specified, an isotropic angular distribution is used.
The energy distribution can be set equal to any univariate probability
distribution. This could be a probability mass function
(openmc.stats.Discrete
), a Watt fission spectrum
(openmc.stats.Watt
), or a tabular distribution
(openmc.stats.Tabular
). By default, if no energy distribution is
specified, a Watt fission spectrum with \(a\) = 0.988 MeV and \(b\) =
2.249 MeV -1 is used.
As an example, to create an isotropic, 10 MeV monoenergetic source uniformly distributed over a cube centered at the origin with an edge length of 10 cm, one would run:
source = openmc.Source()
source.space = openmc.stats.Box((-5, -5, -5), (5, 5, 5))
source.angle = openmc.stats.Isotropic()
source.energy = openmc.stats.Discrete([10.0e6], [1.0])
settings.source = source
The openmc.Source
class also has a Source.strength
attribute
that indicates the relative strength of a source distribution if multiple are
used. For example, to create two sources, one that should be sampled 70% of the
time and another that should be sampled 30% of the time:
src1 = openmc.Source()
src1.strength = 0.7
...
src2 = openmc.Source()
src2.strength = 0.3
...
settings.source = [src1, src2]
For a full list of all classes related to statistical distributions, see openmc.stats – Statistics.
Shannon Entropy¶
To assess convergence of the source distribution, the scalar Shannon entropy
metric is often used in Monte Carlo codes. OpenMC also allows you to calculate
Shannon entropy at each generation over a specified mesh, created using the
openmc.Mesh
class. After instantiating a Mesh
, you need to
specify the lower-left coordinates of the mesh (Mesh.lower_left
), the
number of mesh cells in each direction (Mesh.dimension
) and either the
upper-right coordinates of the mesh (Mesh.upper_right
) or the width of
each mesh cell (Mesh.width
). Once you have a mesh, simply assign it to
the Settings.entropy_mesh
attribute.
entropy_mesh = openmc.Mesh()
entropy_mesh.lower_left = (-50, -50, -25)
entropy_mesh.upper_right = (50, 50, 25)
entropy_mesh.dimension = (8, 8, 8)
settings.entropy_mesh = entropy_mesh
If you’re unsure of what bounds to use for the entropy mesh, you can try getting
a bounding box for the entire geometry using the Geometry.bounding_box
property:
geom = openmc.Geometry()
...
m = openmc.Mesh()
m.lower_left, m.upper_right = geom.bounding_box
m.dimension = (8, 8, 8)
settings.entropy_mesh = m
Generation of Output Files¶
A number of attributes of the openmc.Settings
class can be used to
control what files are output and how often. First, there is the
Settings.output
attribute which takes a dictionary having keys
‘summary’, ‘tallies’, and ‘path’. The first two keys controls whether a
summary.h5
and tallies.out
file are written, respectively (see
Viewing and Analyzing Results for a description of those files). By default, output files
are written to the current working directory; this can be changed by setting the
‘path’ key. For example, if you want to disable the tallies.out
file and
write the summary.h5
to a directory called ‘results’, you’d specify the
Settings.output
dictionary as:
settings.output = {
'tallies': False,
'path': 'results'
}
Generation of statepoint and source files is handled separately through the
Settings.statepoint
and Settings.sourcepoint
attributes. Both of
those attributes expect dictionaries and have a ‘batches’ key which indicates at
which batches statepoints and source files should be written. Note that by
default, the source is written as part of the statepoint file; this behavior can
be changed by the ‘separate’ and ‘write’ keys of the
Settings.sourcepoint
dictionary, the first of which indicates whether
the source should be written to a separate file and the second of which
indicates whether the source should be written at all.
As an example, to write a statepoint file every five batches:
settings.batches = n
settings.statepoint = {'batches': range(5, n + 5, 5)}
Specifying Tallies¶
In order to obtain estimates of physical quantities in your simulation, you need
to create one or more tallies using the openmc.Tally
class. As
explained in detail in the theory manual, tallies
provide estimates of a scoring function times the flux integrated over some
region of phase space, as in:
Thus, to specify a tally, we need to specify what regions of phase space should be included when deciding whether to score an event as well as what the scoring function (\(f\) in the above equation) should be used. The regions of phase space are called filters and the scoring functions are simply called scores.
Filters¶
To specify the regions of phase space, one must create a
openmc.Filter
. Since openmc.Filter
is an abstract class, you
actually need to instantiate one of its sub-classes (for a full listing, see
Constructing Tallies). For example, to indicate that events that occur in a
given cell should score to the tally, we would create a
openmc.CellFilter
:
cell_filter = openmc.CellFilter([fuel.id, moderator.id, reflector.id])
Another commonly used filter is openmc.EnergyFilter
, which specifies
multiple energy bins over which events should be scored. Thus, if we wanted to
tally events where the incident particle has an energy in the ranges [0 eV, 4
eV] and [4 eV, 1 MeV], we would do the following:
energy_filter = openmc.EnergyFilter([0.0, 4.0, 1.0e6])
Energies are specified in eV and need to be monotonically increasing.
Caution
An energy bin between zero and the lowest energy specified is not included by default as it is in MCNP.
Once you have created a filter, it should be assigned to a openmc.Tally
instance through the Tally.filters
attribute:
tally.filters.append(cell_filter)
tally.filters.append(energy_filter)
# This is equivalent
tally.filters = [cell_filter, energy_filter]
Note
You are actually not required to assign any filters to a tally. If you create a tally with no filters, all events will score to the tally. This can be useful if you want to know, for example, a reaction rate over your entire model.
Scores¶
To specify the scoring functions, a list of strings needs to be given to the
Tally.scores
attribute. You can score the flux (‘flux’), a reaction rate
(‘total’, ‘fission’, etc.), or even scattering moments (e.g., ‘scatter-P3’). For
example, to tally the elastic scattering rate and the fission neutron
production, you’d assign:
tally.scores = ['elastic', 'nu-fission']
With no further specification, you will get the total elastic scattering rate
and the total fission neutron production. If you want reaction rates for a
particular nuclide or set of nuclides, you can set the Tally.nuclides
attribute to a list of strings indicating which nuclides. The nuclide names
should follow the same naming convention as that used
for material specification. If we wanted the reaction rates only for U235 and
U238 (separately), we’d set:
tally.nuclides = ['U235', 'U238']
You can also list ‘all’ as a nuclide which will give you a separate reaction rate for every nuclide in the model.
The following tables show all valid scores:
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. |
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. |
Score | Description |
---|---|
delayed-nu-fission | Total production of delayed neutrons due to fission. |
prompt-nu-fission | Total production of prompt neutrons due to fission. |
nu-fission | Total production of neutrons due to fission. |
nu-scatter, nu-scatter-N, nu-scatter-PN, nu-scatter-YN | These scores are similar in functionality to their
scatter* equivalents except the total
production of neutrons due to scattering is scored
vice simply the scattering rate. This accounts for
multiplicity from (n,2n), (n,3n), and (n,4n)
reactions. |
Score | Description |
---|---|
current | Partial currents on the boundaries of each cell in a mesh. Units are particles per source particle. Note that this score can only be used if a mesh filter has been specified. Furthermore, it may not be used in conjunction with any other score. |
events | Number of scoring events. Units are events per source particle. |
inverse-velocity | The flux-weighted inverse velocity where the velocity is in units of centimeters per second. |
kappa-fission | The recoverable energy production rate due to fission. The recoverable energy is defined as the fission product kinetic energy, prompt and delayed neutron kinetic energies, prompt and delayed \(\gamma\)-ray total energies, and the total energy released by the delayed \(\beta\) particles. The neutrino energy does not contribute to this response. The prompt and delayed \(\gamma\)-rays are assumed to deposit their energy locally. Units are eV per source particle. |
fission-q-prompt | The prompt fission energy production rate. This energy comes in the form of fission fragment nuclei, prompt neutrons, and prompt \(\gamma\)-rays. This value depends on the incident energy and it requires that the nuclear data library contains the optional fission energy release data. Energy is assumed to be deposited locally. Units are eV per source particle. |
fission-q-recoverable | The recoverable fission energy production rate. This energy comes in the form of fission fragment nuclei, prompt and delayed neutrons, prompt and delayed \(\gamma\)-rays, and delayed \(\beta\)-rays. This tally differs from the kappa-fission tally in that it is dependent on incident neutron energy and it requires that the nuclear data library contains the optional fission energy release data. Energy is assumed to be deposited locally. Units are eV per source paticle. |
decay-rate | The delayed-nu-fission-weighted decay rate where the decay rate is in units of inverse seconds. |
Geometry Visualization¶
OpenMC is capable of producing two-dimensional slice plots of a geometry as well
as three-dimensional voxel plots using the geometry plotting run mode. The geometry plotting mode relies on the presence of a
plots.xml file that indicates what plots should be created. To
create this file, one needs to create one or more openmc.Plot
instances, add them to a openmc.Plots
collection, and then use the
Plots.export_to_xml
method to write the plots.xml
file.
Slice Plots¶

By default, when an instance of openmc.Plot
is created, it indicates
that a 2D slice plot should be made. You can specify the origin of the plot
(Plot.origin
), the width of the plot in each direction
(Plot.width
), the number of pixels to use in each direction
(Plot.pixels
), and the basis directions for the plot. For example, to
create a \(x\) - \(z\) plot centered at (5.0, 2.0, 3.0) with a width of
(50., 50.) and 400x400 pixels:
plot = openmc.Plot()
plot.basis = 'xz'
plot.origin = (5.0, 2.0, 3.0)
plot.width = (50., 50.)
plot.pixels = (400, 400)
The color of each pixel is determined by placing a particle at the center of
that pixel and using OpenMC’s internal find_cell
routine (the same one used
for particle tracking during simulation) to determine the cell and material at
that location.
Note
In this example, pixels are 50/400=0.125 cm wide. Thus, this plot may miss any features smaller than 0.125 cm, since they could exist between pixel centers. More pixels can be used to resolve finer features but will result in larger files.
By default, a unique color will be assigned to each cell in the geometry. If you
want your plot to be colored by material instead, change the
Plot.color_by
attribute:
plot.color_by = 'material'
If you don’t like the random colors assigned, you can also indicate that particular cells/materials should be given colors of your choosing:
plot.colors = {
water: 'blue',
clad: 'black'
}
# This is equivalent
plot.colors = {
water: (0, 0, 255),
clad: (0, 0, 0)
}
Note that colors can be given as RGB tuples or by a string indicating a valid SVG color.
When you’re done creating your openmc.Plot
instances, you need to then
assign them to a openmc.Plots
collection and export it to XML:
plots = openmc.Plots([plot1, plot2, plot3])
plots.export_to_xml()
# This is equivalent
plots = openmc.Plots()
plots.append(plot1)
plots += [plot2, plot3]
plots.export_to_xml()
To actually generate the plots, run the openmc.plot_geometry()
function. Alternatively, run the openmc executable with the
--plot
command-line flag. When that has finished, you will have one or more
.ppm
files, i.e., portable pixmap files. On some Linux
distributions, these .ppm
files are natively viewable. If you find that
you’re unable to open them on your system (or you don’t like the fact that they
are not compressed), you may want to consider converting them to another format.
This is easily accomplished with the convert
command available on most Linux
distributions as part of the ImageMagick package. (On Debian
derivatives: sudo apt install imagemagick
). Images are then converted like:
convert myplot.ppm myplot.png
Alternatively, if you’re working within a Jupyter
Notebook or QtConsole, you can use the openmc.plot_inline()
to run OpenMC
in plotting mode and display the resulting plot within the notebook.
Voxel Plots¶

The openmc.Plot
class can also be told to generate a 3D voxel plot
instead of a 2D slice plot. Simply change the Plot.type
attribute to
‘voxel’. In this case, the Plot.width
and Plot.pixels
attributes
should be three items long, e.g.:
vox_plot = openmc.Plot()
vox_plot.type = 'voxel'
vox_plot.width = (100., 100., 50.)
vox_plot.pixels = (400, 400, 200)
The voxel plot data is written to an HDF5 file. The voxel file can subsequently be converted into a standard mesh format that can be viewed in ParaView, VisIt, etc. This typically will compress the size of the file significantly. The provided openmc-voxel-to-silovtk script can convert the HDF5 voxel file to VTK or SILO formats. Once processed into a standard 3D file format, colors and masks can be defined using the stored ID numbers to better explore the geometry. The process for doing this will depend on the 3D viewer, but should be straightforward.
Note
3D voxel plotting can be very computer intensive for the viewing program (Visit, ParaView, etc.) if the number of voxels is large (>10 million or so). Thus if you want an accurate picture that renders smoothly, consider using only one voxel in a certain direction.
Executables and Scripts¶
openmc
¶
Once you have a model built (see Basics of Using OpenMC), you can either run
the openmc executable directly from the directory containing your XML input
files, or you can specify as a command-line argument the directory containing
the XML input files. For example, if your XML input files are in the directory
/home/username/somemodel/
, one way to run the simulation would be:
cd /home/username/somemodel
openmc
Alternatively, you could run from any directory:
openmc /home/username/somemodel
Note that in the latter case, any output files will be placed in the present
working directory which may be different from
/home/username/somemodel
. openmc
accepts the following command line
flags:
-c, --volume | Run in stochastic volume calculation mode |
-g, --geometry-debug | |
Run in geometry debugging mode, where cell overlaps are checked for after each move of a particle | |
-n, --particles N | |
Use N particles per generation or batch | |
-p, --plot | Run in plotting mode |
-r, --restart file | |
Restart a previous run from a state point or a particle restart file | |
-s, --threads N | |
Run with N OpenMP threads | |
-t, --track | Write tracks for all particles |
-v, --version | Show version information |
-h, --help | Show help message |
Note
If you’re using the Python API, openmc.run()
is equivalent to
running openmc
from the command line.
openmc-ace-to-hdf5
¶
This script can be used to create HDF5 nuclear data libraries used by OpenMC if you have existing ACE files. There are four different ways you can specify ACE libraries that are to be converted:
- List each ACE library as a positional argument. This is very useful in
conjunction with the usual shell utilities (
ls
,find
, etc.). - Use the
--xml
option to specify a pre-v0.9 cross_sections.xml file. - Use the
--xsdir
option to specify a MCNP xsdir file. - Use the
--xsdata
option to specify a Serpent xsdata file.
The script does not use any extra information from cross_sections.xml/ xsdir/
xsdata files to determine whether the nuclide is metastable. Instead, the
--metastable
argument can be used to specify whether the ZAID naming convention
follows the NNDC data convention (1000*Z + A + 300 + 100*m), or the MCNP data
convention (essentially the same as NNDC, except that the first metastable state
of Am242 is 95242 and the ground state is 95642).
The optional --fission_energy_release
argument will accept an HDF5 file
containing a library of fission energy release (ENDF MF=1 MT=458) data. A
library built from ENDF/B-VII.1 data is released with OpenMC and can be found at
openmc/data/fission_Q_data_endb71.h5. This data is necessary for
‘fission-q-prompt’ and ‘fission-q-recoverable’ tallies, but is not needed
otherwise.
-h, --help | show help message and exit |
-d DESTINATION, --destination DESTINATION | |
Directory to create new library in | |
-m META, --metastable META | |
How to interpret ZAIDs for metastable nuclides. META can be either ‘nndc’ or ‘mcnp’. (default: nndc) | |
--xml XML | Old-style cross_sections.xml that lists ACE libraries |
--xsdir XSDIR | MCNP xsdir file that lists ACE libraries |
--xsdata XSDATA | |
Serpent xsdata file that lists ACE libraries | |
--fission_energy_release FISSION_ENERGY_RELEASE | |
HDF5 file containing fission energy release data |
openmc-convert-mcnp70-data
¶
This script converts ENDF/B-VII.0 ACE data from the MCNP5/6 distribution into an HDF5 library that can be used by OpenMC. This assumes that you have a directory containing files named endf70a, endf70b, ..., endf70k, and endf70sab. The path to the directory containing these files should be given as a positional argument. The following optional arguments are available:
-d DESTINATION, --destination DESTINATION | |
Directory to create new library in (Default: mcnp_endfb70) |
openmc-convert-mcnp71-data
¶
This script converts ENDF/B-VII.1 ACE data from the MCNP6 distribution into an HDF5 library that can be used by OpenMC. This assumes that you have a directory containing subdirectories ‘endf71x’ and ‘ENDF71SaB’. The path to the directory containing these subdirectories should be given as a positional argument. The following optional arguments are available:
-d DESTINATION, --destination DESTINATION | |
Directory to create new library in (Default: mcnp_endfb71) | |
-f FER, --fission_energy_release FER | |
HDF5 file containing fission energy release data |
openmc-get-jeff-data
¶
This script downloads JEFF 3.2 ACE data from OECD/NEA and converts it to a multi-temperature HDF5 library for use with OpenMC. It has the following optional arguments:
-b, --batch | Suppress standard in |
-d DESTINATION, --destination DESTINATION | |
Directory to create new library in (default: jeff-3.2-hdf5) |
Warning
This script will download approximately 9 GB of data. Extracting and processing the data may require as much as 40 GB of additional free disk space.
openmc-get-multipole-data
¶
This script downloads and extracts windowed multipole data based on ENDF/B-VII.1. It has the following optional arguments:
-b, --batch | Suppress standard in |
openmc-get-nndc-data
¶
This script downloads ENDF/B-VII.1 ACE data from NNDC and converts it to an HDF5 library for use with OpenMC. This data is used for OpenMC’s regression test suite. This script has the following optional arguments:
-b, --batch | Suppress standard in |
openmc-plot-mesh-tally
¶
openmc-plot-mesh-tally
provides a graphical user interface for plotting mesh
tallies. The path to the statepoint file can be provided as an optional arugment
(if omitted, a file dialog will be presented).
openmc-track-to-vtk
¶
This script converts HDF5 particle track files to VTK
poly data that can be viewed with ParaView or VisIt. The filenames of the
particle track files should be given as posititional arguments. The output
filename can also be changed with the -o
flag:
-o OUT, --out OUT | |
Output VTK poly filename |
openmc-update-inputs
¶
If you have existing XML files that worked in a previous version of OpenMC that
no longer work with the current version, you can try to update these files using
openmc-update-inputs
. If any of the given files do not match the most
up-to-date formatting, then they will be automatically rewritten. The old
out-of-date files will not be deleted; they will be moved to a new file with
‘.original’ appended to their name.
Formatting changes that will be made:
- geometry.xml
- Lattices containing ‘outside’ attributes/tags will be replaced with lattices containing ‘outer’ attributes, and the appropriate cells/universes will be added. Any ‘surfaces’ attributes/elements on a cell will be renamed ‘region’.
- materials.xml
- Nuclide names will be changed from ACE aliases (e.g., Am-242m) to HDF5/GND names (e.g., Am242_m1). Thermal scattering table names will be changed from ACE aliases (e.g., HH2O) to HDF5/GND names (e.g., c_H_in_H2O).
openmc-update-mgxs
¶
This script updates OpenMC’s deprecated multi-group cross section XML files to the latest HDF5-based format.
-i IN, --input IN | |
Input XML file | |
-o OUT, --output OUT | |
Output file in HDF5 format |
openmc-validate-xml
¶
Input files can be checked before executing OpenMC using the
openmc-validate-xml
script which is installed alongside the Python API. Two
command line arguments can be set when running openmc-validate-xml
:
-i, --input-path | |
Location of OpenMC input files. | |
-r, --relaxng-path | |
Location of OpenMC RelaxNG files |
If the RelaxNG path is not set, the script will search for these files because
it expects that the user is either running the script located in the install
directory bin
folder or in src/utils
. Once executed, it will match
OpenMC XML files with their RelaxNG schema and check if they are valid. Below
is a table of the messages that will be printed after each file is checked.
Message | Description |
---|---|
[XML ERROR] | Cannot parse XML file. |
[NO RELAXNG FOUND] | No RelaxNG file found for XML file. |
[NOT VALID] | XML file does not match RelaxNG. |
[VALID] | XML file matches RelaxNG. |
openmc-voxel-to-silovtk
¶
When OpenMC generates voxel plots, they are in an
HDF5 format that is not terribly useful by itself. The
openmc-voxel-to-silovtk
script converts a voxel HDF5 file to VTK or SILO file. For VTK, you need
to have the VTK Python bindings installed. For SILO, you need to have silomesh installed. To convert a voxel file,
simply provide the path to the file:
openmc-voxel-to-silovtk voxel_1.h5
The openmc-voxel-to-silovtk
script also takes the following optional
command-line arguments:
-o, --output | Path to output VTK or SILO file |
-s, --silo | Flag to convert to SILO instead of VTK |
Data Processing and Visualization¶
This section is intended to explain procedures for carrying out common post-processing tasks with OpenMC. While several utilities of varying complexity are provided to help automate the process, the most powerful capabilities for post-processing derive from use of the Python API.
Working with State Points¶
Tally results are saved in both a text file (tallies.out) as well as an HDF5 statepoint file. While the tallies.out file may be fine for simple tallies, in many cases the user requires more information about the tally or the run, or has to deal with a large number of result values (e.g. for mesh tallies). In these cases, extracting data from the statepoint file via the Python API is the preferred method of data analysis and visualization.
Data Extraction¶
A great deal of information is available in statepoint files (See
State Point File Format), all of which is accessible through the Python
API. The openmc.StatePoint
class can load statepoints and access data
as requested; it is used in many of the provided plotting utilities, OpenMC’s
regression test suite, and can be used in user-created scripts to carry out
manipulations of the data.
An example IPython notebook demonstrates how to extract data from a statepoint using the Python API.
Plotting in 2D¶
The IPython notebook example also demonstrates how to plot a mesh tally in two dimensions using the Python API. One can also use the openmc-plot-mesh-tally script which provides an interactive GUI to explore and plot mesh tallies for any scores and filter bins.

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¶

OpenMC can dump particle tracks—the position of particles as they are transported through the geometry. There are two ways to make OpenMC output tracks: all particle tracks through a command line argument or specific particle tracks through settings.xml.
Running openmc with the argument -t
or --track
will cause
a track file to be created for every particle transported in the code. Be
careful as this will produce as many files as there are source particles in your
simulation. To identify a specific particle for which a track should be created,
set the Settings.track
attribute to a tuple containing the batch,
generation, and particle number of the desired particle. For example, to create
a track file for particle 4 of batch 1 and generation 2:
settings = openmc.Settings()
settings.track = (1, 2, 4)
To specify multiple particles, the length of the iterable should be a multiple of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2:
settings.track = (1, 2, 3, 1, 2, 4)
After running OpenMC, the working directory will contain a file of the form “track_(batch #)_(generation #)_(particle #).h5” for each particle tracked. These track files can be converted into VTK poly data files with the openmc-track-to-vtk script.
Source Site Processing¶
For eigenvalue problems, OpenMC will store information on the fission source
sites in the statepoint file by default. For each source site, the weight,
position, sampled direction, and sampled energy are stored. To extract this data
from a statepoint file, the openmc.statepoint
module can be used. An
example IPython notebook demontrates how to
analyze and plot source information.
Running in Parallel¶
If you are running a simulation on a computer with multiple cores, multiple sockets, or multiple nodes (i.e., a cluster), you can benefit from the fact that OpenMC is able to use all available hardware resources if configured correctly. OpenMC is capable of using both distributed-memory (MPI) and shared-memory (OpenMP) parallelism. If you are on a single-socket workstation or a laptop, using shared-memory parallelism is likely sufficient. On a multi-socket node, cluster, or supercomputer, chances are you will need to use both distributed-memory (across nodes) and shared-memory (within a single node) parallelism.
Distributed-Memory Parallelism (MPI)¶
MPI defines a library specification for message-passing between processes. There are two major implementations of MPI, OpenMPI and MPICH. Both implementations are known to work with OpenMC; there is no obvious reason to prefer one over the other. Building OpenMC with support for MPI requires that you have one of these implementations installed on your system. For instructions on obtaining MPI, see Prerequisites. Once you have an MPI implementation installed, compile OpenMC following Compiling with MPI.
To run a simulation using MPI, openmc needs to be called using the mpiexec wrapper. For example, to run OpenMC using 32 processes:
mpiexec -n 32 openmc
The same thing can be achieved from the Python API by supplying the mpi_args
argument to openmc.run()
:
openmc.run(mpi_args=['mpiexec', '-n', '32'])
Maximizing Performance¶
There are a number of things you can do to ensure that you obtain optimal performance on a machine when running in parallel:
Use OpenMP within each NUMA node. Some large server processors have so many cores that the last level cache is split to reduce memory latency. For example, the Intel Xeon Haswell-EP architecture uses a snoop mode called cluster on die where the L3 cache is split in half. Thus, in general, you should use one MPI process per socket (and OpenMP within each socket), but for these large processors, you will want to go one step further and use one process per NUMA node. The Xeon Phi Knights Landing architecture uses a similar concept called sub NUMA clustering.
Use a sufficiently large number of particles per generation. Between fission generations, a number of synchronization tasks take place. If the number of particles per generation is too low and you are using many processes/threads, the synchronization time may become non-negligible.
Use hardware threading if available.
Use process binding. When running with MPI, you should ensure that processes are bound to a specific hardware region. This can be set using the
-bind-to
(MPICH) or--bind-to
(OpenMPI) option tompiexec
.Turn off generation of tallies.out. For large simulations with millions of tally bins or more, generating this ASCII file might consume considerable time. You can turn off generation of
tallies.out
via theSettings.output
attribute:settings = openmc.Settings() settings.output = {'tallies': False}
Stochastic Volume Calculations¶
OpenMC has a capability to stochastically determine volumes of cells, materials, and universes. The method works by overlaying a bounding box, sampling points from within the box, and seeing what fraction of points were found in a desired domain. The benefit of doing this stochastically (as opposed to equally-spaced points), is that it is possible to give reliable error estimates on each stochastic quantity.
To specify that a volume calculation be run, you first need to create an
instance of openmc.VolumeCalculation
. The constructor takes a list of
cells, materials, or universes; the number of samples to be used; and the
lower-left and upper-right Cartesian coordinates of a bounding box that encloses
the specified domains:
lower_left = (-0.62, -0.62, -50.)
upper_right = (0.62, 0.62, 50.)
vol_calc = openmc.VolumeCalculation([fuel, clad, moderator], 1000000,
lower_left, upper_right)
For domains contained within regions that have simple definitions, OpenMC can sometimes automatically determine a bounding box. In this case, the last two arguments are not necessary. For example,
sphere = openmc.Sphere(R=10.0)
cell = openm.Cell(region=-sphere)
vol_calc = openmc.VolumeCalculation([cell], 1000000)
Of course, the volumes that you need this capability for are often the ones with complex definitions.
Once you have one or more openmc.VolumeCalculation
objects created, you
can then assign then to Settings.volume_calculations
:
settings = openmc.Settings()
settings.volume_calculations = [cell_vol_calc, mat_vol_calc]
To execute the volume calculations, one can either set Settings.run_mode
to ‘volume’ and run openmc.run()
, or alternatively run
openmc.calculate_volumes()
which doesn’t require that
Settings.run_mode
be set.
When your volume calculations have finished, you can load the results using the
VolumeCalculation.load_results()
method on an existing object. If you
don’t have an existing VolumeCalculation
object, you can create one and
load results simultaneously using the VolumeCalculation.from_hdf5()
class
method:
vol_calc = openmc.VolumeCalculation(...)
...
openmc.calculate_volumes()
vol_calc.load_results('volume_1.h5')
# ..or we can create a new object
vol_calc = openmc.VolumeCalculation.from_hdf5('volume_1.h5')
After the results are loaded, volume estimates will be stored in
VolumeCalculation.volumes
. There is also a
VolumeCalculation.atoms_dataframe
attribute that shows stochastic
estimates of the number of atoms of each type of nuclide within the specified
domains along with their uncertainties.
Troubleshooting¶
Problems with Compilation¶
If you are experiencing problems trying to compile OpenMC, first check if the error you are receiving is among the following options.
undefined reference to `_vtab$...¶
If you see this message when trying to compile, the most likely cause is that you are using a compiler that does not support type-bound procedures from Fortran 2003. This affects any version of gfortran prior to 4.6. Downloading and installing the latest gfortran compiler should resolve this problem.
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.
Derived Types and Classes¶
Derived types and classes should have CamelCase names with words not separated by underscores or hyphens.
Indentation¶
Never use tab characters. Indentation should always be applied using spaces. Emacs users should include the following line in their .emacs file:
(setq-default indent-tabs-mode nil)
vim users should include the following line in their .vimrc file:
set expandtab
Use 2 spaces per indentation level. This applies to all constructs such as program, subroutine, function, if, associate, etc. Emacs users should set the variables f90-if-indent, f90-do-indent, f90-continuation-indent, f90-type-indent, f90-associate-indent, and f90-program indent to 2.
Continuation lines should be indented by at least 5 spaces. They may be indented more in order to make the content match the context. For example, either of these are valid continuation indentations:
local_xyz(1) = xyz(1) - (this % lower_left(1) + &
(i_xyz(1) - HALF)*this % pitch(1))
call which_data(scatt_type, get_scatt, get_nuscatt, get_chi_t, get_chi_p, &
get_chi_d, scatt_order)
Whitespace in Expressions¶
Use a single space between arguments to procedures.
Avoid extraneous whitespace in the following situations:
In procedure calls:
Yes: call somesub(x, y(2), z) No: call somesub( x, y( 2 ), z )
In logical expressions, use one space around operators but nowhere else:
Yes: if (variable == 2) then No: if ( variable==2 ) then
The structure component designator %
should be surrounded by one space on
each side.
Do not leave trailing whitespace at the end of a line.
Python¶
Style for Python code should follow PEP8.
Docstrings for functions and methods should follow numpydoc style.
Python code should work with both Python 2.7+ and Python 3.0+.
Use of third-party Python packages should be limited to numpy, scipy, and h5py. Use of other third-party packages must be implemented as optional dependencies rather than required dependencies.
Development Workflow¶
Anyone wishing to make contributions to OpenMC should be fully acquianted and comfortable working with git and GitHub. We assume here that you have git installed on your system, have a GitHub account, and have setup SSH keys to be able to create/push to repositories on GitHub.
Overview¶
Development of OpenMC relies heavily on branching; specifically, we use a branching model sometimes referred to as git flow. If you plan to contribute to OpenMC development, we highly recommend that you read the linked blog post to get a sense of how the branching model works. There are two main branches that always exist: master and develop. The master branch is a stable branch that contains the latest release of the code. The develop branch is where any ongoing development takes place prior to a release and is not guaranteed to be stable. When the development team decides that a release should occur, the develop branch is merged into master.
Trivial changes to the code may be committed directly to the develop branch by a trusted developer. However, most new features should be developed on a branch that branches off of develop. When the feature is completed, a pull request is initiated on GitHub that is then reviewed by a trusted developer. If the pull request is satisfactory, it is then merged into develop. Note that a trusted developer may not review their own pull request (i.e., an independent code review is required).
Code Review Criteria¶
In order to be considered suitable for inclusion in the develop branch, the following criteria must be satisfied for all proposed changes:
- Changes have a clear purpose and are useful.
- Compiles and passes the regression suite with all configurations (This is checked by Travis CI).
- If appropriate, test cases are added to regression suite.
- No memory leaks (checked with valgrind).
- Conforms to the OpenMC style guide.
- No degradation of performance or greatly increased memory usage. This is not a hard rule – in certain circumstances, a performance loss might be acceptable if there are compelling reasons.
- New features/input are documented.
- No unnecessary external software dependencies are introduced.
Contributing¶
Now that you understand the basic development workflow, let’s discuss how an individual to contribute to development. Note that this would apply to both new features and bug fixes. The general steps for contributing are as follows:
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.
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
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.
Issue a pull request from GitHub and select the develop branch of mit-crpg/openmc as the target.
At a minimum, you should describe what the changes you’ve made are and why you are making them. If the changes are related to an oustanding issue, make sure it is cross-referenced.
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.
After the pull request has been thoroughly vetted, it is merged back into the develop branch of mit-crpg/openmc.
OpenMC Test Suite¶
The purpose of this test suite is to ensure that OpenMC compiles using various combinations of compiler flags and options, and that all user input options can be used successfully without breaking the code. The test suite is comprised of regression tests where different types of input files are configured and the full OpenMC code is executed. Results from simulations are compared with expected results. The test suite is comprised of many build configurations (e.g. debug, mpi, hdf5) and the actual tests which reside in sub-directories in the tests directory. We recommend to developers to test their branches before submitting a formal pull request using gfortran and Intel compilers if available.
The test suite is designed to integrate with cmake using ctest. It is configured to run with cross sections from NNDC augmented with 0 K elastic scattering data for select nuclides as well as multipole data. To download the proper data, run the following commands:
wget -O nndc_hdf5.tar.xz $(cat <openmc_root>/.travis.yml | grep anl.box | awk '{print $2}')
tar xJvf nndc_hdf5.tar.xz
export OPENMC_CROSS_SECTIONS=$(pwd)/nndc_hdf5/cross_sections.xml
git clone --branch=master git://github.com/smharper/windowed_multipole_library.git wmp_lib
tar xzvf wmp_lib/multipole_lib.tar.gz
export OPENMC_MULTIPOLE_LIBRARY=$(pwd)/multipole_lib
The test suite can be run on an already existing build using:
cd build
make test
or
cd build
ctest
There are numerous ctest command line options that can be set to have more control over which tests are executed.
Before running the test suite python script, the following environmental variables should be set if the default paths are incorrect:
FC - The command for a Fortran compiler (e.g. gfotran, ifort).
- Default - gfortran
CC - The command for a C compiler (e.g. gcc, icc).
- Default - gcc
CXX - The command for a C++ compiler (e.g. g++, icpc).
- Default - g++
MPI_DIR - The path to the MPI directory.
- Default - /opt/mpich/3.2-gnu
HDF5_DIR - The path to the HDF5 directory.
- Default - /opt/hdf5/1.8.16-gnu
PHDF5_DIR - The path to the parallel HDF5 directory.
- Default - /opt/phdf5/1.8.16-gnu
To run the full test suite, the following command can be executed in the tests directory:
python run_tests.py
A subset of build configurations and/or tests can be run. To see how to use the script run:
python run_tests.py --help
As an example, say we want to run all tests with debug flags only on tests that have cone and plot in their name. Also, we would like to run this on 4 processors. We can run:
python run_tests.py -j 4 -C debug -R "cone|plot"
Note that standard regular expression syntax is used for selecting build configurations and tests. To print out a list of build configurations, we can run:
python run_tests.py -p
Adding tests to test suite¶
To add a new test to the test suite, create a sub-directory in the tests directory that conforms to the regular expression test_. To configure a test you need to add the following files to your new test directory, test_name for example:
- OpenMC input XML files
- test_name.py - Python test driver script, please refer to other tests to see how to construct. Any output files that are generated during testing must be removed at the end of this script.
- inputs_true.dat - ASCII file that contains Python API-generated XML files concatenated together. When the test is run, inputs that are generated are compared to this file.
- results_true.dat - ASCII file that contains the expected results from the test. The file results_test.dat is compared to this file during the execution of the python test driver script. When the above files have been created, generate a results_test.dat file and copy it to this name and commit. It should be noted that this file should be generated with basic compiler options during openmc configuration and build (e.g., no MPI/HDF5, no debug/optimization).
In addition to this description, please see the various types of tests that are already included in the test suite to see how to create them. If all is implemented correctly, the new test directory will automatically be added to the CTest framework.
Private Development¶
While the process above depends on the fork of the OpenMC repository being publicly available on GitHub, you may also wish to do development on a private repository for research or commercial purposes. The proper way to do this is to create a complete copy of the OpenMC repository (not a fork from GitHub). The private repository can then either be stored just locally or in conjunction with a private repository on Github (this requires a paid plan). Alternatively, Bitbucket offers private repositories for free. If you want to merge some changes you’ve made in your private repository back to mit-crpg/openmc repository, simply follow the steps above with an extra step of pulling a branch from your private repository into a public fork.
Making User Input Changes¶
Users are encouraged to use OpenMC’s Python API to build XML files that the OpenMC solver then reads during the initialization phase. Thus, to modify, add, or remove user input options, changes must be made both within the Python API and the Fortran source that reads XML files produced by the Python API. The following steps should be followed to make changes to user input:
Determine the Python class you need to change. For example, if you are adding a new setting, you probably want to change the
openmc.Settings
class. If you are adding a new surface type, you would need to create a subclass ofopenmc.Surface
.To add a new option, the class will need a property attribute. For example, if you wanted to add a “fast_mode” setting, you would need two methods that look like:
@property def fast_mode(self): ... @fast_mode.setter def fast_mode(self, fast_mode): ...
Make sure that when an instance of the class is exported to XML (usually through a
export_to_xml()
orto_xml_element()
method), a new element is written to the appropriate file. OpenMC uses thexml.etree.ElementTree
API, so refer to the documentation of that module for guidance on creating elements/attributes.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”.
Now that you’re done with the Python side, you need to make modifications to the Fortran codebase. Make appropriate changes in the input_xml module to read your new user input. You should use procedures and types defined by the xml_interface module.
If you’ve made changes in the geometry or materials, make sure they are written out to the statepoint or summary files and that the
openmc.StatePoint
andopenmc.Summary
classes read them in.Finally, a set of RELAX NG schemas exists that enables validation of input files. You should modify the RELAX NG schema for the file you changed. The easiest way to do this is to change the compact syntax file (e.g.
src/relaxng/geometry.rnc
) and then convert it to regular XML syntax using trang:trang geometry.rnc geometry.rng
For most user input additions and changes, it is simple enough to follow a “monkey see, monkey do” approach. When in doubt, contact your nearest OpenMC developer or send a message to the developers mailing list.
Building Sphinx Documentation¶
In order to build the documentation in the docs
directory, you will need to
have the Sphinx third-party Python package. The easiest way to install Sphinx
is via pip:
sudo pip install sphinx
Additionally, you will also need a Sphinx extension for numbering figures. The Numfig package can be installed directly with pip:
sudo pip install sphinx-numfig
Building Documentation as a Webpage¶
To build the documentation as a webpage (what appears at
http://mit-crpg.github.io/openmc), simply go to the docs
directory and run:
make html
Building Documentation as a PDF¶
To build PDF documentation, you will need to have a LaTeX distribution installed on your computer as well as Inkscape, which is used to convert .svg files to .pdf files. Inkscape can be installed in a Debian-derivative with:
sudo apt-get install inkscape
One the pre-requisites are installed, simply go to the docs
directory and
run:
make latexpdf
Python API¶
OpenMC includes a rich Python API that enables programmatic pre- and post-processing. The easiest way to begin using the API is to take a look at the Example Notebooks. This assumes that you are already familiar with Python and common third-party packages such as NumPy. If you have never used Python before, the prospect of learning a new code and a programming language might sound daunting. However, you should keep in mind that there are many substantial benefits to using the Python API, including:
- The ability to define dimensions using variables.
- Availability of standard-library modules for working with files.
- An entire ecosystem of third-party packages for scientific computing.
- Ability to create materials based on natural elements or uranium enrichment
- Automated multi-group cross section generation (
openmc.mgxs
) - Convenience functions (e.g., a function returning a hexagonal region)
- Ability to plot individual universes as geometry is being created
- A \(k_\text{eff}\) search function (
openmc.search_for_keff()
) - Random sphere packing for generating TRISO particle locations
(
openmc.model.pack_trisos()
) - A fully-featured nuclear data interface (
openmc.data
)
For those new to Python, there are many good tutorials available online. We recommend going through the modules from Codecademy and/or the Scipy lectures.
The full API documentation serves to provide more information on a given module or class.
Tip
Users are strongly encouraged to use the Python API to generate input files and analyze results.
Modules¶
openmc
– Basic Functionality¶
Handling nuclear data¶
openmc.XSdata |
A multi-group cross section data set providing all the multi-group data necessary for a multi-group OpenMC calculation. |
openmc.MGXSLibrary |
Multi-Group Cross Sections file used for an OpenMC simulation. |
Simulation Settings¶
openmc.Source |
Distribution of phase space coordinates for source sites. |
openmc.VolumeCalculation |
Stochastic volume calculation specifications and results. |
openmc.Settings |
Settings used for an OpenMC simulation. |
Material Specification¶
openmc.Nuclide |
A nuclide that can be used in a material. |
openmc.Element |
A natural element that auto-expands to add the isotopes of an element to a material in their natural abundance. |
openmc.Macroscopic |
A Macroscopic object that can be used in a material. |
openmc.Material |
A material composed of a collection of nuclides/elements. |
openmc.Materials |
Collection of Materials used for an OpenMC simulation. |
Cross sections for nuclides, elements, and materials can be plotted using the following function:
openmc.plot_xs |
Creates a figure of continuous-energy cross sections for this item. |
Building geometry¶
openmc.Plane |
An arbitrary plane of the form \(Ax + By + Cz = D\). |
openmc.XPlane |
A plane perpendicular to the x axis of the form \(x - x_0 = 0\) |
openmc.YPlane |
A plane perpendicular to the y axis of the form \(y - y_0 = 0\) |
openmc.ZPlane |
A plane perpendicular to the z axis of the form \(z - z_0 = 0\) |
openmc.XCylinder |
An infinite cylinder whose length is parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = R^2\). |
openmc.YCylinder |
An infinite cylinder whose length is parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = R^2\). |
openmc.ZCylinder |
An infinite cylinder whose length is parallel to the z-axis of the form \((x - x_0)^2 + (y - y_0)^2 = R^2\). |
openmc.Sphere |
A sphere of the form \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = R^2\). |
openmc.Cone |
A conical surface parallel to the x-, y-, or z-axis. |
openmc.XCone |
A cone parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = R^2 (x - x_0)^2\). |
openmc.YCone |
A cone parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = R^2 (y - y_0)^2\). |
openmc.ZCone |
A cone parallel to the x-axis of the form \((x - x_0)^2 + (y - y_0)^2 = R^2 (z - z_0)^2\). |
openmc.Quadric |
A surface of the form \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\). |
openmc.Halfspace |
A positive or negative half-space region. |
openmc.Intersection |
Intersection of two or more regions. |
openmc.Union |
Union of two or more regions. |
openmc.Complement |
Complement of a region. |
openmc.Cell |
A region of space defined as the intersection of half-space created by quadric surfaces. |
openmc.Universe |
A collection of cells that can be repeated. |
openmc.RectLattice |
A lattice consisting of rectangular prisms. |
openmc.HexLattice |
A lattice consisting of hexagonal prisms. |
openmc.Geometry |
Geometry representing a collection of surfaces, cells, and universes. |
Many of the above classes are derived from several abstract classes:
openmc.Surface |
An implicit surface with an associated boundary condition. |
openmc.Region |
Region of space that can be assigned to a cell. |
openmc.Lattice |
A repeating structure wherein each element is a universe. |
Two helper function are also available to create rectangular and hexagonal prisms defined by the intersection of four and six surface half-spaces, respectively.
openmc.get_hexagonal_prism |
Create a hexagon region from six surface planes. |
openmc.get_rectangular_prism |
Get an infinite rectangular prism from four planar surfaces. |
Constructing Tallies¶
openmc.Filter |
Tally modifier that describes phase-space and other characteristics. |
openmc.UniverseFilter |
Bins tally event locations based on the Universe they occured in. |
openmc.MaterialFilter |
Bins tally event locations based on the Material they occured in. |
openmc.CellFilter |
Bins tally event locations based on the Cell they occured in. |
openmc.CellbornFilter |
Bins tally events based on which Cell the neutron was born in. |
openmc.SurfaceFilter |
Bins particle currents on Mesh surfaces. |
openmc.MeshFilter |
Bins tally event locations onto a regular, rectangular mesh. |
openmc.EnergyFilter |
Bins tally events based on incident particle energy. |
openmc.EnergyoutFilter |
Bins tally events based on outgoing particle energy. |
openmc.MuFilter |
Bins tally events based on particle scattering angle. |
openmc.PolarFilter |
Bins tally events based on the incident particle’s direction. |
openmc.AzimuthalFilter |
Bins tally events based on the incident particle’s direction. |
openmc.DistribcellFilter |
Bins tally event locations on instances of repeated cells. |
openmc.DelayedGroupFilter |
Bins fission events based on the produced neutron precursor groups. |
openmc.EnergyFunctionFilter |
Multiplies tally scores by an arbitrary function of incident energy. |
openmc.Mesh |
A structured Cartesian mesh in one, two, or three dimensions |
openmc.Trigger |
A criterion for when to finish a simulation based on tally uncertainties. |
openmc.TallyDerivative |
A material perturbation derivative to apply to a tally. |
openmc.Tally |
A tally defined by a set of scores that are accumulated for a list of nuclides given a set of filters. |
openmc.Tallies |
Collection of Tallies used for an OpenMC simulation. |
Coarse Mesh Finite Difference Acceleration¶
openmc.CMFDMesh |
A structured Cartesian mesh used for Coarse Mesh Finite Difference (CMFD) acceleration. |
openmc.CMFD |
Parameters that control the use of coarse-mesh finite difference acceleration in OpenMC. |
Geometry Plotting¶
openmc.Plot |
Definition of a finite region of space to be plotted. |
openmc.Plots |
Collection of Plots used for an OpenMC simulation. |
Running OpenMC¶
openmc.run |
Run an OpenMC simulation. |
openmc.calculate_volumes |
Run stochastic volume calculations in OpenMC. |
openmc.plot_geometry |
Run OpenMC in plotting mode |
openmc.plot_inline |
Display plots inline in a Jupyter notebook. |
openmc.search_for_keff |
Function to perform a keff search by modifying a model parametrized by a single independent variable. |
Post-processing¶
openmc.Particle |
Information used to restart a specific particle that caused a simulation to fail. |
openmc.StatePoint |
State information on a simulation at a certain point in time (at the end of a given batch). |
openmc.Summary |
Summary of geometry, materials, and tallies used in a simulation. |
Various classes may be created when performing tally slicing and/or arithmetic:
openmc.arithmetic.CrossScore |
A special-purpose tally score used to encapsulate all combinations of two tally’s scores as an outer product for tally arithmetic. |
openmc.arithmetic.CrossNuclide |
A special-purpose nuclide used to encapsulate all combinations of two tally’s nuclides as an outer product for tally arithmetic. |
openmc.arithmetic.CrossFilter |
A special-purpose filter used to encapsulate all combinations of two tally’s filter bins as an outer product for tally arithmetic. |
openmc.arithmetic.AggregateScore |
A special-purpose tally score used to encapsulate an aggregate of a subset or all of tally’s scores for tally aggregation. |
openmc.arithmetic.AggregateNuclide |
A special-purpose tally nuclide used to encapsulate an aggregate of a subset or all of tally’s nuclides for tally aggregation. |
openmc.arithmetic.AggregateFilter |
A special-purpose tally filter used to encapsulate an aggregate of a subset or all of a tally filter’s bins for tally aggregation. |
openmc.stats
– Statistics¶
Univariate Probability Distributions¶
openmc.stats.Univariate |
Probability distribution of a single random variable. |
openmc.stats.Discrete |
Distribution characterized by a probability mass function. |
openmc.stats.Uniform |
Distribution with constant probability over a finite interval [a,b] |
openmc.stats.Maxwell |
Maxwellian distribution in energy. |
openmc.stats.Watt |
Watt fission energy spectrum. |
openmc.stats.Tabular |
Piecewise continuous probability distribution. |
openmc.stats.Legendre |
Probability density given by a Legendre polynomial expansion \(\sum\limits_{\ell=0}^N \frac{2\ell + 1}{2} a_\ell P_\ell(\mu)\). |
openmc.stats.Mixture |
Probability distribution characterized by a mixture of random variables. |
Angular Distributions¶
openmc.stats.UnitSphere |
Distribution of points on the unit sphere. |
openmc.stats.PolarAzimuthal |
Angular distribution represented by polar and azimuthal angles |
openmc.stats.Isotropic |
Isotropic angular distribution. |
openmc.stats.Monodirectional |
Monodirectional angular distribution. |
Spatial Distributions¶
openmc.stats.Spatial |
Distribution of locations in three-dimensional Euclidean space. |
openmc.stats.CartesianIndependent |
Spatial distribution with independent x, y, and z distributions. |
openmc.stats.Box |
Uniform distribution of coordinates in a rectangular cuboid. |
openmc.stats.Point |
Delta function in three dimensions. |
openmc.mgxs
– Multi-Group Cross Section Generation¶
Energy Groups¶
openmc.mgxs.EnergyGroups |
An energy groups structure used for multi-group cross-sections. |
Multi-group Cross Sections¶
openmc.mgxs.MGXS |
An abstract multi-group cross section for some energy group structure within some spatial domain. |
openmc.mgxs.AbsorptionXS |
An absorption multi-group cross section. |
openmc.mgxs.CaptureXS |
A capture multi-group cross section. |
openmc.mgxs.Chi |
The fission spectrum. |
openmc.mgxs.FissionXS |
A fission multi-group cross section. |
openmc.mgxs.InverseVelocity |
An inverse velocity multi-group cross section. |
openmc.mgxs.KappaFissionXS |
A recoverable fission energy production rate multi-group cross section. |
openmc.mgxs.MultiplicityMatrixXS |
The scattering multiplicity matrix. |
openmc.mgxs.NuFissionMatrixXS |
A fission production matrix multi-group cross section. |
openmc.mgxs.ScatterXS |
A scattering multi-group cross section. |
openmc.mgxs.ScatterMatrixXS |
A scattering matrix multi-group cross section with the cosine of the change-in-angle represented as one or more Legendre moments or a histogram. |
openmc.mgxs.ScatterProbabilityMatrix |
The group-to-group scattering probability matrix. |
openmc.mgxs.TotalXS |
A total multi-group cross section. |
openmc.mgxs.TransportXS |
A transport-corrected total multi-group cross section. |
Multi-delayed-group Cross Sections¶
openmc.mgxs.MDGXS |
An abstract multi-delayed-group cross section for some energy and delayed group structures within some spatial domain. |
openmc.mgxs.ChiDelayed |
The delayed fission spectrum. |
openmc.mgxs.DelayedNuFissionXS |
A fission delayed neutron production multi-group cross section. |
openmc.mgxs.DelayedNuFissionMatrixXS |
A fission delayed neutron production matrix multi-group cross section. |
openmc.mgxs.Beta |
The delayed neutron fraction. |
openmc.mgxs.DecayRate |
The decay rate for delayed neutron precursors. |
Multi-group Cross Section Libraries¶
openmc.mgxs.Library |
A multi-energy-group and multi-delayed-group cross section library for some energy group structure. |
openmc.model
– Model Building¶
TRISO Fuel Modeling¶
Classes¶
openmc.model.TRISO |
Tristructural-isotopic (TRISO) micro fuel particle |
Functions¶
openmc.model.create_triso_lattice |
Create a lattice containing TRISO particles for optimized tracking. |
openmc.model.pack_trisos |
Generate a random, non-overlapping configuration of TRISO particles within a container. |
Model Container¶
Classes¶
openmc.model.Model |
Model container. |
openmc.data
– Nuclear Data Interface¶
Core Classes¶
openmc.data.IncidentNeutron |
Continuous-energy neutron interaction data. |
openmc.data.Reaction |
A nuclear reaction |
openmc.data.Product |
Secondary particle emitted in a nuclear reaction |
openmc.data.Tabulated1D |
A one-dimensional tabulated function. |
openmc.data.FissionEnergyRelease |
Energy relased by fission reactions. |
openmc.data.ThermalScattering |
A ThermalScattering object contains thermal scattering data as represented by an S(alpha, beta) table. |
openmc.data.CoherentElastic |
Coherent elastic scattering data from a crystalline material |
openmc.data.FissionEnergyRelease |
Energy relased by fission reactions. |
openmc.data.DataLibrary |
Collection of cross section data libraries. |
openmc.data.Decay |
Radioactive decay data. |
openmc.data.FissionProductYields |
Independent and cumulative fission product yields. |
openmc.data.WindowedMultipole |
Resonant cross sections represented in the windowed multipole format. |
Core Functions¶
openmc.data.atomic_mass |
Return atomic mass of isotope in atomic mass units. |
openmc.data.linearize |
Return a tabulated representation of a function of one variable. |
openmc.data.thin |
Check for (x,y) points that can be removed. |
openmc.data.write_compact_458_library |
Read ENDF files, strip the MF=1 MT=458 data and write to small HDF5. |
Angle-Energy Distributions¶
openmc.data.AngleEnergy |
Distribution in angle and energy of a secondary particle. |
openmc.data.KalbachMann |
Kalbach-Mann distribution |
openmc.data.CorrelatedAngleEnergy |
Correlated angle-energy distribution |
openmc.data.UncorrelatedAngleEnergy |
Uncorrelated angle-energy distribution |
openmc.data.NBodyPhaseSpace |
N-body phase space distribution |
openmc.data.LaboratoryAngleEnergy |
Laboratory angle-energy distribution |
openmc.data.AngleDistribution |
Angle distribution as a function of incoming energy |
openmc.data.EnergyDistribution |
Abstract superclass for all energy distributions. |
openmc.data.ArbitraryTabulated |
Arbitrary tabulated function given in ENDF MF=5, LF=1 represented as |
openmc.data.GeneralEvaporation |
General evaporation spectrum given in ENDF MF=5, LF=5 represented as |
openmc.data.MaxwellEnergy |
Simple Maxwellian fission spectrum represented as |
openmc.data.Evaporation |
Evaporation spectrum represented as |
openmc.data.WattEnergy |
Energy-dependent Watt spectrum represented as |
openmc.data.MadlandNix |
Energy-dependent fission neutron spectrum (Madland and Nix) given in |
openmc.data.DiscretePhoton |
Discrete photon energy distribution |
openmc.data.LevelInelastic |
Level inelastic scattering |
openmc.data.ContinuousTabular |
Continuous tabular distribution |
Resonance Data¶
openmc.data.Resonances |
Resolved and unresolved resonance data |
openmc.data.ResonanceRange |
Resolved resonance range |
openmc.data.SingleLevelBreitWigner |
Single-level Breit-Wigner resolved resonance formalism data. |
openmc.data.MultiLevelBreitWigner |
Multi-level Breit-Wigner resolved resonance formalism data. |
openmc.data.ReichMoore |
Reich-Moore resolved resonance formalism data. |
openmc.data.RMatrixLimited |
R-matrix limited resolved resonance formalism data. |
openmc.data.ParticlePair |
|
openmc.data.SpinGroup |
Resonance spin group |
openmc.data.Unresolved |
Unresolved resonance parameters as identified by LRU=2 in MF=2. |
ACE Format¶
Classes¶
openmc.data.ace.Library |
A Library objects represents an ACE-formatted file which may contain multiple tables with data. |
openmc.data.ace.Table |
ACE cross section table |
Functions¶
openmc.data.ace.ascii_to_binary |
Convert an ACE file in ASCII format (type 1) to binary format (type 2). |
ENDF Format¶
Classes¶
openmc.data.endf.Evaluation |
ENDF material evaluation with multiple files/sections |
Functions¶
openmc.data.endf.float_endf |
Convert string of floating point number in ENDF to float. |
openmc.data.endf.get_cont_record |
Return data from a CONT record in an ENDF-6 file. |
openmc.data.endf.get_evaluations |
Return a list of all evaluations within an ENDF file. |
openmc.data.endf.get_head_record |
Return data from a HEAD record in an ENDF-6 file. |
openmc.data.endf.get_tab1_record |
Return data from a TAB1 record in an ENDF-6 file. |
openmc.data.endf.get_tab2_record |
|
openmc.data.endf.get_text_record |
Return data from a TEXT record in an ENDF-6 file. |
NJOY Interface¶
openmc.data.njoy.run |
Run NJOY with given commands |
openmc.data.njoy.make_pendf |
Generate ACE file from an ENDF file |
openmc.data.njoy.make_ace |
Generate incident neutron ACE file from an ENDF file |
openmc.data.njoy.make_ace_thermal |
Generate thermal scattering ACE file from ENDF files |
openmc.examples
– Example Models¶
Simple Models¶
openmc.examples.slab_mg |
Create a one-group, 1D slab model. |
Reactor Models¶
openmc.examples.pwr_pin_cell |
Create a PWR pin-cell model. |
openmc.examples.pwr_assembly |
Create a PWR assembly model. |
openmc.examples.pwr_core |
Create a PWR full-core model. |
openmc.openmoc_compatible
– OpenMOC Compatibility¶
Core Classes¶
openmc.openmoc_compatible.get_openmoc_material |
Return an OpenMOC material corresponding to an OpenMC material. |
openmc.openmoc_compatible.get_openmc_material |
Return an OpenMC material corresponding to an OpenMOC material. |
openmc.openmoc_compatible.get_openmoc_surface |
Return an OpenMOC surface corresponding to an OpenMC surface. |
openmc.openmoc_compatible.get_openmc_surface |
Return an OpenMC surface corresponding to an OpenMOC surface. |
openmc.openmoc_compatible.get_openmoc_cell |
Return an OpenMOC cell corresponding to an OpenMC cell. |
openmc.openmoc_compatible.get_openmc_cell |
Return an OpenMC cell corresponding to an OpenMOC cell. |
openmc.openmoc_compatible.get_openmoc_universe |
Return an OpenMOC universe corresponding to an OpenMC universe. |
openmc.openmoc_compatible.get_openmc_universe |
Return an OpenMC universe corresponding to an OpenMOC universe. |
openmc.openmoc_compatible.get_openmoc_lattice |
Return an OpenMOC lattice corresponding to an OpenMOC lattice. |
openmc.openmoc_compatible.get_openmc_lattice |
Return an OpenMC lattice corresponding to an OpenMOC lattice. |
openmc.openmoc_compatible.get_openmoc_geometry |
Return an OpenMC geometry corresponding to an OpenMOC geometry. |
openmc.openmoc_compatible.get_openmc_geometry |
Return an OpenMC geometry corresponding to an OpenMOC geometry. |
File Format Specifications¶
Input Files¶
Geometry Specification – geometry.xml¶
<surface>
Element¶
Each <surface>
element can have the following attributes or sub-elements:
id: A unique integer that can be used to identify the surface.
Default: None
name: An optional string name to identify the surface in summary output files. This string is limited to 52 characters for formatting purposes.
Default: “”
type: The type of the surfaces. This can be “x-plane”, “y-plane”, “z-plane”, “plane”, “x-cylinder”, “y-cylinder”, “z-cylinder”, “sphere”, “x-cone”, “y-cone”, “z-cone”, or “quadric”.
Default: None
coeffs: The corresponding coefficients for the given type of surface. See below for a list a what coefficients to specify for a given surface
Default: None
boundary: The boundary condition for the surface. This can be “transmission”, “vacuum”, “reflective”, or “periodic”. Periodic boundary conditions can only be applied to x-, y-, and z-planes. Only axis-aligned periodicity is supported, i.e., x-planes can only be paired with x-planes. Specify which planes are periodic and the code will automatically identify which planes are paired together.
Default: “transmission”
periodic_surface_id: If a periodic boundary condition is applied, this attribute identifies the
id
of the corresponding periodic sufrace.
The following quadratic surfaces can be modeled:
x-plane: A plane perpendicular to the x axis, i.e. a surface of the form \(x - x_0 = 0\). The coefficients specified are “\(x_0\)”. y-plane: A plane perpendicular to the y axis, i.e. a surface of the form \(y - y_0 = 0\). The coefficients specified are “\(y_0\)”. z-plane: A plane perpendicular to the z axis, i.e. a surface of the form \(z - z_0 = 0\). The coefficients specified are “\(z_0\)”. plane: An arbitrary plane of the form \(Ax + By + Cz = D\). The coefficients specified are “\(A \: B \: C \: D\)”. x-cylinder: An infinite cylinder whose length is parallel to the x-axis. This is a quadratic surface of the form \((y - y_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(y_0 \: z_0 \: R\)”. y-cylinder: An infinite cylinder whose length is parallel to the y-axis. This is a quadratic surface of the form \((x - x_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(x_0 \: z_0 \: R\)”. z-cylinder: An infinite cylinder whose length is parallel to the z-axis. This is a quadratic surface of the form \((x - x_0)^2 + (y - y_0)^2 = R^2\). The coefficients specified are “\(x_0 \: y_0 \: R\)”. sphere: A sphere of the form \((x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = R^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R\)”. x-cone: A cone parallel to the x-axis of the form \((y - y_0)^2 + (z - z_0)^2 = R^2 (x - x_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”. y-cone: A cone parallel to the y-axis of the form \((x - x_0)^2 + (z - z_0)^2 = R^2 (y - y_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”. z-cone: A cone parallel to the x-axis of the form \((x - x_0)^2 + (y - y_0)^2 = R^2 (z - z_0)^2\). The coefficients specified are “\(x_0 \: y_0 \: z_0 \: R^2\)”. quadric: A general quadric surface of the form \(Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0\) The coefficients specified are “\(A \: B \: C \: D \: E \: F \: G \: H \: J \: K\)”.
<cell>
Element¶
Each <cell>
element can have the following attributes or sub-elements:
id: A unique integer that can be used to identify the cell.
Default: None
name: An optional string name to identify the cell in summary output files. This string is limmited to 52 characters for formatting purposes.
Default: “”
universe: The
id
of the universe that this cell is contained in.Default: 0
fill: The
id
of the universe that fills this cell.Note
If a fill is specified, no material should be given.
Default: None
material: The
id
of the material that this cell contains. If the cell should contain no material, this can also be set to “void”. A list of materials can be specified for the “distributed material” feature. This will give each unique instance of the cell its own material.Note
If a material is specified, no fill should be given.
Default: None
region: A Boolean expression of half-spaces that defines the spatial region which the cell occupies. Each half-space is identified by the unique ID of the surface prefixed by - or + to indicate that it is the negative or positive half-space, respectively. The + sign for a positive half-space can be omitted. Valid Boolean operators are parentheses, union |, complement ~, and intersection. Intersection is implicit and indicated by the presence of whitespace. The order of operator precedence is parentheses, complement, intersection, and then union.
As an example, the following code gives a cell that is the union of the negative half-space of surface 3 and the complement of the intersection of the positive half-space of surface 5 and the negative half-space of surface 2:
<cell id="1" material="1" region="-3 | ~(5 -2)" />Note
The
region
attribute/element can be omitted to make a cell fill its entire universe.Default: A region filling all space.
temperature: The temperature of the cell in Kelvin. If windowed-multipole data is avalable, this temperature will be used to Doppler broaden some cross sections in the resolved resonance region. A list of temperatures can be specified for the “distributed temperature” feature. This will give each unique instance of the cell its own temperature.
Default: If a material default temperature is supplied, it is used. In the absence of a material default temperature, the global default temperature is used.
rotation: If the cell is filled with a universe, this element specifies the angles in degrees about the x, y, and z axes that the filled universe should be rotated. Should be given as three real numbers. For example, if you wanted to rotate the filled universe by 90 degrees about the z-axis, the cell element would look something like:
<cell fill="..." rotation="0 0 90" />The rotation applied is an intrinsic rotation whose Tait-Bryan angles are given as those specified about the x, y, and z axes respectively. That is to say, if the angles are \((\phi, \theta, \psi)\), then the rotation matrix applied is \(R_z(\psi) R_y(\theta) R_x(\phi)\) or
\[\begin{split}\left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\theta \sin\psi + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi + \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi \cos\theta \end{array} \right ]\end{split}\]Default: None
translation: If the cell is filled with a universe, this element specifies a vector that is used to translate (shift) the universe. Should be given as three real numbers.
Note
Any translation operation is applied after a rotation, if also specified.
Default: None
<lattice>
Element¶
The <lattice>
can be used to represent repeating structures (e.g. fuel pins
in an assembly) or other geometry which fits onto a rectilinear grid. Each cell
within the lattice is filled with a specified universe. A <lattice>
accepts
the following attributes or sub-elements:
id: A unique integer that can be used to identify the lattice.
name: An optional string name to identify the lattice in summary output files. This string is limited to 52 characters for formatting purposes.
Default: “”
dimension: Two or three integers representing the number of lattice cells in the x- and y- (and z-) directions, respectively.
Default: None
lower_left: The coordinates of the lower-left corner of the lattice. If the lattice is two-dimensional, only the x- and y-coordinates are specified.
Default: None
pitch: If the lattice is 3D, then three real numbers that express the distance between the centers of lattice cells in the x-, y-, and z- directions. If the lattice is 2D, then omit the third value.
Default: None
outer: The unique integer identifier of a universe that will be used to fill all space outside of the lattice. The universe will be tiled repeatedly as if it were placed in a lattice of infinite size. This element is optional.
Default: An error will be raised if a particle leaves a lattice with no outer universe.
universes: A list of the universe numbers that fill each cell of the lattice.
Default: None
Here is an example of a properly defined 2d rectangular lattice:
<lattice id="10" dimension="3 3" outer="1">
<lower_left> -1.5 -1.5 </lower_left>
<pitch> 1.0 1.0 </pitch>
<universes>
2 2 2
2 1 2
2 2 2
</universes>
</lattice>
<hex_lattice>
Element¶
The <hex_lattice>
can be used to represent repeating structures (e.g. fuel
pins in an assembly) or other geometry which naturally fits onto a hexagonal
grid or hexagonal prism grid. Each cell within the lattice is filled with a
specified universe. This lattice uses the “flat-topped hexagon” scheme where two
of the six edges are perpendicular to the y-axis. A <hex_lattice>
accepts
the following attributes or sub-elements:
id: A unique integer that can be used to identify the lattice.
name: An optional string name to identify the hex_lattice in summary output files. This string is limited to 52 characters for formatting purposes.
Default: “”
n_rings: An integer representing the number of radial ring positions in the xy-plane. Note that this number includes the degenerate center ring which only has one element.
Default: None
n_axial: An integer representing the number of positions along the z-axis. This element is optional.
Default: None
center: The coordinates of the center of the lattice. If the lattice does not have axial sections then only the x- and y-coordinates are specified.
Default: None
pitch: If the lattice is 3D, then two real numbers that express the distance between the centers of lattice cells in the xy-plane and along the z-axis, respectively. If the lattice is 2D, then omit the second value.
Default: None
outer: The unique integer identifier of a universe that will be used to fill all space outside of the lattice. The universe will be tiled repeatedly as if it were placed in a lattice of infinite size. This element is optional.
Default: An error will be raised if a particle leaves a lattice with no outer universe.
universes: A list of the universe numbers that fill each cell of the lattice.
Default: None
Here is an example of a properly defined 2d hexagonal lattice:
<hex_lattice id="10" n_rings="3" outer="1">
<center> 0.0 0.0 </center>
<pitch> 1.0 </pitch>
<universes>
202
202 202
202 202 202
202 202
202 101 202
202 202
202 202 202
202 202
202
</universes>
</hex_lattice>
Materials Specification – materials.xml¶
<cross_sections>
Element¶
The <cross_sections>
element has no attributes and simply indicates the path
to an XML cross section listing file (usually named cross_sections.xml). If this
element is absent from the settings.xml file, the
OPENMC_CROSS_SECTIONS
environment variable will be used to find the
path to the XML cross section listing when in continuous-energy mode, and the
OPENMC_MG_CROSS_SECTIONS
environment variable will be used in
multi-group mode.
<multipole_library>
Element¶
The <multipole_library>
element indicates the directory containing a
windowed multipole library. If a windowed multipole library is available,
OpenMC can use it for on-the-fly Doppler-broadening of resolved resonance range
cross sections. If this element is absent from the settings.xml file, the
OPENMC_MULTIPOLE_LIBRARY
environment variable will be used.
Note
The <temperature_multipole> element must also be set to “true” for windowed multipole functionality.
<material>
Element¶
Each material
element can have the following attributes or sub-elements:
id: A unique integer that can be used to identify the material.
name: An optional string name to identify the material in summary output files. This string is limited to 52 characters for formatting purposes.
Default: “”
temperature: An element with no attributes which is used to set the default temperature of the material in Kelvin.
Default: If a material default temperature is not given and a cell temperature is not specified, the global default temperature is used.
density: An element with attributes/sub-elements called
value
andunits
. Thevalue
attribute is the numeric value of the density while theunits
can be “g/cm3”, “kg/m3”, “atom/b-cm”, “atom/cm3”, or “sum”. The “sum” unit indicates that values appearing inao
orwo
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 amacroscopic
quantity to indicate that the density is already included in the library and thus not needed here. However, if a value is provided for thevalue
, 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 anuclide
,element
, orsab
quantity.nuclide: An element with attributes/sub-elements called
name
, andao
orwo
. Thename
attribute is the name of the cross-section for a desired nuclide. Finally, theao
andwo
attributes specify the atom or weight percent of that nuclide within the material, respectively. One example would be as follows:<nuclide name="H1" ao="2.0" /> <nuclide name="O16" ao="1.0" />Note
If one nuclide is specified in atom percent, all others must also be given in atom percent. The same applies for weight percentages.
An optional attribute/sub-element for each nuclide is
scattering
. This attribute may be set to “data” to use the scattering laws specified by the cross section library (default). Alternatively, when set to “iso-in-lab”, the scattering laws are used to sample the outgoing energy but an isotropic-in-lab distribution is used to sample the outgoing angle at each scattering interaction. Thescattering
attribute may be most useful when using OpenMC to compute multi-group cross-sections for deterministic transport codes and to quantify the effects of anisotropic scattering.Default: None
Note
The
scattering
attribute/sub-element is not used in the multi-group <energy_mode> Element.sab: Associates an S(a,b) table with the material. This element has one attribute/sub-element called
name
. Thename
attribute is the name of the S(a,b) table that should be associated with the material.Default: None
Note
This element is not used in the multi-group <energy_mode> Element.
macroscopic: The
macroscopic
element is similar to thenuclide
element, but, recognizes that some multi-group libraries may be providing material specific macroscopic cross sections instead of always providing nuclide specific data like in the continuous-energy case. To that end, the macroscopic element has one attribute/sub-element calledname
. Thename
attribute is the name of the cross-section for a desired nuclide. One example would be as follows:<macroscopic name="UO2" />Note
This element is only used in the multi-group <energy_mode> Element.
Default: None
Settings Specification – settings.xml¶
All simulation parameters and miscellaneous options are specified in the settings.xml file.
<batches>
Element¶
The <batches>
element indicates the total number of batches to execute,
where each batch corresponds to a tally realization. In a fixed source
calculation, each batch consists of a number of source particles. In an
eigenvalue calculation, each batch consists of one or many fission source
iterations (generations), where each generation itself consists of a number of
source neutrons.
Default: None
<confidence_intervals>
Element¶
The <confidence_intervals>
element has no attributes and has an accepted
value of “true” or “false”. If set to “true”, uncertainties on tally results
will be reported as the half-width of the 95% two-sided confidence interval. If
set to “false”, uncertainties on tally results will be reported as the sample
standard deviation.
Default: false
<cutoff>
Element¶
The <cutoff>
element indicates two kinds of cutoffs. The first is the weight
cutoff used below which particles undergo Russian roulette. Surviving particles
are assigned a user-determined weight. Note that weight cutoffs and Russian
rouletting are not turned on by default. The second is the energy cutoff which
is used to kill particles under certain energy. The energy cutoff should not be
used unless you know particles under the energy are of no importance to results
you care. This element has the following attributes/sub-elements:
weight: The weight below which particles undergo Russian roulette.
Default: 0.25
weight_avg: The weight that is assigned to particles that are not killed after Russian roulette.
Default: 1.0
energy: The energy under which particles will be killed.
Default: 0.0
<energy_grid>
Element¶
The <energy_grid>
element determines the treatment of the energy grid during
a simulation. The valid options are “nuclide”, “logarithm”, and
“material-union”. Setting this element to “nuclide” will cause OpenMC to use a
nuclide’s energy grid when determining what points to interpolate between for
determining cross sections (i.e. non-unionized energy grid). Setting this
element to “logarithm” causes OpenMC to use a logarithmic mapping technique
described in LA-UR-14-24530. Setting this element to “material-union” will
cause OpenMC to create energy grids that are unionized material-by-material and
use these grids when determining the energy-cross section pairs to interpolate
cross section values between.
Default: logarithm
Note
This element is not used in the multi-group <energy_mode> Element.
<energy_mode>
Element¶
The <energy_mode>
element tells OpenMC if the run-mode should be
continuous-energy or multi-group. Options for entry are: continuous-energy
or multi-group
.
Default: continuous-energy
<entropy>
Element¶
The <entropy>
element describes a mesh that is used for calculating Shannon
entropy. This mesh should cover all possible fissionable materials in the
problem. It has the following attributes/sub-elements:
dimension: The number of mesh cells in the x, y, and z directions, respectively.
- Default: If this tag is not present, the number of mesh cells is
automatically determined by the code.
lower_left: The Cartesian coordinates of the lower-left corner of the mesh.
Default: None
upper_right: The Cartesian coordinates of the upper-right corner of the mesh.
Default: None
<generations_per_batch>
Element¶
The <generations_per_batch>
element indicates the number of total fission
source iterations per batch for an eigenvalue calculation. This element is
ignored for all run modes other than “eigenvalue”.
Default: 1
<inactive>
Element¶
The <inactive>
element indicates the number of inactive batches used in a
k-eigenvalue calculation. In general, the starting fission source iterations in
an eigenvalue calculation can not be used to contribute to tallies since the
fission source distribution and eigenvalue are generally not converged
immediately. This element is ignored for all run modes other than “eigenvalue”.
Default: 0
<keff_trigger>
Element¶
The <keff_trigger>
element (ignored for all run modes other than
“eigenvalue”.) specifies a precision trigger on the combined
\(k_{eff}\). The trigger is a convergence criterion on the uncertainty of
the estimated eigenvalue. It has the following attributes/sub-elements:
type: The type of precision trigger. Accepted options are “variance”, “std_dev”, and “rel_err”.
variance: Variance of the batch mean \(\sigma^2\) std_dev: Standard deviation of the batch mean \(\sigma\) rel_err: Relative error of the batch mean \(\frac{\sigma}{\mu}\) Default: None
threshold: The precision trigger’s convergence criterion for the combined \(k_{eff}\).
Default: None
Note
See section on the <trigger> Element for more information.
<log_grid_bins>
Element¶
The <log_grid_bins>
element indicates the number of bins to use for the
logarithmic-mapped energy grid. Using more bins will result in energy grid
searches over a smaller range at the expense of more memory. The default is
based on the recommended value in LA-UR-14-24530.
Default: 8000
Note
This element is not used in the multi-group <energy_mode> Element.
<max_order>
Element¶
The <max_order>
element allows the user to set a maximum scattering order
to apply to every nuclide/material in the problem. That is, if the data
library has \(P_3\) data available, but <max_order>
was set to 1
,
then, OpenMC will only use up to the \(P_1\) data.
Default: Use the maximum order in the data library
Note
This element is not used in the continuous-energy <energy_mode> Element.
<no_reduce>
Element¶
The <no_reduce>
element has no attributes and has an accepted value of
“true” or “false”. If set to “true”, all user-defined tallies and global tallies
will not be reduced across processors in a parallel calculation. This means that
the accumulate score in one batch on a single processor is considered as an
independent realization for the tally random variable. For a problem with large
tally data, this option can significantly improve the parallel efficiency.
Default: false
<output>
Element¶
The <output>
element determines what output files should be written to disk
during the run. The sub-elements are described below, where “true” will write
out the file and “false” will not.
summary: Writes out an HDF5 summary file describing all of the user input files that were read in.
Default: true
tallies: Write out an ASCII file of tally results.
Default: true
Note
The tally results will always be written to a binary/HDF5 state point file.
path: Absolute or relative path where all output files should be written to. The specified path must exist or else OpenMC will abort.
Default: Current working directory
<particles>
Element¶
This element indicates the number of neutrons to simulate per fission source iteration when a k-eigenvalue calculation is performed or the number of neutrons per batch for a fixed source simulation.
Default: None
<ptables>
Element¶
The <ptables>
element determines whether probability tables should be used
in the unresolved resonance range if available. This element has no attributes
or sub-elements and can be set to either “false” or “true”.
Default: true
Note
This element is not used in the multi-group <energy_mode> Element.
<resonance_scattering>
Element¶
The resonance_scattering
element indicates to OpenMC that a method be used
to properly account for resonance elastic scattering (typically for nuclides
with Z > 40). This element can contain one or more of the following attributes
or sub-elements:
enable: Indicates whether a resonance elastic scattering method should be turned on. Accepts values of “true” or “false”.
Default: If the
<resonance_scattering>
element is present, “true”.method: Which resonance elastic scattering method is to be applied: “ares” (accelerated resonance elastic scattering), “dbrc” (Doppler broadening rejection correction), or “wcm” (weight correction method). Descriptions of each of these methods are documented here.
Default: “ares”
energy_min: The energy in eV above which the resonance elastic scattering method should be applied.
Default: 0.01 eV
energy_max: The energy in eV below which the resonance elastic scattering method should be applied.
Default: 1000.0 eV
nuclides: A list of nuclides to which the resonance elastic scattering method should be applied.
Default: If
<resonance_scattering>
is present but the<nuclides>
sub-element is not given, the method is applied to all nuclides with 0 K elastic scattering data present.Note
If the
resonance_scattering
element is not given, the free gas, constant cross section scattering model, which has historically been used by Monte Carlo codes to sample target velocities, is used to treat the target motion of all nuclides. Ifresonance_scattering
is present, the constant cross section method is applied belowenergy_min
and the target-at-rest (asymptotic) kernel is used aboveenergy_max
.Note
This element is not used in the multi-group <energy_mode> Element.
<run_cmfd>
Element¶
The <run_cmfd>
element indicates whether or not CMFD acceleration should be
turned on or off. This element has no attributes or sub-elements and can be set
to either “false” or “true”.
Default: false
<run_mode>
Element¶
The <run_mode>
element indicates which run mode should be used when OpenMC
is executed. This element has no attributes or sub-elements and can be set to
“eigenvalue”, “fixed source”, “plot”, “volume”, or “particle restart”.
Default: None
<seed>
Element¶
The seed
element is used to set the seed used for the linear congruential
pseudo-random number generator.
Default: 1
<source>
Element¶
The source
element gives information on an external source distribution to
be used either as the source for a fixed source calculation or the initial
source guess for criticality calculations. Multiple <source>
elements may be
specified to define different source distributions. Each one takes the following
attributes/sub-elements:
strength: The strength of the source. If multiple sources are present, the source strength indicates the relative probability of choosing one source over the other.
Default: 1.0
file: If this attribute is given, it indicates that the source is to be read from a binary source file whose path is given by the value of this element. Note, the number of source sites needs to be the same as the number of particles simulated in a fission source generation.
Default: None
space: An element specifying the spatial distribution of source sites. This element has the following attributes:
type: The type of spatial distribution. Valid options are “box”, “fission”, “point”, and “cartesian”. A “box” spatial distribution has coordinates sampled uniformly in a parallelepiped. A “fission” spatial distribution samples locations from a “box” distribution but only locations in fissionable materials are accepted. A “point” spatial distribution has coordinates specified by a triplet. An “cartesian” spatial distribution specifies independent distributions of x-, y-, and z-coordinates.
Default: None
parameters: For a “box” or “fission” spatial distribution,
parameters
should be given as six real numbers, the first three of which specify the lower-left corner of a parallelepiped and the last three of which specify the upper-right corner. Source sites are sampled uniformly through that parallelepiped.For a “point” spatial distribution,
parameters
should be given as three real numbers which specify the (x,y,z) location of an isotropic point source.For an “cartesian” distribution, no parameters are specified. Instead, the
x
,y
, andz
elements must be specified.Default: None
x: For an “cartesian” distribution, this element specifies the distribution of x-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
y: For an “cartesian” distribution, this element specifies the distribution of y-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
z: For an “cartesian” distribution, this element specifies the distribution of z-coordinates. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
angle: An element specifying the angular distribution of source sites. This element has the following attributes:
type: The type of angular distribution. Valid options are “isotropic”, “monodirectional”, and “mu-phi”. The angle of the particle emitted from a source site is isotropic if the “isotropic” option is given. The angle of the particle emitted from a source site is the direction specified in the
reference_uvw
element/attribute if “monodirectional” option is given. The “mu-phi” option produces directions with the cosine of the polar angle and the azimuthal angle explicitly specified.Default: isotropic
reference_uvw: The direction from which the polar angle is measured. Represented by the x-, y-, and z-components of a unit vector. For a monodirectional distribution, this defines the direction of all sampled particles.
mu: An element specifying the distribution of the cosine of the polar angle. Only relevant when the type is “mu-phi”. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
phi: An element specifying the distribution of the azimuthal angle. Only relevant when the type is “mu-phi”. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
energy: An element specifying the energy distribution of source sites. The necessary sub-elements/attributes are those of a univariate probability distribution (see the description in Univariate Probability Distributions).
Default: Watt spectrum with \(a\) = 0.988 MeV and \(b\) = 2.249 MeV -1
write_initial: An element specifying whether to write out the initial source bank used at the beginning of the first batch. The output file is named “initial_source.h5”
Default: false
Univariate Probability Distributions¶
Various components of a source distribution involve probability distributions of a single random variable, e.g. the distribution of the energy, the distribution of the polar angle, and the distribution of x-coordinates. Each of these components supports the same syntax with an element whose tag signifies the variable and whose sub-elements/attributes are as follows:
type: | The type of the distribution. Valid options are “uniform”, “discrete”, “tabular”, “maxwell”, and “watt”. The “uniform” option produces variates sampled from a uniform distribution over a finite interval. The “discrete” option produces random variates that can assume a finite number of values (i.e., a distribution characterized by a probability mass function). The “tabular” option produces random variates sampled from a tabulated distribution where the density function is either a histogram or linearly-interpolated between tabulated points. The “watt” option produces random variates is sampled from a Watt fission spectrum (only used for energies). The “maxwell” option produce variates sampled from a Maxwell fission spectrum (only used for energies). Default: None |
---|---|
parameters: | For a “uniform” distribution, For a “discrete” or “tabular” distribution, For a “watt” distribution, For a “maxwell” distribution, Note The above format should be used even when using the multi-group <energy_mode> Element. |
interpolation: | For a “tabular” distribution, Default: histogram |
<state_point>
Element¶
The <state_point>
element indicates at what batches a state point file
should be written. A state point file can be used to restart a run or to get
tally results at any batch. The default behavior when using this tag is to
write out the source bank in the state_point file. This behavior can be
customized by using the <source_point>
element. This element has the
following attributes/sub-elements:
batches: A list of integers separated by spaces indicating at what batches a state point file should be written.
Default: Last batch only
<source_point>
Element¶
The <source_point>
element indicates at what batches the source bank
should be written. The source bank can be either written out within a state
point file or separately in a source point file. This element has the following
attributes/sub-elements:
batches: A list of integers separated by spaces indicating at what batches a state point file should be written. It should be noted that if the
separate
attribute is not set to “true”, this list must be a subset of state point batches.Default: Last batch only
separate: If this element is set to “true”, a separate binary source point file will be written. Otherwise, the source sites will be written in the state point directly.
Default: false
write: If this element is set to “false”, source sites are not written to the state point or source point file. This can substantially reduce the size of state points if large numbers of particles per batch are used.
Default: true
overwrite_latest: If this element is set to “true”, a source point file containing the source bank will be written out to a separate file named
source.binary
orsource.h5
depending on if HDF5 is enabled. This file will be overwritten at every single batch so that the latest source bank will be available. It should be noted that a user can set both this element to “true” and specify batches to write a permanent source bank.Default: false
<survival_biasing>
Element¶
The <survival_biasing>
element has no attributes and has an accepted value
of “true” or “false”. If set to “true”, this option will enable the use of
survival biasing, otherwise known as implicit capture or absorption.
Default: false
<tabular_legendre>
Element¶
The optional <tabular_legendre>
element specifies how the multi-group
Legendre scattering kernel is represented if encountered in a multi-group
problem. Specifically, the options are to either convert the Legendre
expansion to a tabular representation or leave it as a set of Legendre
coefficients. Converting to a tabular representation will cost memory but can
allow for a decrease in runtime compared to leaving as a set of Legendre
coefficients. This element has the following attributes/sub-elements:
enable: This attribute/sub-element denotes whether or not the conversion of a Legendre scattering expansion to the tabular format should be performed or not. A value of “true” means the conversion should be performed, “false” means it will not.
Default: true
num_points: If the conversion is to take place the number of tabular points is required. This attribute/sub-element allows the user to set the desired number of points.
Default: 33
Note
This element is only used in the multi-group <energy_mode> Element.
<temperature_default>
Element¶
The <temperature_default>
element specifies a default temperature in Kelvin
that is to be applied to cells in the absence of an explicit cell temperature or
a material default temperature.
Default: 293.6 K
<temperature_method>
Element¶
The <temperature_method>
element has an accepted value of “nearest” or
“interpolation”. A value of “nearest” indicates that for each
cell, the nearest temperature at which cross sections are given is to be
applied, within a given tolerance (see <temperature_tolerance> Element). A value of
“interpolation” indicates that cross sections are to be linear-linear
interpolated between temperatures at which nuclear data are present (see
Temperature Treatment).
Default: “nearest”
<temperature_multipole>
Element¶
The <temperature_multipole>
element toggles the windowed multipole
capability on or off. If this element is set to “True” and the relevant data is
available, OpenMC will use the windowed multipole method to evaluate and Doppler
broaden cross sections in the resolved resonance range. This override other
methods like “nearest” and “interpolation” in the resolved resonance range.
Default: False
<temperature_tolerance>
Element¶
The <temperature_tolerance>
element specifies a tolerance in Kelvin that is
to be applied when the “nearest” temperature method is used. For example, if a
cell temperature is 340 K and the tolerance is 15 K, then the closest
temperature in the range of 325 K to 355 K will be used to evaluate cross
sections.
Default: 10 K
<threads>
Element¶
The <threads>
element indicates the number of OpenMP threads to be used for
a simulation. It has no attributes and accepts a positive integer value.
Default: None (Determined by environment variableOMP_NUM_THREADS
)
<trace>
Element¶
The <trace>
element can be used to print out detailed information about a
single particle during a simulation. This element should be followed by three
integers: the batch number, generation number, and particle number.
Default: None
<track>
Element¶
The <track>
element specifies particles for which OpenMC will output binary
files describing particle position at every step of its transport. This element
should be followed by triplets of integers. Each triplet describes one
particle. The integers in each triplet specify the batch number, generation
number, and particle number, respectively.
Default: None
<trigger>
Element¶
OpenMC includes tally precision triggers which allow the user to define
uncertainty thresholds on \(k_{eff}\) in the <keff_trigger>
subelement
of settings.xml
, and/or tallies in tallies.xml
. When using triggers,
OpenMC will run until it completes as many batches as defined by <batches>
.
At this point, the uncertainties on all tallied values are computed and compared
with their corresponding trigger thresholds. If any triggers have not been met,
OpenMC will continue until either all trigger thresholds have been satisfied or
<max_batches>
has been reached.
The <trigger>
element provides an active “toggle switch” for tally
precision trigger(s), the maximum number of batches and the batch interval. It
has the following attributes/sub-elements:
active: This determines whether or not to use trigger(s). Trigger(s) are used when this tag is set to “true”.
max_batches: This describes the maximum number of batches allowed when using trigger(s).
Note
When max_batches is set, the number of
batches
shown in the<batches>
element represents minimum number of batches to simulate when using the trigger(s).batch_interval: This tag describes the number of batches in between convergence checks. OpenMC will check if the trigger has been reached at each batch defined by
batch_interval
after the minimum number of batches is reached.Note
If this tag is not present, the
batch_interval
is predicted dynamically by OpenMC for each convergence check. The predictive model assumes no correlation between fission sources distributions from batch-to-batch. This assumption is reasonable for fixed source and small criticality calculations, but is very optimistic for highly coupled full-core reactor problems.
<uniform_fs>
Element¶
The <uniform_fs>
element describes a mesh that is used for re-weighting
source sites at every generation based on the uniform fission site methodology
described in Kelly et al., “MC21 Analysis of the Nuclear Energy Agency Monte
Carlo Performance Benchmark Problem,” Proceedings of Physor 2012, Knoxville,
TN (2012). This mesh should cover all possible fissionable materials in the
problem. It has the following attributes/sub-elements:
dimension: The number of mesh cells in the x, y, and z directions, respectively.
Default: None
lower_left: The Cartesian coordinates of the lower-left corner of the mesh.
Default: None
upper_right: The Cartesian coordinates of the upper-right corner of the mesh.
Default: None
<verbosity>
Element¶
The <verbosity>
element tells the code how much information to display to
the standard output. A higher verbosity corresponds to more information being
displayed. The text of this element should be an integer between between 1
and 10. The verbosity levels are defined as follows:
1: don’t display any output 2: only show OpenMC logo 3: all of the above + headers 4: all of the above + results 5: all of the above + file I/O 6: all of the above + timing statistics and initialization messages 7: all of the above + \(k\) by generation 9: all of the above + indicate when each particle starts 10: all of the above + event information Default: 7
<create_fission_neutrons>
Element¶
The <create_fission_neutrons>
element indicates whether fission neutrons
should be created or not. If this element is set to “true”, fission neutrons
will be created; otherwise the fission is treated as capture and no fission
neutron will be created. Note that this option is only applied to fixed source
calculation. For eigenvalue calculation, fission will always be treated as real
fission.
Default: true
<volume_calc>
Element¶
The <volume_calc>
element indicates that a stochastic volume calculation
should be run at the beginning of the simulation. This element has the following
sub-elements/attributes:
cells: The unique IDs of cells for which the volume should be estimated.
Default: None
samples: The number of samples used to estimate volumes.
Default: None
lower_left: The lower-left Cartesian coordinates of a bounding box that is used to sample points within.
Default: None
upper_right: The upper-right Cartesian coordinates of a bounding box that is used to sample points within.
Default: None
Tallies Specification – tallies.xml¶
The tallies.xml file allows the user to tell the code what results he/she is interested in, e.g. the fission rate in a given cell or the current across a given surface. There are two pieces of information that determine what quantities should be scored. First, one needs to specify what region of phase space should count towards the tally and secondly, the actual quantity to be scored also needs to be specified. The first set of parameters we call filters since they effectively serve to filter events, allowing some to score and preventing others from scoring to the tally.
The structure of tallies in OpenMC is flexible in that any combination of filters can be used for a tally. The following types of filter are available: cell, universe, material, surface, birth region, pre-collision energy, post-collision energy, and an arbitrary structured mesh.
The three valid elements in the tallies.xml file are <tally>
, <mesh>
,
and <assume_separate>
.
<tally>
Element¶
The <tally>
element accepts the following sub-elements:
name: An optional string name to identify the tally in summary output files. This string is limited to 52 characters for formatting purposes.
Default: “”
filter: Specify a filter that modifies tally behavior. Most tallies (e.g.
cell
,energy
, andmaterial
) restrict the tally so that only particles within certain regions of phase space contribute to the tally. Others (e.g.delayedgroup
andenergyfunction
) can apply some other function to the scored values. This element and its attributes/sub-elements are described below.Note
You may specify zero, one, or multiple filters to apply to the tally. To specify multiple filters, you must use multiple
<filter>
elements.The
filter
element has the following attributes/sub-elements:
type: The type of the filter. Accepted options are “cell”, “cellborn”, “material”, “universe”, “energy”, “energyout”, “mu”, “polar”, “azimuthal”, “mesh”, “distribcell”, “delayedgroup”, and “energyfunction”. bins: A description of the bins for each type of filter can be found in Filter Types. energy: energyfunction
filters multiply tally scores by an arbitrary function. The function is described by a piecewise linear-linear set of (energy, y) values. This entry specifies the energy values. The function will be evaluated as zero outside of the bounds of this energy grid. (Only used forenergyfunction
filters)y: energyfunction
filters multiply tally scores by an arbitrary function. The function is described by a piecewise linear-linear set of (energy, y) values. This entry specifies the y values. (Only used forenergyfunction
filters)nuclides: If specified, the scores listed will be for particular nuclides, not the summation of reactions from all nuclides. The format for nuclides should be [Atomic symbol]-[Mass number], e.g. “U-235”. The reaction rate for all nuclides can be obtained with “total”. For example, to obtain the reaction rates for U-235, Pu-239, and all nuclides in a material, this element should be:
<nuclides>U-235 Pu-239 total</nuclides>Default: total
estimator: The estimator element is used to force the use of either
analog
,collision
, ortracklength
tally estimation.analog
is generally the least efficient though it can be used with every score type.tracklength
is generally the most efficient, but neithertracklength
norcollision
can be used to score a tally that requires post-collision information. For example, a scattering tally with outgoing energy filters cannot be used withtracklength
orcollision
because the code will not know the outgoing energy distribution.Default:
tracklength
but will revert toanalog
if necessary.scores: A space-separated list of the desired responses to be accumulated. A full list of valid scores can be found in the user’s guide.
trigger: Precision trigger applied to all filter bins and nuclides for this tally. It must specify the trigger’s type, threshold and scores to which it will be applied. It has the following attributes/sub-elements:
type: The type of the trigger. Accepted options are “variance”, “std_dev”, and “rel_err”.
variance: Variance of the batch mean \(\sigma^2\) std_dev: Standard deviation of the batch mean \(\sigma\) rel_err: Relative error of the batch mean \(\frac{\sigma}{\mu}\) Default: None
threshold: The precision trigger’s convergence criterion for tallied values.
Default: None
scores: The score(s) in this tally to which the trigger should be applied.
Note
The
scores
intrigger
must have been defined inscores
intally
. An optional “all” may be used to select all scores in this tally.Default: “all”
derivative: The id of a
derivative
element. This derivative will be applied to all scores in the tally. Differential tallies are currently only implemented for collision and analog estimators.Default: None
Filter Types¶
For each filter type, the following table describes what the bins
attribute
should be set to:
cell: | A list of unique IDs for cells in which the tally should be accumulated. |
---|---|
cellborn: | This filter allows the tally to be scored to only when particles were originally born in a specified cell. A list of cell IDs should be given. |
material: | A list of unique IDs for matreials in which the tally should be accumulated. |
universe: | A list of unique IDs for universes in which the tally should be accumulated. |
energy: | In continuous-energy mode, this filter should be provided as a monotonically increasing list of bounding pre-collision energies for a number of groups. For example, if this filter is specified as <filter type="energy" bins="0.0 1.0e6 20.0e6" />
then two energy bins will be created, one with energies between 0 and 1 MeV and the other with energies between 1 and 20 MeV. In multi-group mode the bins provided must match group edges defined in the multi-group library. |
energyout: | In continuous-energy mode, this filter should be provided as a monotonically increasing list of bounding post-collision energies for a number of groups. For example, if this filter is specified as <filter type="energyout" bins="0.0 1.0e6 20.0e6" />
then two post-collision energy bins will be created, one with energies between 0 and 1 MeV and the other with energies between 1 and 20 MeV. In multi-group mode the bins provided must match group edges defined in the multi-group library. |
mu: | A monotonically increasing list of bounding post-collision cosines of the change in a particle’s angle (i.e., \(\mu = \hat{\Omega} \cdot \hat{\Omega}'\)), which represents a portion of the possible values of \([-1,1]\). For example, spanning all of \([-1,1]\) with five equi-width bins can be specified as: <filter type="mu" bins="-1.0 -0.6 -0.2 0.2 0.6 1.0" />
Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([-1,1]\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([-1,1]\) can be instead written as: <filter type="mu" bins="5" />
|
polar: | A monotonically increasing list of bounding particle polar angles which represents a portion of the possible values of \([0,\pi]\). For example, spanning all of \([0,\pi]\) with five equi-width bins can be specified as: <filter type="polar" bins="0.0 0.6283 1.2566 1.8850 2.5132 3.1416"/>
Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([0,\pi]\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([0,\pi]\) can be instead written as: <filter type="polar" bins="5" />
|
azimuthal: | A monotonically increasing list of bounding particle azimuthal angles which represents a portion of the possible values of \([-\pi,\pi)\). For example, spanning all of \([-\pi,\pi)\) with two equi-width bins can be specified as: <filter type="azimuthal" bins="0.0 3.1416 6.2832" />
Alternatively, if only one value is provided as a bin, OpenMC will interpret this to mean the complete range of \([-\pi,\pi)\) should be automatically subdivided in to the provided value for the bin. That is, the above example of five equi-width bins spanning \([-\pi,\pi)\) can be instead written as: <filter type="azimuthal" bins="2" />
|
mesh: | The unique ID of a structured mesh to be tallied over. |
distribcell: | The single cell which should be tallied uniquely for all instances. Note The distribcell filter will take a single cell ID and will tally each unique occurrence of that cell separately. This filter will not accept more than one cell ID. It is not recommended to combine this filter with a cell or mesh filter. |
delayedgroup: | A list of delayed neutron precursor groups for which the tally should be accumulated. For instance, to tally to all 6 delayed groups in the ENDF/B-VII.1 library the filter is specified as: <filter type="delayedgroup" bins="1 2 3 4 5 6" />
|
energyfunction: |
|
<mesh>
Element¶
If a structured mesh is desired as a filter for a tally, it must be specified in
a separate element with the tag name <mesh>
. This element has the following
attributes/sub-elements:
type: The type of structured mesh. The only valid option is “regular”. dimension: The number of mesh cells in each direction. lower_left: The lower-left corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh. upper_right: The upper-right corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh. width: The width of mesh cells in each direction. Note
One of
<upper_right>
or<width>
must be specified, but not both (even if they are consistent with one another).
<derivative>
Element¶
OpenMC can take the first-order derivative of many tallies with respect to material perturbations. It works by propagating a derivative through the transport equation. Essentially, OpenMC keeps track of how each particle’s weight would change as materials are perturbed, and then accounts for that weight change in the tallies. Note that this assumes material perturbations are small enough not to change the distribution of fission sites. This element has the following attributes/sub-elements:
id: A unique integer that can be used to identify the derivative. variable: The independent variable of the derivative. Accepted options are “density”, “nuclide_density”, and “temperature”. A “density” derivative will give the derivative with respect to the density of the material in [g / cm^3]. A “nuclide_density” derivative will give the derivative with respect to the density of a particular nuclide in units of [atom / b / cm]. A “temperature” derivative is with respect to a material temperature in units of [K]. The temperature derivative requires windowed multipole to be turned on. Note also that the temperature derivative only accounts for resolved resonance Doppler broadening. It does not account for thermal expansion, S(a, b) scattering, resonance scattering, or unresolved Doppler broadening. material: The perturbed material. (Necessary for all derivative types) nuclide: The perturbed nuclide. (Necessary only for “nuclide_density”)
<assume_separate>
Element¶
In cases where the user needs to specify many different tallies each of which are spatially separate, this tag can be used to cut down on some of the tally overhead. The effect of assuming all tallies are spatially separate is that once one tally is scored to, the same event is assumed not to score to any other tallies. This element should be followed by “true” or “false”.
Warning
If used incorrectly, the assumption that all tallies are spatially separate can lead to incorrect results.
Default: false
Geometry Plotting Specification – plots.xml¶
Basic plotting capabilities are available in OpenMC by creating a plots.xml file
and subsequently running with the --plot
command-line flag. The root element
of the plots.xml is simply <plots>
and any number output plots can be
defined with <plot>
sub-elements. Two plot types are currently implemented
in openMC:
slice
2D pixel plot along one of the major axes. Produces a PPM image file.voxel
3D voxel data dump. Produces a binary file containing voxel xyz position and cell or material id.
<plot>
Element¶
Each plot is specified by a combination of the following attributes or sub-elements:
id: The unique
id
of the plot.Default: None - Required entry
filename: Filename for the output plot file.
Default: “plot”
color_by: Keyword for plot coloring. This can be either “cell” or “material”, which colors regions by cells and materials, respectively. For voxel plots, this determines which id (cell or material) is associated with each position.
Default: “cell”
level: Universe depth to plot at (optional). This parameter controls how many universe levels deep to pull cell and material ids from when setting plot colors. If a given location does not have as many levels as specified, colors will be taken from the lowest level at that location. For example, if
level
is set to zero colors will be taken from top-level (universe zero) cells only. However, iflevel
is set to 1 colors will be taken from cells in universes that fill top-level fill-cells, and from top-level cells that contain materials.Default: Whatever the deepest universe is in the model
origin: Specifies the (x,y,z) coordinate of the center of the plot. Should be three floats separated by spaces.
Default: None - Required entry
width: Specifies the width of the plot along each of the basis directions. Should be two or three floats separated by spaces for 2D plots and 3D plots, respectively.
Default: None - Required entry
type: Keyword for type of plot to be produced. Currently only “slice” and “voxel” plots are implemented. The “slice” plot type creates 2D pixel maps saved in the PPM file format. PPM files can be displayed in most viewers (e.g. the default Gnome viewer, IrfanView, etc.). The “voxel” plot type produces a binary datafile containing voxel grid positioning and the cell or material (specified by the
color
tag) at the center of each voxel. These datafiles can be processed into 3D SILO files using the openmc-voxel-to-silovtk script provided with OpenMC, and subsequently viewed with a 3D viewer such as VISIT or Paraview. See the Voxel Plot File Format for information about the datafile structure.Note
Since the PPM format is saved without any kind of compression, the resulting file sizes can be quite large. Saving the image in the PNG format can often times reduce the file size by orders of magnitude without any loss of image quality. Likewise, high-resolution voxel files produced by OpenMC can be quite large, but the equivalent SILO files will be significantly smaller.
Default: “slice”
<plot>
elements of type
“slice” and “voxel” must contain the pixels
attribute or sub-element:
pixels: Specifies the number of pixels or voxels to be used along each of the basis directions for “slice” and “voxel” plots, respectively. Should be two or three integers separated by spaces.
Warning
The
pixels
input determines the output file size. For the PPM format, 10 million pixels will result in a file just under 30 MB in size. A 10 million voxel binary file will be around 40 MB.Warning
If the aspect ratio defined in
pixels
does not match the aspect ratio defined inwidth
the plot may appear stretched or squeezed.Warning
Geometry features along a basis direction smaller than
width
/pixels
along that basis direction may not appear in the plot.Default: None - Required entry for “slice” and “voxel” plots
<plot>
elements of type
“slice” can also contain the following
attributes or sub-elements. These are not used in “voxel” plots:
basis: Keyword specifying the plane of the plot for “slice” type plots. Can be one of: “xy”, “xz”, “yz”.
Default: “xy”
background: Specifies the RGB color of the regions where no OpenMC cell can be found. Should be three integers separated by spaces.
Default: 0 0 0 (black)
color: Any number of this optional tag may be included in each
<plot>
element, which can override the default random colors for cells or materials. Eachcolor
element must containid
andrgb
sub-elements.
id: Specifies the cell or material unique id for the color specification. rgb: Specifies the custom color for the cell or material. Should be 3 integers separated by spaces. As an example, if your plot is colored by material and you want material 23 to be blue, the corresponding
color
element would look like:<color id="23" rgb="0 0 255" />Default: None
mask: The special
mask
sub-element allows for the selective plotting of only user-specified cells or materials. Only onemask
element is allowed perplot
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 anycolor
color specifications.Default: 255 255 255 (white)
meshlines: The
meshlines
sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only onemeshlines
element is allowed perplot
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 formeshtype="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”.
<dhat_reset>
Element¶
The <dhat_reset>
element controls whether \(\widehat{D}\) nonlinear
CMFD parameters should be reset to zero before solving CMFD eigenproblem.
It can be turned on with “true” and off with “false”.
Default: false
<display>
Element¶
The <display>
element sets one additional CMFD output column. Options are:
“balance” - prints the RMS [%] of the resdiual from the neutron balance equation on CMFD tallies.
“dominance” - prints the estimated dominance ratio from the CMFD iterations. This will only work for power iteration eigensolver.
“entropy” - prints the entropy of the CMFD predicted fission source. Can only be used if OpenMC entropy is active as well.
“source” - prints the RMS [%] between the OpenMC fission source and CMFD fission source.
Default: balance
<downscatter>
Element¶
The <downscatter>
element controls whether an effective downscatter cross
section should be used when using 2-group CMFD. It can be turned on with “true”
and off with “false”.
Default: false
<feedback>
Element¶
The <feedback>
element controls whether or not the CMFD diffusion result is
used to adjust the weight of fission source neutrons on the next OpenMC batch.
It can be turned on with “true” and off with “false”.
Default: false
<gauss_seidel_tolerance>
Element¶
The <gauss_seidel_tolerance>
element specifies two parameters. The first is
the absolute inner tolerance for Gauss-Seidel iterations when performing CMFD
and the second is the relative inner tolerance for Gauss-Seidel iterations
for CMFD calculations.
Default: 1.e-10 1.e-5
<ktol>
Element¶
The <ktol>
element specifies the tolerance on the eigenvalue when performing
CMFD power iteration.
Default: 1.e-8
<mesh>
Element¶
The CMFD mesh is a structured Cartesian mesh. This element has the following attributes/sub-elements:
lower_left: The lower-left corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh.
upper_right: The upper-right corner of the structrued mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh.
dimension: The number of mesh cells in each direction.
width: The width of mesh cells in each direction.
energy: Energy bins [in eV], listed in ascending order (e.g. 0.0 0.625 20.0e6) for CMFD tallies and acceleration. If no energy bins are listed, OpenMC automatically assumes a one energy group calculation over the entire energy range.
albedo: Surface ratio of incoming to outgoing partial currents on global boundary conditions. They are listed in the following order: -x +x -y +y -z +z.
Default: 1.0 1.0 1.0 1.0 1.0 1.0
map: An optional acceleration map can be specified to overlay on the coarse mesh spatial grid. If this option is used, a
1
is used for a non-accelerated region and a2
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
andwidth
. Any combination of two of these will yield the third.
<norm>
Element¶
The <norm>
element is used to normalize the CMFD fission source distribution
to a particular value. For example, if a fission source is calculated for a
17 x 17 lattice of pins, the fission source may be normalized to the number of
fission source regions, in this case 289. This is useful when visualizing this
distribution as the average peaking factor will be unity. This parameter will
not impact the calculation.
Default: 1.0
<power_monitor>
Element¶
The <power_monitor>
element is used to view the convergence of power
iteration. This option can be turned on with “true” and turned off with “false”.
Default: false
<run_adjoint>
Element¶
The <run_adjoint>
element can be turned on with “true” to have an adjoint
calculation be performed on the last batch when CMFD is active.
Default: false
<shift>
Element¶
The <shift>
element specifies an optional Wielandt shift parameter for
accelerating power iterations. It is by default very large so the impact of the
shift is effectively zero.
Default: 1e6
<spectral>
Element¶
The <spectral>
element specifies an optional spectral radius that can be set to
accelerate the convergence of Gauss-Seidel iterations during CMFD power iteration
solve.
Default: 0.0
<stol>
Element¶
The <stol>
element specifies the tolerance on the fission source when performing
CMFD power iteration.
Default: 1.e-8
<tally_reset>
Element¶
The <tally_reset>
element contains a list of batch numbers in which CMFD tallies
should be reset.
Default: None
<write_matrices>
Element¶
The <write_matrices>
element is used to write the sparse matrices created
when solving CMFD equations. This option can be turned on with “true” and off
with “false”.
Default: false
Data Files¶
Cross Sections Listing – cross_sections.xml¶
<directory>
Element¶
The <directory>
element specifies a root directory to which the path for all
files listed in a <library> Element are given relative to. This element has
no attributes or sub-elements; the directory should be given within the text
node. For example,
<directory>/opt/data/cross_sections/</directory>
<library>
Element¶
The <library>
element indicates where an HDF5 cross section file is located,
whether it contains incident neutron or thermal scattering data, and what
materials are listed within. It has the following attributes:
materials: A space-separated list of nuclides or thermal scattering tables. For example,
<library materials="U234 U235 U238" /> <library materials="c_H_in_H2O c_D_in_G2O" />Often, just a single nuclide or thermal scattering table is contained in a given file.
path: Path to the HDF5 file. If the <directory> Element is specified, the path is relative to the directory given. Otherwise, it is relative to the directory containing the
cross_sections.xml
file.type: The type of data contained in the file, either ‘neutron’ or ‘thermal’.
Nuclear Data File Format¶
Incident Neutron Data¶
/
Attributes: |
|
---|
/<nuclide name>/
Attributes: |
|
---|---|
Datasets: |
|
/<nuclide name>/kTs/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Datasets: |
|
---|
/<nuclide name>/reactions/reaction_<mt>/
Attributes: |
|
---|
/<nuclide name>/reactions/reaction_<mt>/<TTT>K/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Datasets: |
|
---|
/<nuclide name>/reactions/reaction_<mt>/product_<j>/
Reaction product data is described in Reaction Products.
/<nuclide name>/urr/<TTT>K/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Attributes: |
|
---|---|
Datasets: |
|
/<nuclide name>/total_nu/
This special product is used to define the total number of neutrons produced from fission. It is formatted as a reaction product, described in Reaction Products.
/<nuclide name>/fission_energy_release/
Datasets: |
|
---|
Thermal Neutron Scattering Data¶
/
Attributes: |
|
---|
/<thermal name>/
Attributes: |
|
---|
/<thermal name>/kTs/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Datasets: |
|
---|
/<thermal name>/elastic/<TTT>K/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Datasets: |
|
---|
/<thermal name>/inelastic/<TTT>K/
<TTT>K is the temperature in Kelvin, rounded to the nearest integer, of the temperature-dependent data set. For example, the data set corresponding to 300 Kelvin would be located at 300K.
Datasets: |
|
---|
If the secondary mode is continuous, the outgoing energy-angle distribution is given as a correlated angle-energy distribution.
Reaction Products¶
Object type: | Group |
||
---|---|---|---|
Attributes: |
|
||
Datasets: |
|
||
Groups: |
|
One-dimensional Functions¶
Scalar¶
Object type: | Dataset |
---|---|
Datatype: | double |
Attributes: |
|
Tabulated¶
Object type: | Dataset |
---|---|
Datatype: | double[2][] |
Description: | x-values are listed first followed by corresponding y-values |
Attributes: |
|
Polynomial¶
Object type: | Dataset |
---|---|
Datatype: | double[] |
Description: | Polynomial coefficients listed in order of increasing power |
Attributes: |
|
Coherent elastic scattering¶
Object type: | Dataset |
---|---|
Datatype: | double[2][] |
Description: | The first row lists Bragg edges and the second row lists structure factor cumulative sums. |
Attributes: |
|
Angle-Energy Distributions¶
Kalbach-Mann¶
Object type: | Group |
||||
---|---|---|---|---|---|
Attributes: |
|
||||
Datasets: |
|
N-Body Phase Space¶
Object type: | Group |
---|---|
Attributes: |
|
Energy Distributions¶
Maxwell¶
Object type: | Group |
---|---|
Attributes: |
|
Datasets: |
|
Evaporation¶
Object type: | Group |
---|---|
Attributes: |
|
Datasets: |
|
Watt Fission Spectrum¶
Object type: | Group |
---|---|
Attributes: |
|
Datasets: |
Madland-Nix¶
Object type: | Group |
---|---|
Attributes: |
|
Discrete Photon¶
Object type: | Group |
---|---|
Attributes: |
|
Level Inelastic¶
Object type: | Group |
---|---|
Attributes: |
|
Continuous Tabular¶
Object type: | Group |
||||
---|---|---|---|---|---|
Attributes: |
|
||||
Datasets: |
|
Multi-Group Cross Section Library Format¶
OpenMC can be run in continuous-energy mode or multi-group mode, provided the
nuclear data is available. In continuous-energy mode, the
cross_sections.xml
file contains necessary meta-data for each dataset,
including the name and a file system location where the complete library
can be found. In multi-group mode, the multi-group meta-data and the
nuclear data itself is contained within an mgxs.h5
file. This portion of
the manual describes the format of the multi-group data library required
to be used in the mgxs.h5
file.
The multi-group library is provided in the HDF5 format. This library must provide some meta-data about the library itself (such as the number of energy groups, delayed groups, and the energy group structure, etc.) as well as the actual cross section data itself for each of the necessary nuclides or materials.
The current version of the multi-group library file format is 1.0.
MGXS Library Specification¶
/
Attributes: |
|
---|
/<library name>/
The data within <library name> contains the temperature-dependent multi-group data for the nuclide or material that it represents.
Attributes: |
|
---|
/<library name>/kTs/
Datasets: |
|
---|
/<library name>/<TTT>K/
Temperature-dependent data, provided for temperature <TTT>K.
Datasets: |
|
---|
/<library name>/<TTT>K/scatter_data/
Data specific to neutron scattering for the temperature <TTT>K
Datasets: |
|
---|
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.
- 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 bywindows
.
- sqrtAWR (double)
Square root of the atomic weight ratio.
- start_E (double)
Lowest energy the windowed multipole part of the library is valid for.
- w_start (int[])
The pole to start from for each window.
- w_end (int[])
The pole to end at for each window.
- windows (int)
Number of windows.
- /nuclide/reactions/MT<i>
- MT_sigma (double[]) – Cross section value for this reaction.
- Q_value (double) – Energy released in this reaction, in eV.
- threshold (int) – The first non-zero entry in
MT_sigma
.
Fission Energy Release File Format¶
This file is a compact HDF5 representation of the ENDF MT=1, MF=458 data (see
ENDF-102 for details). It gives the information needed to compute the energy
carried away from fission reactions by each reaction product (e.g. fragment
nuclei, neutrons) which depends on the incident neutron energy. OpenMC is
distributed with one of these files under
data/fission_Q_data_endfb71.h5. More files of this format can be created from
ENDF files with the
openmc.data.write_compact_458_library
function. They can be read with the
openmc.data.FissionEnergyRelease.from_compact_hdf5
class method.
Attributes: |
|
---|
- /<nuclide name>/
- Nuclides are named by concatenating their atomic symbol and mass number. For example, ‘U235’ or ‘Pu239’. Metastable nuclides are appended with an ‘_m’ and their metastable number. For example, ‘Am242_m1’
Datasets: |
|
---|
Output Files¶
State Point File Format¶
The current version of the statepoint file format is 16.0.
/
Attributes: |
|
---|---|
Datasets: |
|
/cmfd/
Datasets: |
|
---|
/tallies/
Attributes: |
|
---|
/tallies/meshes/
Attributes: |
|
---|
/tallies/meshes/mesh <uid>/
Datasets: |
|
---|
/tallies/derivatives/derivative <id>/
Datasets: |
|
---|
/tallies/tally <uid>/
Datasets: |
|
---|
/tallies/tally <uid>/filter <j>/
Datasets: |
|
---|
/runtime/
All values are given in seconds and are measured on the master process.
Datasets: |
|
---|
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 fieldswgt
,xyz
,uvw
,E
, anddelayed_group
, which represent the weight, position, direction, energy, energy group, and delayed_group of the source particle, respectively.
Summary File Format¶
The current version of the summary file format is 5.0.
/
Attributes: |
|
---|
/geometry/
Attributes: |
|
---|
/geometry/cells/cell <uid>/
Datasets: |
|
---|
/geometry/surfaces/surface <uid>/
Datasets: |
|
---|
/geometry/universes/universe <uid>/
Datasets: |
|
---|
/geometry/lattices/lattice <uid>/
Datasets: |
|
---|
/materials/
Attributes: |
|
---|
/materials/material <uid>/
Datasets: |
|
---|
/nuclides/
Attributes: |
|
---|---|
Datasets: |
|
/tallies/tally <uid>/
Datasets: |
|
---|
Particle Restart File Format¶
The current version of the particle restart file format is 2.0.
/
Attributes: |
|
---|---|
Datasets: |
|
Track File Format¶
The current revision of the particle track file format is 2.0.
/
Attributes: |
|
---|---|
Datasets: |
|
Voxel Plot File Format¶
The current version of the voxel file format is 1.0.
/
Attributes: |
|
---|---|
Datasets: |
|
Volume File Format¶
The current version of the volume file format is 1.0.
/
Attributes: |
|
---|
/domain_<id>/
Datasets: |
|
---|
Publications¶
Overviews¶
- Paul K. Romano, Nicholas E. Horelik, Bryan R. Herman, Adam G. Nelson, Benoit Forget, and Kord Smith, “OpenMC: A State-of-the-Art Monte Carlo Code for Research and Development,” Ann. Nucl. Energy, 82, 90–97 (2015).
- Paul K. Romano, Bryan R. Herman, Nicholas E. Horelik, Benoit Forget, Kord Smith, and Andrew R. Siegel, “Progress and Status of the OpenMC Monte Carlo Code,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).
- Paul K. Romano and Benoit Forget, “The OpenMC Monte Carlo Particle Transport Code,” Ann. Nucl. Energy, 51, 274–281 (2013).
Benchmarking¶
- Khurrum S. Chaudri and Sikander M. Mirza, “Burnup dependent Monte Carlo neutron physics calculations of IAEA MTR benchmark,” Prog. Nucl. Energy, 81, 43-52 (2015).
- Daniel J. Kelly, Brian N. Aviles, Paul K. Romano, Bryan R. Herman, Nicholas E. Horelik, and Benoit Forget, “Analysis of select BEAVRS PWR benchmark cycle 1 results using MC21 and OpenMC,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
- Bryan R. Herman, Benoit Forget, Kord Smith, Paul K. Romano, Thomas M. Sutton, Daniel J. Kelly, III, and Brian N. Aviles, “Analysis of tally correlations in large light water reactors,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
- Nicholas Horelik, Bryan Herman, Benoit Forget, and Kord Smith, “Benchmark for Evaluation and Validation of Reactor Simulations,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).
- Jonathan A. Walsh, Benoit Forget, and Kord S. Smith, “Validation of OpenMC Reactor Physics Simulations with the B&W 1810 Series Benchmarks,” Trans. Am. Nucl. Soc., 109, 1301–1304 (2013).
Coupling and Multi-physics¶
- Matthew Ellis, Derek Gaston, Benoit Forget, and Kord Smith, “Preliminary Coupling of the Monte Carlo Code OpenMC and the Multiphysics Object-Oriented Simulation Environment for Analyzing Doppler Feedback in Monte Carlo Simulations,” Nucl. Sci. Eng., 185, 184-193 (2017).
- Matthew Ellis, Benoit Forget, Kord Smith, and Derek Gaston, “Continuous Temperature Representation in Coupled OpenMC/MOOSE Simulations,” Proc. PHYSOR 2016, Sun Valley, Idaho, May 1-5, 2016.
- Antonios G. Mylonakis, Melpomeni Varvayanni, and Nicolas Catsaros, “Investigating a Matrix-free, Newton-based, Neutron-Monte Carlo/Thermal-Hydraulic Coupling Scheme”, Proc. Int. Conf. Nuclear Energy for New Europe, Portoroz, Slovenia, Sep .14-17 (2015).
- 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).
- 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 and Visualization¶
- Logan Abel, William Boyd, Benoit Forget, and Kord Smith, “Interactive Visualization of Multi-Group Cross Sections on High-Fidelity Spatial Meshes,” Trans. Am. Nucl. Soc., 114, 391-394 (2016).
- Derek M. Lax, “Memory efficient indexing algorithm for physical properties in OpenMC,” S. M. Thesis, Massachusetts Institute of Technology (2015).
- Derek Lax, William Boyd, Nicholas Horelik, Benoit Forget, and Kord Smith, “A memory efficient algorithm for classifying unique regions in constructive solid geometries,” Proc. PHYSOR, Kyoto, Japan, Sep. 28–Oct. 3 (2014).
Miscellaneous¶
- Amanda L. Lund, Paul K. Romano, and Andrew R. Siegel, “Accelerating Source Convergence in Monte Carlo Criticality Calculations Using a Particle Ramp-Up Technique,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- Antonios G. Mylonakis, M. Varvayanni, D.G.E. Grigoriadis, and N. Catsaros, “Developing and investigating a pure Monte-Carlo module for transient neutron transport analysis,” Ann. Nucl. Energy, 104, 103-112 (2017).
- Timothy P. Burke, Brian C. Kiedrowski, William R. Martin, and Forrest B. Brown, “GPU Acceleration of Kernel Density Estimators in Monte Carlo Neutron Transport Simulations,” Trans. Am. Nucl. Soc., 115, 531-534 (2016).
- Timothy P. Burke, Brian C. Kiedrowski, and William R. Martin, “Cylindrical Kernel Density Estimators for Monte Carlo Neutron Transport Reactor Physics Problems,” Trans. Am. Nucl. Soc., 115, 563-566 (2016).
- Yunzhao Li, Qingming He, Liangzhi Cao, Hongchun Wu, and Tiejun Zu, “Resonance Elastic Scattering and Interference Effects Treatments in Subgroup Method,” Nucl. Eng. Tech., 48, 339-350 (2016).
- William Boyd, Sterling Harper, and Paul K. Romano, “Equipping OpenMC for the big data era,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
- Michal Kostal, Vojtech Rypar, Jan Milcak, Vlastimil Juricek, Evzen Losa, Benoit Forget, and Sterling Harper, “Study of graphite reactivity worth on well-defined cores assembled on LR-0 reactor,” Ann. Nucl. Energy, 87, 601-611 (2016).
- Qicang Shen, William Boyd, Benoit Forget, and Kord Smith, “Tally precision triggers for the OpenMC Monte Carlo code,” Trans. Am. Nucl. Soc., 112, 637-640 (2015).
- Kyungkwan Noh and Deokjung Lee, “Whole Core Analysis using OpenMC Monte Carlo Code,” Trans. Kor. Nucl. Soc. Autumn Meeting, Gyeongju, Korea, Oct. 24-25, 2013.
- Timothy P. Burke, Brian C. Kiedrowski, and William R. Martin, “Flux and Reaction Rate Kernel Density Estimators in OpenMC,” Trans. Am. Nucl. Soc., 109, 683-686 (2013).
Multi-group Cross Section Generation¶
- Hong Shuang, Yang Yongwei, Zhang Lu, and Gao Yucui, “Fabrication and validation of multigroup cross section library based on the OpenMC code,” Nucl. Techniques 40 (4), 040504 (2017). (in Mandarin)
- Nicholas E. Stauff, Changho Lee, Paul K. Romano, and Taek K. Kim, “Verification of Mixed Stochastic/Deterministic Approach for Fast and Thermal Reactor Analysis,” Proc. ICAPP, Fukui and Kyoto, Japan, Apr. 24-28, 2017.
- Zhauyuan Liu, Kord Smith, and Benoit Forget, “Progress of Cumulative Migration Method for Computing Diffusion Coefficients with OpenMC,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- Geoffrey Gunow, Samuel Shaner, William Boyd, Benoit Forget, and Kord Smith, “Accuracy and Performance of 3D MOC for Full-Core PWR Problems,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- Tianliang Hu, Liangzhi Cao, Hongchun Wu, and Kun Zhuang, “A coupled neutronics and thermal-hydraulic modeling approach to the steady-state and dynamic behavior of MSRs,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- William R. D. Boyd, “Reactor Agnostic Multi-Group Cross Section Generation for Fine-Mesh Deterministic Neutron Transport Simulations,” Ph.D. Thesis, Massachusetts Institute of Technology (2017).
- Zhaoyuan Liu, Kord Smith, and Benoit Forget, “A Cumulative Migration Method for Computing Rigorous Transport Cross Sections and Diffusion Coefficients for LWR Lattices with Monte Carlo,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
- Adam G. Nelson and William R. Martin, “Improved Monte Carlo tallying of multi-group scattering moments using the NDPP code,” Trans. Am. Nucl. Soc., 113, 645-648 (2015)
- Adam G. Nelson and William R. Martin, “Improved Monte Carlo tallying of multi-group scattering moment matrices,” Trans. Am. Nucl. Soc., 110, 217-220 (2014).
- Adam G. Nelson and William R. Martin, “Improved Convergence of Monte Carlo Generated Multi-Group Scattering Moments,” Proc. Int. Conf. Mathematics and Computational Methods Applied to Nuclear Science and Engineering, Sun Valley, Idaho, May 5–9 (2013).
Doppler Broadening¶
- Colin Josey, Pablo Ducru, Benoit Forget, and Kord Smith, “Windowed multipole for cross section Doppler broadening,” J. Comput. Phys., 307, 715-727 (2016).
- Jonathan A. Walsh, Benoit Forget, Kord S. Smith, and Forrest B. Brown, “On-the-fly Doppler Broadening of Unresolved Resonance Region Cross Sections via Probability Band Interpolation,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
- 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).
- Paul K. Romano and Timothy H. Trumbull, “Comparison of algorithms for Doppler broadening pointwise tabulated cross sections,” Ann. Nucl. Energy, 75, 358–364 (2015).
- 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).
- 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).
Nuclear Data¶
- Jonathan A. Walsh, Benoit Forget, Kord S. Smith, and Forrest B. Brown, “Uncertainty in Fast Reactor-Relevant Critical Benchmark Simulations Due to Unresolved Resonance Structure,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- Vivian Y. Tran, Jonathan A. Walsh, and Benoit Forget, “Treatments for Neutron Resonance Elastic Scattering Using the Multipole Formalism in Monte Carlo Codes,” Trans. Am. Nucl. Soc., 115, 1133-1137 (2016).
- Paul K. Romano and Sterling M. Harper, “Nuclear data processing capabilities in OpenMC”, Proc. Nuclear Data, Sep. 11-16, 2016.
- Jonathan A. Walsh, Benoit Froget, Kord S. Smith, and Forrest B. Brown, “Neutron Cross Section Processing Methods for Improved Integral Benchmarking of Unresolved Resonance Region Evaluations,” Eur. Phys. J. Web Conf. 111, 06001 (2016).
- Jonathan A. Walsh, Paul K. Romano, Benoit Forget, and Kord S. Smith, “Optimizations of the energy grid search algorithm in continuous-energy Monte Carlo particle transport codes”, Comput. Phys. Commun., 196, 134-142 (2015).
- Jonathan A. Walsh, Benoit Forget, Kord S. Smith, Brian C. Kiedrowski, and Forrest B. Brown, “Direct, on-the-fly calculation of unresolved resonance region cross sections in Monte Carlo simulations,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
- Amanda L. Lund, Andrew R. Siegel, Benoit Forget, Colin Josey, and Paul K. Romano, “Using fractional cascading to accelerate cross section lookups in Monte Carlo particle transport calculations,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
- Ronald O. Rahaman, Andrew R. Siegel, and Paul K. Romano, “Monte Carlo performance analysis for varying cross section parameter regimes,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
- Jonathan A. Walsh, Benoit Forget, and Kord S. Smith, “Accelerated sampling of the free gas resonance elastic scattering kernel,” Ann. Nucl. Energy, 69, 116–124 (2014).
Parallelism¶
- Paul K. Romano and Andrew R. Siegel, “Limits on the efficiency of event-based algorithms for Monte Carlo neutron transport,” Proc. Int. Conf. Mathematics & Computational Methods Applied to Nuclear Science and Engineering, Jeju, Korea, Apr. 16-20, 2017.
- Paul K. Romano, John R. Tramm, and Andrew R. Siegel, “Efficacy of hardware threading for Monte Carlo particle transport calculations on multi- and many-core systems,” PHYSOR 2016, Sun Valley, Idaho, May 1-5, 2016.
- David Ozog, Allen D. Malony, and Andrew R. Siegel, “A performance analysis of SIMD algorithms for Monte Carlo simulations of nuclear reactor cores,” Proc. IEEE Int. Parallel and Distributed Processing Symposium, Hyderabad, India, May 25–29 (2015).
- David Ozog, Allen D. Malony, and Andrew Siegel, “Full-core PWR transport simulations on Xeon Phi clusters,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
- Paul K. Romano, Andrew R. Siegel, and Ronald O. Rahaman, “Influence of the memory subsystem on Monte Carlo code performance,” Proc. Joint Int. Conf. M&C+SNA+MC, Nashville, Tennessee, Apr. 19–23 (2015).
- Hajime Fujita, Nan Dun, Aiman Fang, Zachary A. Rubinstein, Ziming Zheng, Kamil Iskra, Jeff Hammonds, Anshu Dubey, Pavan Balaji, and Andrew A. Chien, “Using Global View Resilience (GVR) to add Resilience to Exascale Applications,” Proc. Supercomputing, New Orleans, Louisiana, Nov. 16–21, 2014.
- Nicholas Horelik, Benoit Forget, Kord Smith, and Andrew Siegel, “Domain decomposition and terabyte tallies with the OpenMC Monte Carlo neutron transport code,” Proc. PHYSOR, Kyoto Japan, Sep. 28–Oct. 3 (2014).
- John R. Tramm, Andrew R. Siegel, Tanzima Islam, and Martin Schulz, “XSBench – the development and verification of a performance abstraction for Monte Carlo reactor analysis,” Proc. PHYSOR, Kyoto, Japan, Sep 28–Oct. 3, 2014.
- Nicholas Horelik, Andrew Siegel, Benoit Forget, and Kord Smith, “Monte Carlo domain decomposition for robust nuclear reactor analysis,” Parallel Comput., 40, 646–660 (2014).
- Andrew Siegel, Kord Smith, Kyle Felker, Paul Romano, Benoit Forget, and Peter Beckman, “Improved cache performance in Monte Carlo transport calculations using energy banding,” Comput. Phys. Commun., 185 (4), 1195–1199 (2014).
- Paul K. Romano, Benoit Forget, Kord Smith, and Andrew Siegel, “On the use of tally servers in Monte Carlo simulations of light-water reactors,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
- Kyle G. Felker, Andrew R. Siegel, Kord S. Smith, Paul K. Romano, and Benoit Forget, “The energy band memory server algorithm for parallel Monte Carlo calculations,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
- John R. Tramm and Andrew R. Siegel, “Memory Bottlenecks and Memory Contention in Multi-Core Monte Carlo Transport Codes,” Proc. Joint International Conference on Supercomputing in Nuclear Applications and Monte Carlo, Paris, France, Oct. 27–31 (2013).
- Andrew R. Siegel, Kord Smith, Paul K. Romano, Benoit Forget, and Kyle Felker, “Multi-core performance studies of a Monte Carlo neutron transport code,” Int. J. High Perform. Comput. Appl., 28 (1), 87–96 (2014).
- Paul K. Romano, Andrew R. Siegel, Benoit Forget, and Kord Smith, “Data decomposition of Monte Carlo particle transport simulations via tally servers,” J. Comput. Phys., 252, 20–36 (2013).
- Andrew R. Siegel, Kord Smith, Paul K. Romano, Benoit Forget, and Kyle Felker, “The effect of load imbalances on the performance of Monte Carlo codes in LWR analysis,” J. Comput. Phys., 235, 901–911 (2013).
- Paul K. Romano and Benoit Forget, “Reducing Parallel Communication in Monte Carlo Simulations via Batch Statistics,” Trans. Am. Nucl. Soc., 107, 519–522 (2012).
- Paul K. Romano and Benoit Forget, “Parallel Fission Bank Algorithms in Monte Carlo Criticality Calculations,” Nucl. Sci. Eng., 170, 125–135 (2012).
Depletion¶
- Matthew S. Ellis, Colin Josey, Benoit Forget, and Kord Smith, “Spatially Continuous Depletion Algorithm for Monte Carlo Simulations,” Trans. Am. Nucl. Soc., 115, 1221-1224 (2016).
- Anas Gul, K. S. Chaudri, R. Khan, and M. Azeen, “Development and verification of LOOP: A Linkage of ORIGEN2.2 and OpenMC,” Ann. Nucl. Energy, 99, 321–327 (2017).
- Kai Huang, Hongchun Wu, Yunzhao Li, and Liangzhi Cao, “Generalized depletion chain simplification based of significance analysis,” Proc. PHYSOR, Sun Valley, Idaho, May 1-5, 2016.
License Agreement¶
Copyright © 2011-2017 Massachusetts Institute of Technology
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Development Team¶
The following people have contributed to development of the OpenMC Monte Carlo code: