MGXS Part III: LibrariesΒΆ

mgxs-part-iii

This IPython Notebook illustrates the use of the openmc.mgxs.Library class. The Library class is designed to automate the calculation of multi-group cross sections for use cases with one or more domains, cross section types, and/or nuclides. In particular, this Notebook illustrates the following features:

  • Calculation of multi-group cross sections for a fuel assembly
  • Automated creation, manipulation and storage of MGXS with openmc.mgxs.Library
  • Validation of multi-group cross sections with OpenMOC
  • Steady-state pin-by-pin fission rates comparison between OpenMC and OpenMOC

Note: This Notebook was created using OpenMOC to verify the multi-group cross-sections generated by OpenMC. In order to run this Notebook in its entirety, you must have OpenMOC installed on your system, along with OpenCG to convert the OpenMC geometries into OpenMOC geometries. In addition, this Notebook illustrates the use of Pandas DataFrames to containerize multi-group cross section data.

Generate Input Files

In [1]:
import math
import pickle

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

import openmc
import openmc.mgxs
import openmoc
import openmoc.process
from openmoc.opencg_compatible import get_openmoc_geometry
from openmoc.materialize import load_openmc_mgxs_lib

%matplotlib inline
/home/wboyd/anaconda2/lib/python2.7/site-packages/matplotlib/__init__.py:1350: UserWarning:  This call to matplotlib.use() has no effect
because the backend has already been chosen;
matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
or matplotlib.backends is imported for the first time.

  warnings.warn(_use_error_msg)

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

In [2]:
# Instantiate some Nuclides
h1 = openmc.Nuclide('H-1')
b10 = openmc.Nuclide('B-10')
o16 = openmc.Nuclide('O-16')
u235 = openmc.Nuclide('U-235')
u238 = openmc.Nuclide('U-238')
zr90 = openmc.Nuclide('Zr-90')

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

In [3]:
# 1.6 enriched fuel
fuel = openmc.Material(name='1.6% Fuel')
fuel.set_density('g/cm3', 10.31341)
fuel.add_nuclide(u235, 3.7503e-4)
fuel.add_nuclide(u238, 2.2625e-2)
fuel.add_nuclide(o16, 4.6007e-2)

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

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

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

In [4]:
# Instantiate a Materials object
materials_file = openmc.Materials((fuel, water, zircaloy))
materials_file.default_xs = '71c'

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

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

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

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

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

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

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

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

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

Likewise, we can construct a control rod guide tube with the same surfaces.

In [7]:
# Create a Universe to encapsulate a control rod guide tube
guide_tube_universe = openmc.Universe(name='Guide Tube')

# Create guide tube Cell
guide_tube_cell = openmc.Cell(name='Guide Tube Water')
guide_tube_cell.fill = water
guide_tube_cell.region = -fuel_outer_radius
guide_tube_universe.add_cell(guide_tube_cell)

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

# Create a moderator Cell
moderator_cell = openmc.Cell(name='Guide Tube Moderator')
moderator_cell.fill = water
moderator_cell.region = +clad_outer_radius
guide_tube_universe.add_cell(moderator_cell)

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

In [8]:
# Create fuel assembly Lattice
assembly = openmc.RectLattice(name='1.6% Fuel Assembly')
assembly.pitch = (1.26, 1.26)
assembly.lower_left = [-1.26 * 17. / 2.0] * 2

Next, we create a NumPy array of fuel pin and guide tube universes for the lattice.

In [9]:
# Create array indices for guide tube locations in lattice
template_x = np.array([5, 8, 11, 3, 13, 2, 5, 8, 11, 14, 2, 5, 8,
                       11, 14, 2, 5, 8, 11, 14, 3, 13, 5, 8, 11])
template_y = np.array([2, 2, 2, 3, 3, 5, 5, 5, 5, 5, 8, 8, 8, 8,
                       8, 11, 11, 11, 11, 11, 13, 13, 14, 14, 14])

# Initialize an empty 17x17 array of the lattice universes
universes = np.empty((17, 17), dtype=openmc.Universe)

# Fill the array with the fuel pin and guide tube universes
universes[:,:] = fuel_pin_universe
universes[template_x, template_y] = guide_tube_universe

# Store the array of universes in the lattice
assembly.universes = universes

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

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

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

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

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

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

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

