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


In [1]:
from IPython.display import Image
import numpy as np
import matplotlib.pyplot as plt

import openmc
from openmc.statepoint import StatePoint
from openmc.source import Source
from openmc.stats import Box

%matplotlib inline

Generate Input Files

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


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

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


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

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

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

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


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

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

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


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

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

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


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

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

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

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

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


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

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

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

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


In [8]:
# Create Geometry and set root Universe
geometry = openmc.Geometry()
geometry.root_universe = root_universe

In [9]:
# Instantiate a GeometryFile
geometry_file = openmc.GeometryFile()
geometry_file.geometry = geometry

# Export to "geometry.xml"
geometry_file.export_to_xml()

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


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

# Instantiate a SettingsFile
settings_file = openmc.SettingsFile()
settings_file.batches = batches
settings_file.inactive = inactive
settings_file.particles = particles
source_bounds = [-0.63, -0.63, -0.63, 0.63, 0.63, 0.63]
settings_file.source = Source(space=Box(
        source_bounds[:3], source_bounds[3:]))

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

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


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

# Instantiate a PlotsFile, add Plot, and export to "plots.xml"
plot_file = openmc.PlotsFile()
plot_file.add_plot(plot)
plot_file.export_to_xml()

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


In [12]:
# Run openmc in plotting mode
executor = openmc.Executor()
executor.plot_geometry(output=False)


Out[12]:
0

In [13]:
# Convert OpenMC's funky ppm to png
!convert materials-xy.ppm materials-xy.png

# Display the materials plot inline
Image(filename='materials-xy.png')


Out[13]:

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


In [14]:
# Instantiate an empty TalliesFile
tallies_file = openmc.TalliesFile()

In [15]:
# Create mesh which will be used for tally
mesh = openmc.Mesh()
mesh.dimension = [100, 100]
mesh.lower_left = [-0.63, -0.63]
mesh.upper_right = [0.63, 0.63]
tallies_file.add_mesh(mesh)

# Create mesh filter for tally
mesh_filter = openmc.Filter(type='mesh', bins=[1])
mesh_filter.mesh = mesh

# Create mesh tally to score flux and fission rate
tally = openmc.Tally(name='flux')
tally.add_filter(mesh_filter)
tally.add_score('flux')
tally.add_score('fission')
tallies_file.add_tally(tally)

In [16]:
# Export to "tallies.xml"
tallies_file.export_to_xml()

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