In [13]:
# OpenMC simulation parameters
batches = 50
inactive = 10
particles = 2500

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

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

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

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

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

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

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

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

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

As we can see from the plot, we have a nice array of fuel and guide tube pin cells with fuel, cladding, and water!

Create an MGXS Library

Now we are ready to generate multi-group cross sections! First, let's define a 2-group structure using the built-in EnergyGroups class.

In [17]:
# Instantiate a 2-group EnergyGroups object
groups = openmc.mgxs.EnergyGroups()
groups.group_edges = np.array([0., 0.625e-6, 20.])

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

In [18]:
# Initialize an 2-group MGXS Library for OpenMOC
mgxs_lib = openmc.mgxs.Library(geometry)
mgxs_lib.energy_groups = groups

Now, we must specify to the Library which types of cross sections to compute. In particular, the following are the multi-group cross section MGXS subclasses that are mapped to string codes accepted by the Library class:

  • TotalXS ("total")
  • TransportXS ("transport")
  • NuTransportXS ("nu-transport")
  • AbsorptionXS ("absorption")
  • CaptureXS ("capture")
  • FissionXS ("fission")
  • NuFissionXS ("nu-fission")
  • KappaFissionXS ("kappa-fission")
  • ScatterXS ("scatter")
  • NuScatterXS ("nu-scatter")
  • ScatterMatrixXS ("scatter matrix")
  • NuScatterMatrixXS ("nu-scatter matrix")
  • Chi ("chi")

In this case, let's create the multi-group cross sections needed to run an OpenMOC simulation to verify the accuracy of our cross sections. In particular, we will define "transport", "nu-fission", '"fission", "nu-scatter matrix" and "chi" cross sections for our Library.

Note: A variety of different approximate transport-corrected total multi-group cross sections (and corresponding scattering matrices) can be found in the literature. At the present time, the openmc.mgxs module only supports the "P0" transport correction. This correction can be turned on and off through the boolean Library.correction property which may take values of "P0" (default) or None.

In [19]:
# Specify multi-group cross section types to compute
mgxs_lib.mgxs_types = ['transport', 'nu-fission', 'fission', 'nu-scatter matrix', 'chi']

Now we must specify the type of domain over which we would like the Library to compute multi-group cross sections. The domain type corresponds to the type of tally filter to be used in the tallies created to compute multi-group cross sections. At the present time, the Library supports "material," "cell," and "universe" domain types. We will use a "cell" domain type here to compute cross sections in each of the cells in the fuel assembly geometry.

Note: By default, the Library class will instantiate MGXS objects for each and every domain (material, cell or universe) in the geometry of interest. However, one may specify a subset of these domains to the Library.domains property. In our case, we wish to compute multi-group cross sections in each and every cell since they will be needed in our downstream OpenMOC calculation on the identical combinatorial geometry mesh.

In [20]:
# Specify a "cell" domain type for the cross section tally filters
mgxs_lib.domain_type = 'cell'

# Specify the cell domains over which to compute multi-group cross sections
mgxs_lib.domains = geometry.get_all_material_cells()

We can easily instruct the Library to compute multi-group cross sections on a nuclide-by-nuclide basis with the boolean Library.by_nuclide property. By default, by_nuclide is set to False, but we will set it to True here.

In [21]:
# Compute cross sections on a nuclide-by-nuclide basis
mgxs_lib.by_nuclide = True

Lastly, we use the Library to construct the tallies needed to compute all of the requested multi-group cross sections in each domain and nuclide.

In [22]:
# Construct all tallies needed for the multi-group cross section library
mgxs_lib.build_library()

The tallies can now be export to a "tallies.xml" input file for OpenMC.

NOTE: At this point the Library has constructed nearly 100 distinct Tally objects. The overhead to tally in OpenMC scales as $O(N)$ for $N$ tallies, which can become a bottleneck for large tally datasets. To compensate for this, the Python API's Tally, Filter and Tallies classes allow for the smart merging of tallies when possible. The Library class supports this runtime optimization with the use of the optional merge paramter (False by default) for the Library.add_to_tallies_file(...) method, as shown below.

In [23]:
# Create a "tallies.xml" file for the MGXS Library
tallies_file = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies_file, merge=True)

In addition, we instantiate a fission rate mesh tally to compare with OpenMOC.

In [24]:
# Instantiate a tally Mesh
mesh = openmc.Mesh(mesh_id=1)
mesh.type = 'regular'
mesh.dimension = [17, 17]
mesh.lower_left = [-10.71, -10.71]
mesh.upper_right = [+10.71, +10.71]

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

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

# Add tally to collection
tallies_file.append(tally)
In [25]:
# Export all tallies to a "tallies.xml" file
tallies_file.export_to_xml()
In [26]:
# Run OpenMC
openmc.run()
       .d88888b.                             888b     d888  .d8888b.
      d88P" "Y88b                            8888b   d8888 d88P  Y88b
      888     888                            88888b.d88888 888    888
      888     888 88888b.   .d88b.  88888b.  888Y88888P888 888       
      888     888 888 "88b d8P  Y8b 888 "88b 888 Y888P 888 888       
      888     888 888  888 88888888 888  888 888  Y8P  888 888    888
      Y88b. .d88P 888 d88P Y8b.     888  888 888   "   888 Y88b  d88P
       "Y88888P"  88888P"   "Y8888  888  888 888       888  "Y8888P"
__________________888______________________________________________________
                  888
                  888

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

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

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

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

  Bat./Gen.      k            Average k         
  =========   ========   ====================   
        1/1    1.03852                       
        2/1    0.99743                       
        3/1    1.02987                       
        4/1    1.04472                       
        5/1    1.02183                       
        6/1    1.05263                       
        7/1    0.99048                       
        8/1    1.02753                       
        9/1    1.03159                       
       10/1    1.04005                       
       11/1    1.05278                       
       12/1    1.02555    1.03917 +/- 0.01362
       13/1    0.99400    1.02411 +/- 0.01699
       14/1    1.03508    1.02685 +/- 0.01232
       15/1    1.00055    1.02159 +/- 0.01090
       16/1    1.01334    1.02022 +/- 0.00900
       17/1    0.99822    1.01707 +/- 0.00823
       18/1    1.01767    1.01715 +/- 0.00713
       19/1    1.05052    1.02086 +/- 0.00730
       20/1    1.03133    1.02190 +/- 0.00661
       21/1    1.04112    1.02365 +/- 0.00623
       22/1    1.04175    1.02516 +/- 0.00588
       23/1    1.01909    1.02469 +/- 0.00543
       24/1    1.07119    1.02801 +/- 0.00603
       25/1    0.97445    1.02444 +/- 0.00665
       26/1    1.04737    1.02588 +/- 0.00638
       27/1    1.04656    1.02709 +/- 0.00612
       28/1    1.03464    1.02751 +/- 0.00578
       29/1    1.02528    1.02739 +/- 0.00547
       30/1    1.02799    1.02742 +/- 0.00519
       31/1    1.05846    1.02890 +/- 0.00516
       32/1    1.03811    1.02932 +/- 0.00493
       33/1    1.00894    1.02843 +/- 0.00480
       34/1    1.02049    1.02810 +/- 0.00460
       35/1    1.00690    1.02726 +/- 0.00450
       36/1    1.03129    1.02741 +/- 0.00432
       37/1    0.98864    1.02597 +/- 0.00440
       38/1    1.00017    1.02505 +/- 0.00434
       39/1    1.03635    1.02544 +/- 0.00421
       40/1    1.07090    1.02696 +/- 0.00434
       41/1    1.03141    1.02710 +/- 0.00420
       42/1    1.02624    1.02707 +/- 0.00406
       43/1    1.02668    1.02706 +/- 0.00394
       44/1    1.05940    1.02801 +/- 0.00394
       45/1    1.01149    1.02754 +/- 0.00385
       46/1    1.06958    1.02871 +/- 0.00392
       47/1    1.02674    1.02866 +/- 0.00381
       48/1    1.02542    1.02857 +/- 0.00371
       49/1    1.03516    1.02874 +/- 0.00362
       50/1    1.06818    1.02973 +/- 0.00366
 Creating state point statepoint.50.h5...

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


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

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

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

 k-effective (Collision)     =  1.02763 +/-  0.00343
 k-effective (Track-length)  =  1.02973 +/-  0.00366
 k-effective (Absorption)    =  1.02732 +/-  0.00319
 Combined k-effective        =  1.02826 +/-  0.00259
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[26]:
0

Tally Data Processing

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

In [27]:
# Load the last statepoint file
sp = openmc.StatePoint('statepoint.50.h5')