In [17]:
# Run OpenMC!
executor.run_simulation()


       .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-2015 Massachusetts Institute of Technology
      License:        http://mit-crpg.github.io/openmc/license.html
      Version:        0.7.1
      Git SHA1:       ea9fb637f63f9374c7436456141afa850b84acf9
      Date/Time:      2016-01-14 07:02:06

 ===========================================================================
 ========================>     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.04894                       
        2/1    1.01711                       
        3/1    1.05357                       
        4/1    1.03052                       
        5/1    1.06523                       
        6/1    1.06806                       
        7/1    1.05161                       
        8/1    1.04199                       
        9/1    1.05010                       
       10/1    1.04617                       
       11/1    1.04894                       
       12/1    1.06806    1.05850 +/- 0.00956
       13/1    1.05002    1.05567 +/- 0.00620
       14/1    1.03471    1.05043 +/- 0.00683
       15/1    1.01803    1.04395 +/- 0.00837
       16/1    1.05588    1.04594 +/- 0.00712
       17/1    1.07503    1.05010 +/- 0.00731
       18/1    1.02786    1.04732 +/- 0.00691
       19/1    1.00071    1.04214 +/- 0.00800
       20/1    1.05587    1.04351 +/- 0.00729
       21/1    1.03886    1.04309 +/- 0.00660
       22/1    1.04335    1.04311 +/- 0.00603
       23/1    1.04057    1.04292 +/- 0.00555
       24/1    1.01976    1.04126 +/- 0.00540
       25/1    1.05811    1.04238 +/- 0.00515
       26/1    1.02351    1.04120 +/- 0.00496
       27/1    1.05261    1.04188 +/- 0.00471
       28/1    1.03355    1.04141 +/- 0.00446
       29/1    1.02797    1.04071 +/- 0.00428
       30/1    1.03758    1.04055 +/- 0.00406
       31/1    1.04883    1.04094 +/- 0.00388
       32/1    1.03557    1.04070 +/- 0.00371
       33/1    1.02947    1.04021 +/- 0.00358
       34/1    1.03651    1.04006 +/- 0.00343
       35/1    1.03331    1.03979 +/- 0.00330
       36/1    1.05947    1.04054 +/- 0.00326
       37/1    1.05093    1.04093 +/- 0.00316
       38/1    1.06787    1.04189 +/- 0.00319
       39/1    1.01451    1.04095 +/- 0.00322
       40/1    1.02351    1.04037 +/- 0.00317
       41/1    1.04826    1.04062 +/- 0.00307
       42/1    1.04228    1.04067 +/- 0.00298
       43/1    1.03214    1.04041 +/- 0.00290
       44/1    1.04950    1.04068 +/- 0.00282
       45/1    1.06616    1.04141 +/- 0.00284
       46/1    1.07039    1.04221 +/- 0.00287
       47/1    1.00292    1.04115 +/- 0.00299
       48/1    1.04477    1.04125 +/- 0.00291
       49/1    1.03360    1.04105 +/- 0.00284
       50/1    1.04783    1.04122 +/- 0.00277
       51/1    1.03985    1.04119 +/- 0.00271
       52/1    1.02507    1.04080 +/- 0.00267
       53/1    1.03477    1.04066 +/- 0.00261
       54/1    1.00412    1.03983 +/- 0.00268
       55/1    1.02239    1.03945 +/- 0.00265
       56/1    1.04308    1.03952 +/- 0.00259
       57/1    1.05534    1.03986 +/- 0.00256
       58/1    1.06667    1.04042 +/- 0.00257
       59/1    1.06458    1.04091 +/- 0.00256
       60/1    1.00304    1.04015 +/- 0.00262
       61/1    1.05038    1.04036 +/- 0.00258
       62/1    1.02904    1.04014 +/- 0.00254
       63/1    1.00249    1.03943 +/- 0.00259
       64/1    1.01779    1.03903 +/- 0.00257
       65/1    1.05335    1.03929 +/- 0.00254
       66/1    1.06231    1.03970 +/- 0.00253
       67/1    1.02382    1.03942 +/- 0.00250
       68/1    1.03796    1.03939 +/- 0.00245
       69/1    1.03672    1.03935 +/- 0.00241
       70/1    1.02926    1.03918 +/- 0.00238
       71/1    1.05834    1.03950 +/- 0.00236
       72/1    1.04332    1.03956 +/- 0.00232
       73/1    1.05613    1.03982 +/- 0.00230
       74/1    1.01963    1.03950 +/- 0.00228
       75/1    1.02228    1.03924 +/- 0.00226
       76/1    1.04842    1.03938 +/- 0.00223
       77/1    1.02157    1.03911 +/- 0.00222
       78/1    1.02810    1.03895 +/- 0.00219
       79/1    1.05030    1.03912 +/- 0.00216
       80/1    1.02391    1.03890 +/- 0.00214
       81/1    1.02488    1.03870 +/- 0.00212
       82/1    1.04957    1.03885 +/- 0.00210
       83/1    1.03499    1.03880 +/- 0.00207
       84/1    1.05922    1.03907 +/- 0.00206
       85/1    1.05898    1.03934 +/- 0.00205
       86/1    1.02242    1.03912 +/- 0.00204
       87/1    1.03278    1.03904 +/- 0.00201
       88/1    1.06134    1.03932 +/- 0.00201
       89/1    1.04521    1.03940 +/- 0.00198
       90/1    1.04277    1.03944 +/- 0.00196
       91/1    1.04214    1.03947 +/- 0.00193
       92/1    1.05610    1.03967 +/- 0.00192
       93/1    1.04531    1.03974 +/- 0.00190
       94/1    1.01534    1.03945 +/- 0.00190
       95/1    1.03971    1.03945 +/- 0.00187
       96/1    1.07183    1.03983 +/- 0.00189
       97/1    1.07214    1.04020 +/- 0.00191
       98/1    1.03710    1.04017 +/- 0.00188
       99/1    1.02532    1.04000 +/- 0.00187
      100/1    1.03965    1.04000 +/- 0.00185
 Creating state point statepoint.100.h5...

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


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

 Total time for initialization     =  3.6000E-01 seconds
   Reading cross sections          =  1.0600E-01 seconds
 Total time in simulation          =  2.5756E+02 seconds
   Time in transport only          =  2.5751E+02 seconds
   Time in inactive batches        =  9.7270E+00 seconds
   Time in active batches          =  2.4783E+02 seconds
   Time synchronizing fission bank =  2.1000E-02 seconds
     Sampling source sites         =  1.3000E-02 seconds
     SEND/RECV source sites        =  8.0000E-03 seconds
   Time accumulating tallies       =  1.3000E-02 seconds
 Total time for finalization       =  1.4600E-01 seconds
 Total time elapsed                =  2.5809E+02 seconds
 Calculation Rate (inactive)       =  5140.33 neutrons/second
 Calculation Rate (active)         =  1815.75 neutrons/second

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

 k-effective (Collision)     =  1.03912 +/-  0.00160
 k-effective (Track-length)  =  1.04000 +/-  0.00185
 k-effective (Absorption)    =  1.04240 +/-  0.00156
 Combined k-effective        =  1.04078 +/-  0.00127
 Leakage Fraction            =  0.00000 +/-  0.00000

Out[17]:
0

Tally Data Processing

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


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

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


In [19]:
tally = sp.get_tally(scores=['flux'])
print(tally)


Tally
	ID             =	10000
	Name           =	
	Filters        =	
                		mesh	[10000]
	Nuclides       =	total 
	Scores         =	[u'flux', u'fission']
	Estimator      =	tracklength

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


In [20]:
tally.sum


Out[20]:
array([[[ 0.4107676 ,  0.        ]],

       [[ 0.40849402,  0.        ]],

       [[ 0.41014343,  0.        ]],

       ..., 
       [[ 0.41049467,  0.        ]],

       [[ 0.40982242,  0.        ]],

       [[ 0.40996987,  0.        ]]])

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


In [21]:
print(tally.mean.shape)
(tally.mean, tally.std_dev)


(10000, 1, 2)
Out[21]:
(array([[[ 0.00456408,  0.        ]],
 
        [[ 0.00453882,  0.        ]],
 
        [[ 0.00455715,  0.        ]],
 
        ..., 
        [[ 0.00456105,  0.        ]],
 
        [[ 0.00455358,  0.        ]],
 
        [[ 0.00455522,  0.        ]]]),
 array([[[  1.95085625e-05,   0.00000000e+00]],
 
        [[  1.78129859e-05,   0.00000000e+00]],
 
        [[  1.89709648e-05,   0.00000000e+00]],
 
        ..., 
        [[  1.56286612e-05,   0.00000000e+00]],
 
        [[  1.65813279e-05,   0.00000000e+00]],
 
        [[  1.67530331e-05,   0.00000000e+00]]]))

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


In [22]:
flux = tally.get_slice(scores=['flux'])
fission = tally.get_slice(scores=['fission'])
print(flux)


Tally
	ID             =	10000
	Name           =	
	Filters        =	
                		mesh	[10000]
	Nuclides       =	total 
	Scores         =	[u'flux']
	Estimator      =	tracklength

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


In [23]:
flux.std_dev.shape = (100, 100)
flux.mean.shape = (100, 100)
fission.std_dev.shape = (100, 100)
fission.mean.shape = (100, 100)

In [24]:
fig = plt.subplot(121)
fig.imshow(flux.mean)
fig2 = plt.subplot(122)
fig2.imshow(fission.mean)


Out[24]:
<matplotlib.image.AxesImage at 0x7fc3808ca950>

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


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

# distribution of relative errors
ret = plt.hist(relative_error[nonzero], bins=50)


Source Sites

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


In [26]:
sp.source


Out[26]:
array([ (1.0, [0.2712169917165897, -0.04844236597355761, -0.1887902218343974], [0.3889598463000694, 0.8470657529949065, 0.36220139158953857], 2.2746035619924734, 0),
       (1.0, [0.080729018085932, 0.19838688738571317, -0.38053428394017363], [-0.6604834049157511, -0.6893239101986768, 0.2976478097673534], 0.7833467555325838, 0),
       (1.0, [0.019430574216787868, 0.06594180627832635, 0.23329810254580194], [-0.7472138923667574, 0.13227244377548197, -0.651287493870243], 1.1632342240714935, 0),
       ...,
       (1.0, [0.18544614514351207, -0.0113070561851496, 0.5468392238881264], [-0.8006491411918817, 0.43855795172388223, -0.4082007786475368], 1.4358240241589555, 0),
       (1.0, [0.18544614514351207, -0.0113070561851496, 0.5468392238881264], [-0.5150076397044656, -0.34922134026850293, 0.7828228321575105], 1.5771133724329802, 0),
       (1.0, [-0.2722999793764598, 0.22680062445008103, 0.2987060438567475], [0.9207818175032396, -0.2884020326181676, 0.26265017063984586], 2.932342523379745, 0)], 
      dtype=[('wgt', '<f8'), ('xyz', '<f8', (3,)), ('uvw', '<f8', (3,)), ('E', '<f8'), ('delayed_group', '<i4')])

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


In [27]:
sp.source['E']


Out[27]:
array([ 2.27460356,  0.78334676,  1.16323422, ...,  1.43582402,
        1.57711337,  2.93234252])

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


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

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

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

# Plot source energy PDF
plt.semilogx(energy_bins[:-1], probability*np.diff(energy_bins), linestyle='steps')
plt.xlabel('Energy (MeV)')
plt.ylabel('Probability/MeV')


1.0
Out[28]:
<matplotlib.text.Text at 0x7fc380511390>

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


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


Out[29]:
(-0.5, 0.5)
/usr/local/lib/python2.7/dist-packages/matplotlib/collections.py:590: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
  if self._edgecolors == str('face'):