The statepoint is now ready to be analyzed by the Library. We simply have to load the tallies from the statepoint into the Library and our MGXS objects will compute the cross sections for us under-the-hood.

In [28]:
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)

Voila! Our multi-group cross sections are now ready to rock 'n roll!

Extracting and Storing MGXS Data

The Library supports a rich API to automate a variety of tasks, including multi-group cross section data retrieval and storage. We will highlight a few of these features here. First, the Library.get_mgxs(...) method allows one to extract an MGXS object from the Library for a particular domain and cross section type. The following cell illustrates how one may extract the NuFissionXS object for the fuel cell.

Note: The MGXS.get_mgxs(...) method will accept either the domain or the integer domain ID of interest.

In [29]:
# Retrieve the NuFissionXS object for the fuel cell from the library
fuel_mgxs = mgxs_lib.get_mgxs(fuel_cell, 'nu-fission')

The NuFissionXS object supports all of the methods described previously in the openmc.mgxs tutorials, such as Pandas DataFrames: Note that since so few histories were simulated, we should expect a few division-by-error errors as some tallies have not yet scored any results.

In [30]:
df = fuel_mgxs.get_pandas_dataframe()
df
/home/wboyd/Documents/NSE-CRPG-Codes/openmc/openmc/tallies.py:1988: RuntimeWarning: invalid value encountered in true_divide
  self_rel_err = data['self']['std. dev.'] / data['self']['mean']
Out[30]:
cell group in nuclide mean std. dev.
3 10000 1 U-235 8.055246e-03 2.857567e-05
4 10000 1 U-238 7.339215e-03 4.349466e-05
5 10000 1 O-16 0.000000e+00 0.000000e+00
0 10000 2 U-235 3.615565e-01 2.050486e-03
1 10000 2 U-238 6.742638e-07 3.795256e-09
2 10000 2 O-16 0.000000e+00 0.000000e+00

Similarly, we can use the MGXS.print_xs(...) method to view a string representation of the multi-group cross section data.

In [31]:
fuel_mgxs.print_xs()
Multi-Group XS
	Reaction Type  =	nu-fission
	Domain Type    =	cell
	Domain ID      =	10000
	Nuclide        =	U-235
	Cross Sections [cm^-1]:
            Group 1 [6.25e-07   - 20.0      MeV]:	8.06e-03 +/- 3.55e-01%
            Group 2 [0.0        - 6.25e-07  MeV]:	3.62e-01 +/- 5.67e-01%

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

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



One can export the entire Library to HDF5 with the Library.build_hdf5_store(...) method as follows:

In [32]:
# Store the cross section data in an "mgxs/mgxs.h5" HDF5 binary file
mgxs_lib.build_hdf5_store(filename='mgxs.h5', directory='mgxs')

The HDF5 store will contain the numerical multi-group cross section data indexed by domain, nuclide and cross section type. Some data workflows may be optimized by storing and retrieving binary representations of the MGXS objects in the Library. This feature is supported through the Library.dump_to_file(...) and Library.load_from_file(...) routines which use Python's pickle module. This is illustrated as follows.

In [33]:
# Store a Library and its MGXS objects in a pickled binary file "mgxs/mgxs.pkl"
mgxs_lib.dump_to_file(filename='mgxs', directory='mgxs')
In [34]:
# Instantiate a new MGXS Library from the pickled binary file "mgxs/mgxs.pkl"
mgxs_lib = openmc.mgxs.Library.load_from_file(filename='mgxs', directory='mgxs')

The Library class may be used to leverage the energy condensation features supported by the MGXS class. In particular, one can use the Library.get_condensed_library(...) with a coarse group structure which is a subset of the original "fine" group structure as shown below.

In [35]:
# Create a 1-group structure
coarse_groups = openmc.mgxs.EnergyGroups(group_edges=[0., 20.])

# Create a new MGXS Library on the coarse 1-group structure
coarse_mgxs_lib = mgxs_lib.get_condensed_library(coarse_groups)
In [36]:
# Retrieve the NuFissionXS object for the fuel cell from the 1-group library
coarse_fuel_mgxs = coarse_mgxs_lib.get_mgxs(fuel_cell, 'nu-fission')

# Show the Pandas DataFrame for the 1-group MGXS
coarse_fuel_mgxs.get_pandas_dataframe()
Out[36]:
cell group in nuclide mean std. dev.
0 10000 1 U-235 0.074860 0.000303
1 10000 1 U-238 0.005952 0.000035
2 10000 1 O-16 0.000000 0.000000

Verification with OpenMOC

Of course it is always a good idea to verify that one's cross sections are accurate. We can easily do so here with the deterministic transport code OpenMOC. We will extract an OpenCG geometry from the summary file and convert it into an equivalent OpenMOC geometry.

In [37]:
# Create an OpenMOC Geometry from the OpenCG Geometry
openmoc_geometry = get_openmoc_geometry(mgxs_lib.opencg_geometry)

Now, we can inject the multi-group cross sections into the equivalent fuel assembly OpenMOC geometry. The openmoc.materialize module supports the loading of Library objects from OpenMC as illustrated below.

In [38]:
# Load the library into the OpenMOC geometry
materials = load_openmc_mgxs_lib(mgxs_lib, openmoc_geometry)

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

In [39]:
# Generate tracks for OpenMOC
track_generator = openmoc.TrackGenerator(openmoc_geometry, num_azim=32, azim_spacing=0.1)
track_generator.generateTracks()

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

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

In [40]:
# Print report of keff and bias with OpenMC
openmoc_keff = solver.getKeff()
openmc_keff = sp.k_combined[0]
bias = (openmoc_keff - openmc_keff) * 1e5

print('openmc keff = {0:1.6f}'.format(openmc_keff))
print('openmoc keff = {0:1.6f}'.format(openmoc_keff))
print('bias [pcm]: {0:1.1f}'.format(bias))
openmc keff = 1.028263
openmoc keff = 1.028491
bias [pcm]: 22.8

There is a non-trivial bias between the eigenvalues computed by OpenMC and OpenMOC. One can show that these biases do not converge to <100 pcm with more particle histories. For heterogeneous geometries, additional measures must be taken to address the following three sources of bias:

  • Appropriate transport-corrected cross sections
  • Spatial discretization of OpenMOC's mesh
  • Constant-in-angle multi-group cross sections

Flux and Pin Power Visualizations

We will conclude this tutorial by illustrating how to visualize the fission rates computed by OpenMOC and OpenMC. First, we extract volume-integrated fission rates from OpenMC's mesh fission rate tally for each pin cell in the fuel assembly.

In [41]:
# Get the OpenMC fission rate mesh tally data
mesh_tally = sp.get_tally(name='mesh tally')
openmc_fission_rates = mesh_tally.get_values(scores=['nu-fission'])

# Reshape array to 2D for plotting
openmc_fission_rates.shape = (17,17)

# Normalize to the average pin power
openmc_fission_rates /= np.mean(openmc_fission_rates)

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

In [42]:
# Create OpenMOC Mesh on which to tally fission rates
openmoc_mesh = openmoc.process.Mesh()
openmoc_mesh.dimension = np.array(mesh.dimension)
openmoc_mesh.lower_left = np.array(mesh.lower_left)
openmoc_mesh.upper_right = np.array(mesh.upper_right)
openmoc_mesh.width = openmoc_mesh.upper_right - openmoc_mesh.lower_left
openmoc_mesh.width /= openmoc_mesh.dimension

# Tally OpenMOC fission rates on the Mesh
openmoc_fission_rates = openmoc_mesh.tally_fission_rates(solver)
openmoc_fission_rates = np.squeeze(openmoc_fission_rates)
openmoc_fission_rates = np.fliplr(openmoc_fission_rates)

# Normalize to the average pin fission rate
openmoc_fission_rates /= np.mean(openmoc_fission_rates)

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

In [43]:
# Ignore zero fission rates in guide tubes with Matplotlib color scheme
openmc_fission_rates[openmc_fission_rates == 0] = np.nan
openmoc_fission_rates[openmoc_fission_rates == 0] = np.nan

# Plot OpenMC's fission rates in the left subplot
fig = plt.subplot(121)
plt.imshow(openmc_fission_rates, interpolation='none', cmap='jet')
plt.title('OpenMC Fission Rates')

# Plot OpenMOC's fission rates in the right subplot
fig2 = plt.subplot(122)
plt.imshow(openmoc_fission_rates, interpolation='none', cmap='jet')
plt.title('OpenMOC Fission Rates')
Out[43]:
<matplotlib.text.Text at 0x7f1d808d8b50>