from __future__ import division
from abc import ABCMeta
from collections import Iterable, OrderedDict
import copy
import hashlib
from numbers import Real, Integral
from xml.etree import ElementTree as ET
from six import add_metaclass
import numpy as np
import openmc
import openmc.checkvalue as cv
_FILTER_TYPES = ['universe', 'material', 'cell', 'cellborn', 'surface',
'mesh', 'energy', 'energyout', 'mu', 'polar', 'azimuthal',
'distribcell', 'delayedgroup', 'energyfunction']
_CURRENT_NAMES = {1: 'x-min out', 2: 'x-min in',
3: 'x-max out', 4: 'x-max in',
5: 'y-min out', 6: 'y-min in',
7: 'y-max out', 8: 'y-max in',
9: 'z-min out', 10: 'z-min in',
11: 'z-max out', 12: 'z-max in'}
class FilterMeta(ABCMeta):
def __new__(cls, name, bases, namespace, **kwargs):
# Check the class name.
if not name.endswith('Filter'):
raise ValueError("All filter class names must end with 'Filter'")
# Create a 'short_name' attribute that removes the 'Filter' suffix.
namespace['short_name'] = name[:-6]
# Subclass methods can sort of inherit the docstring of parent class
# methods. If a function is defined without a docstring, most (all?)
# Python interpreters will search through the parent classes to see if
# there is a docstring for a function with the same name, and they will
# use that docstring. However, Sphinx does not have that functionality.
# This chunk of code handles this docstring inheritance manually so that
# the autodocumentation will pick it up.
if name != 'Filter':
# Look for newly-defined functions that were also in Filter.
for func_name in namespace:
if func_name in Filter.__dict__:
# Inherit the docstring from Filter if not defined.
if isinstance(namespace[func_name],
(classmethod, staticmethod)):
new_doc = namespace[func_name].__func__.__doc__
old_doc = Filter.__dict__[func_name].__func__.__doc__
if new_doc is None and old_doc is not None:
namespace[func_name].__func__.__doc__ = old_doc
else:
new_doc = namespace[func_name].__doc__
old_doc = Filter.__dict__[func_name].__doc__
if new_doc is None and old_doc is not None:
namespace[func_name].__doc__ = old_doc
# Make the class.
return super(FilterMeta, cls).__new__(cls, name, bases, namespace,
**kwargs)
@add_metaclass(FilterMeta)
[docs]class Filter(object):
"""Tally modifier that describes phase-space and other characteristics.
Parameters
----------
bins : Integral or Iterable of Integral or Iterable of Real
The bins for the filter. This takes on different meaning for different
filters. See the docstrings for sublcasses of this filter or the online
documentation for more details.
Attributes
----------
bins : Integral or Iterable of Integral or Iterable of Real
The bins for the filter
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
def __init__(self, bins):
self.bins = bins
self._num_bins = 0
self._stride = None
def __eq__(self, other):
if type(self) is not type(other):
return False
elif len(self.bins) != len(other.bins):
return False
elif not np.allclose(self.bins, other.bins):
return False
else:
return True
def __ne__(self, other):
return not self == other
def __gt__(self, other):
if type(self) is not type(other):
if self.short_name in _FILTER_TYPES and \
other.short_name in _FILTER_TYPES:
delta = _FILTER_TYPES.index(self.short_name) - \
_FILTER_TYPES.index(other.short_name)
return delta > 0
else:
return False
else:
return max(self.bins) > max(other.bins)
def __lt__(self, other):
return not self > other
def __hash__(self):
return hash(repr(self))
def __repr__(self):
string = type(self).__name__ + '\n'
string += '{: <16}=\t{}\n'.format('\tBins', self.bins)
return string
@classmethod
def _recursive_subclasses(cls):
"""Return all subclasses and their subclasses, etc."""
all_subclasses = []
for subclass in cls.__subclasses__():
all_subclasses.append(subclass)
all_subclasses.extend(subclass._recursive_subclasses())
return all_subclasses
@classmethod
[docs] def from_hdf5(cls, group, **kwargs):
"""Construct a new Filter instance from HDF5 data.
Parameters
----------
group : h5py.Group
HDF5 group to read from
Keyword arguments
-----------------
meshes : dict
Dictionary mapping integer IDs to openmc.Mesh objects. Only used
for openmc.MeshFilter objects.
"""
# If the HDF5 'type' variable matches this class's short_name, then
# there is no overriden from_hdf5 method. Pass the bins to __init__.
if group['type'].value.decode() == cls.short_name.lower():
out = cls(group['bins'].value)
out.num_bins = group['n_bins'].value
return out
# Search through all subclasses and find the one matching the HDF5
# 'type'. Call that class's from_hdf5 method.
for subclass in cls._recursive_subclasses():
if group['type'].value.decode() == subclass.short_name.lower():
return subclass.from_hdf5(group, **kwargs)
raise ValueError("Unrecognized Filter class: '"
+ group['type'].value.decode() + "'")
@property
def bins(self):
return self._bins
@property
def num_bins(self):
return self._num_bins
@property
def stride(self):
return self._stride
@bins.setter
def bins(self, bins):
# Format the bins as a 1D numpy array.
bins = np.atleast_1d(bins)
# Check the bin values.
self.check_bins(bins)
self._bins = bins
@num_bins.setter
def num_bins(self, num_bins):
cv.check_type('filter num_bins', num_bins, Integral)
cv.check_greater_than('filter num_bins', num_bins, 0, equality=True)
self._num_bins = num_bins
@stride.setter
def stride(self, stride):
cv.check_type('filter stride', stride, Integral)
if stride < 0:
msg = 'Unable to set stride "{0}" for a "{1}" since it ' \
'is a negative value'.format(stride, type(self))
raise ValueError(msg)
self._stride = stride
[docs] def check_bins(self, bins):
"""Make sure given bins are valid for this filter.
Raises
------
TypeError
ValueError
"""
pass
[docs] def to_xml_element(self):
"""Return XML Element representing the Filter.
Returns
-------
ElementTree.Element
"""
element = ET.Element('filter')
element.set('type', self.short_name.lower())
element.set('bins', ' '.join(str(b) for b in self.bins))
return element
[docs] def can_merge(self, other):
"""Determine if filter can be merged with another.
Parameters
----------
other : openmc.Filter
Filter to compare with
Returns
-------
bool
Whether the filter can be merged
"""
if type(self) is not type(other):
return False
return True
[docs] def merge(self, other):
"""Merge this filter with another.
Parameters
----------
other : openmc.Filter
Filter to merge with
Returns
-------
merged_filter : openmc.Filter
Filter resulting from the merge
"""
if not self.can_merge(other):
msg = 'Unable to merge "{0}" with "{1}" '.format(
type(self), type(other))
raise ValueError(msg)
# Merge unique filter bins
merged_bins = np.concatenate((self.bins, other.bins))
merged_bins = np.unique(merged_bins)
# Create a new filter with these bins
return type(self)(merged_bins)
[docs] def is_subset(self, other):
"""Determine if another filter is a subset of this filter.
If all of the bins in the other filter are included as bins in this
filter, then it is a subset of this filter.
Parameters
----------
other : openmc.Filter
The filter to query as a subset of this filter
Returns
-------
bool
Whether or not the other filter is a subset of this filter
"""
if type(self) is not type(other):
return False
for bin in other.bins:
if bin not in self.bins:
return False
return True
[docs] def get_bin_index(self, filter_bin):
"""Returns the index in the Filter for some bin.
Parameters
----------
filter_bin : Integral or tuple
The bin is the integer ID for 'material', 'surface', 'cell',
'cellborn', and 'universe' Filters. The bin is an integer for the
cell instance ID for 'distribcell' Filters. The bin is a 2-tuple of
floats for 'energy' and 'energyout' filters corresponding to the
energy boundaries of the bin of interest. The bin is an (x,y,z)
3-tuple for 'mesh' filters corresponding to the mesh cell of
interest.
Returns
-------
filter_index : Integral
The index in the Tally data array for this filter bin.
See also
--------
Filter.get_bin()
"""
if filter_bin not in self.bins:
msg = 'Unable to get the bin index for Filter since "{0}" ' \
'is not one of the bins'.format(filter_bin)
raise ValueError(msg)
return np.where(self.bins == filter_bin)[0][0]
[docs] def get_bin(self, bin_index):
"""Returns the filter bin for some filter bin index.
Parameters
----------
bin_index : Integral
The zero-based index into the filter's array of bins. The bin
index for 'material', 'surface', 'cell', 'cellborn', and 'universe'
filters corresponds to the ID in the filter's list of bins. For
'distribcell' tallies the bin index necessarily can only be zero
since only one cell can be tracked per tally. The bin index for
'energy' and 'energyout' filters corresponds to the energy range of
interest in the filter bins of energies. The bin index for 'mesh'
filters is the index into the flattened array of (x,y) or (x,y,z)
mesh cell bins.
Returns
-------
bin : 1-, 2-, or 3-tuple of Real
The bin in the Tally data array. The bin for 'material', surface',
'cell', 'cellborn', 'universe' and 'distribcell' filters is a
1-tuple of the ID corresponding to the appropriate filter bin.
The bin for 'energy' and 'energyout' filters is a 2-tuple of the
lower and upper energies bounding the energy interval for the filter
bin. The bin for 'mesh' tallies is a 2-tuple or 3-tuple of the x,y
or x,y,z mesh cell indices corresponding to the bin in a 2D/3D mesh.
See also
--------
Filter.get_bin_index()
"""
cv.check_type('bin_index', bin_index, Integral)
cv.check_greater_than('bin_index', bin_index, 0, equality=True)
cv.check_less_than('bin_index', bin_index, self.num_bins)
# Return a 1-tuple of the bin.
return (self.bins[bin_index],)
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Keyword arguments
-----------------
paths : bool
Only used for DistirbcellFilter. If True (default), expand
distribcell indices into multi-index columns describing the path
to that distribcell through the CSG tree. NOTE: This option assumes
that all distribcell paths are of the same length and do not have
the same universes and cells but different lattice cell indices.
Returns
-------
pandas.DataFrame
A Pandas DataFrame with columns of strings that characterize the
filter's bins. The number of rows in the DataFrame is the same as
the total number of bins in the corresponding tally, with the filter
bin appropriately tiled to map to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
filter_bins = np.repeat(self.bins, self.stride)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
df = pd.concat([df, pd.DataFrame(
{self.short_name.lower(): filter_bins})])
return df
class WithIDFilter(Filter):
"""Abstract parent for filters of types with ids (Cell, Material, etc.)."""
@property
def num_bins(self):
return len(self.bins)
# Since num_bins property is declared, also need a num_bins.setter, but
# we don't want it to do anything since num_bins is completely determined
# by len(self.bins). We also don't want to raise an error because that
# makes importing from HDF5 more complicated.
@num_bins.setter
def num_bins(self, num_bins): pass
def _smart_set_bins(self, bins, bin_type):
# Format the bins as a 1D numpy array.
bins = np.atleast_1d(bins)
# Check the bin values.
cv.check_iterable_type('filter bins', bins, (Integral, bin_type))
for edge in bins:
if isinstance(edge, Integral):
cv.check_greater_than('filter bin', edge, 0, equality=True)
# Extract id values.
bins = np.atleast_1d([b if isinstance(b, Integral) else b.id
for b in bins])
self._bins = bins
[docs]class UniverseFilter(WithIDFilter):
"""Bins tally event locations based on the Universe they occured in.
Parameters
----------
bins : openmc.Universe, Integral, or iterable thereof
The Universes to tally. Either openmc.Universe objects or their
Integral ID numbers can be used.
Attributes
----------
bins : Iterable of Integral
openmc.Universe IDs.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@bins.setter
def bins(self, bins):
self._smart_set_bins(bins, openmc.Universe)
[docs]class MaterialFilter(WithIDFilter):
"""Bins tally event locations based on the Material they occured in.
Parameters
----------
bins : openmc.Material, Integral, or iterable thereof
The Materials to tally. Either openmc.Material objects or their
Integral ID numbers can be used.
Attributes
----------
bins : Iterable of Integral
openmc.Material IDs.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@bins.setter
def bins(self, bins):
self._smart_set_bins(bins, openmc.Material)
[docs]class CellFilter(WithIDFilter):
"""Bins tally event locations based on the Cell they occured in.
Parameters
----------
bins : openmc.Cell, Integral, or iterable thereof
The Cells to tally. Either openmc.Cell objects or their
Integral ID numbers can be used.
Attributes
----------
bins : Iterable of Integral
openmc.Cell IDs.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@bins.setter
def bins(self, bins):
self._smart_set_bins(bins, openmc.Cell)
[docs]class CellbornFilter(WithIDFilter):
"""Bins tally events based on which Cell the neutron was born in.
Parameters
----------
bins : openmc.Cell, Integral, or iterable thereof
The birth Cells to tally. Either openmc.Cell objects or their
Integral ID numbers can be used.
Attributes
----------
bins : Iterable of Integral
openmc.Cell IDs.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@bins.setter
def bins(self, bins):
self._smart_set_bins(bins, openmc.Cell)
[docs]class SurfaceFilter(Filter):
"""Bins particle currents on Mesh surfaces.
Parameters
----------
bins : Iterable of Integral
Indices corresponding to which face of a mesh cell the current is
crossing.
Attributes
----------
bins : Iterable of Integral
Indices corresponding to which face of a mesh cell the current is
crossing.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@property
def num_bins(self):
return len(self.bins)
@bins.setter
def bins(self, bins):
# Format the bins as a 1D numpy array.
bins = np.atleast_1d(bins)
# Check the bin values.
cv.check_iterable_type('filter bins', bins, Integral)
for edge in bins:
cv.check_greater_than('filter bin', edge, 0, equality=True)
self._bins = bins
@num_bins.setter
def num_bins(self, num_bins): pass
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with a column of strings describing which surface
the current is crossing and which direction it points. The number
of rows in the DataFrame is the same as the total number of bins in
the corresponding tally, with the filter bin appropriately tiled to
map to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
filter_bins = np.repeat(self.bins, self.stride)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
filter_bins = [_CURRENT_NAMES[x] for x in filter_bins]
df = pd.concat([df, pd.DataFrame(
{self.short_name.lower(): filter_bins})])
return df
[docs]class MeshFilter(Filter):
"""Bins tally event locations onto a regular, rectangular mesh.
Parameters
----------
mesh : openmc.Mesh
The Mesh object that events will be tallied onto
Attributes
----------
bins : Integral
The Mesh ID
mesh : openmc.Mesh
The Mesh object that events will be tallied onto
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
def __init__(self, mesh):
self.mesh = mesh
super(MeshFilter, self).__init__(mesh.id)
@classmethod
[docs] def from_hdf5(cls, group, **kwargs):
if group['type'].value.decode() != cls.short_name.lower():
raise ValueError("Expected HDF5 data for filter type '"
+ cls.short_name.lower() + "' but got '"
+ group['type'].value.decode() + " instead")
if 'meshes' not in kwargs:
raise ValueError(cls.__name__ + " requires a 'meshes' keyword "
"argument.")
mesh_id = group['bins'].value
mesh_obj = kwargs['meshes'][mesh_id]
out = cls(mesh_obj)
out.num_bins = group['n_bins'].value
return out
@property
def mesh(self):
return self._mesh
@mesh.setter
def mesh(self, mesh):
cv.check_type('filter mesh', mesh, openmc.Mesh)
self._mesh = mesh
self.bins = mesh.id
[docs] def check_bins(self, bins):
if not len(bins) == 1:
msg = 'Unable to add bins "{0}" to a MeshFilter since ' \
'only a single mesh can be used per tally'.format(bins)
raise ValueError(msg)
elif not isinstance(bins[0], Integral):
msg = 'Unable to add bin "{0}" to MeshFilter since it ' \
'is a non-integer'.format(bins[0])
raise ValueError(msg)
elif bins[0] < 0:
msg = 'Unable to add bin "{0}" to MeshFilter since it ' \
'is a negative integer'.format(bins[0])
raise ValueError(msg)
[docs] def can_merge(self, other):
# Mesh filters cannot have more than one bin
return False
[docs] def get_bin_index(self, filter_bin):
# Filter bins for a mesh are an (x,y,z) tuple. Convert (x,y,z) to a
# single bin -- this is similar to subroutine mesh_indices_to_bin in
# openmc/src/mesh.F90.
if len(self.mesh.dimension) == 3:
nx, ny, nz = self.mesh.dimension
val = (filter_bin[0] - 1) * ny * nz + \
(filter_bin[1] - 1) * nz + \
(filter_bin[2] - 1)
else:
nx, ny = self.mesh.dimension
val = (filter_bin[0] - 1) * ny + \
(filter_bin[1] - 1)
return val
[docs] def get_bin(self, bin_index):
cv.check_type('bin_index', bin_index, Integral)
cv.check_greater_than('bin_index', bin_index, 0, equality=True)
cv.check_less_than('bin_index', bin_index, self.num_bins)
# Construct 3-tuple of x,y,z cell indices for a 3D mesh
if len(self.mesh.dimension) == 3:
nx, ny, nz = self.mesh.dimension
x = bin_index / (ny * nz)
y = (bin_index - (x * ny * nz)) / nz
z = bin_index - (x * ny * nz) - (y * nz)
return (x, y, z)
# Construct 2-tuple of x,y cell indices for a 2D mesh
else:
nx, ny = self.mesh.dimension
x = bin_index / ny
y = bin_index - (x * ny)
return (x, y)
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with three columns describing the x,y,z mesh
cell indices corresponding to each filter bin. The number of rows
in the DataFrame is the same as the total number of bins in the
corresponding tally, with the filter bin appropriately tiled to map
to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
# Initialize dictionary to build Pandas Multi-index column
filter_dict = {}
# Append Mesh ID as outermost index of multi-index
mesh_key = 'mesh {0}'.format(self.mesh.id)
# Find mesh dimensions - use 3D indices for simplicity
if len(self.mesh.dimension) == 3:
nx, ny, nz = self.mesh.dimension
elif len(self.mesh.dimension) == 2:
nx, ny = self.mesh.dimension
nz = 1
else:
nx = self.mesh.dimension
ny = nz = 1
# Generate multi-index sub-column for x-axis
filter_bins = np.arange(1, nx + 1)
repeat_factor = ny * nz * self.stride
filter_bins = np.repeat(filter_bins, repeat_factor)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
filter_dict[(mesh_key, 'x')] = filter_bins
# Generate multi-index sub-column for y-axis
filter_bins = np.arange(1, ny + 1)
repeat_factor = nz * self.stride
filter_bins = np.repeat(filter_bins, repeat_factor)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
filter_dict[(mesh_key, 'y')] = filter_bins
# Generate multi-index sub-column for z-axis
filter_bins = np.arange(1, nz + 1)
repeat_factor = self.stride
filter_bins = np.repeat(filter_bins, repeat_factor)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
filter_dict[(mesh_key, 'z')] = filter_bins
# Initialize a Pandas DataFrame from the mesh dictionary
df = pd.concat([df, pd.DataFrame(filter_dict)])
return df
class RealFilter(Filter):
"""Tally modifier that describes phase-space and other characteristics
Parameters
----------
bins : Iterable of Real
A grid of bin values.
Attributes
----------
bins : Iterable of Real
A grid of bin values.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
def __gt__(self, other):
if type(self) is type(other):
# Compare largest/smallest bin edges in filters
# This logic is used when merging tallies with real filters
return self.bins[0] >= other.bins[-1]
else:
return super(RealFilter, self).__gt__(other)
@property
def num_bins(self):
return len(self.bins) - 1
@num_bins.setter
def num_bins(self, num_bins):
cv.check_type('filter num_bins', num_bins, Integral)
cv.check_greater_than('filter num_bins', num_bins, 0, equality=True)
self._num_bins = num_bins
def can_merge(self, other):
if type(self) is not type(other):
return False
if self.bins[0] == other.bins[-1]:
# This low edge coincides with other's high edge
return True
elif self.bins[-1] == other.bins[0]:
# This high edge coincides with other's low edge
return True
else:
return False
def merge(self, other):
if not self.can_merge(other):
msg = 'Unable to merge "{0}" with "{1}" ' \
'filters'.format(type(self), type(other))
raise ValueError(msg)
# Merge unique filter bins
merged_bins = np.concatenate((self.bins, other.bins))
merged_bins = np.unique(merged_bins)
# Create a new filter with these bins
return type(self)(sorted(merged_bins))
def is_subset(self, other):
"""Determine if another filter is a subset of this filter.
If all of the bins in the other filter are included as bins in this
filter, then it is a subset of this filter.
Parameters
----------
other : openmc.Filter
The filter to query as a subset of this filter
Returns
-------
bool
Whether or not the other filter is a subset of this filter
"""
if type(self) is not type(other):
return False
elif len(self.bins) != len(other.bins):
return False
else:
return np.allclose(self.bins, other.bins)
def get_bin_index(self, filter_bin):
i = np.where(self.bins == filter_bin[1])[0]
if len(i) == 0:
msg = 'Unable to get the bin index for Filter since "{0}" ' \
'is not one of the bins'.format(filter_bin)
raise ValueError(msg)
else:
return i[0] - 1
def get_bin(self, bin_index):
cv.check_type('bin_index', bin_index, Integral)
cv.check_greater_than('bin_index', bin_index, 0, equality=True)
cv.check_less_than('bin_index', bin_index, self.num_bins)
# Construct 2-tuple of lower, upper bins for real-valued filters
return (self.bins[bin_index], self.bins[bin_index + 1])
[docs]class EnergyFilter(RealFilter):
"""Bins tally events based on incident particle energy.
Parameters
----------
bins : Iterable of Real
A grid of energy values in eV.
Attributes
----------
bins : Iterable of Real
A grid of energy values in eV.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
[docs] def get_bin_index(self, filter_bin):
# Use lower energy bound to find index for RealFilters
deltas = np.abs(self.bins - filter_bin[1]) / filter_bin[1]
min_delta = np.min(deltas)
if min_delta < 1E-3:
return deltas.argmin() - 1
else:
msg = 'Unable to get the bin index for Filter since "{0}" ' \
'is not one of the bins'.format(filter_bin)
raise ValueError(msg)
[docs] def check_bins(self, bins):
for edge in bins:
if not isinstance(edge, Real):
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is a non-integer or floating point ' \
'value'.format(edge, type(self))
raise ValueError(msg)
elif edge < 0.:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is a negative value'.format(edge, type(self))
raise ValueError(msg)
# Check that bin edges are monotonically increasing
for index in range(1, len(bins)):
if bins[index] < bins[index-1]:
msg = 'Unable to add bin edges "{0}" to a "{1}" Filter ' \
'since they are not monotonically ' \
'increasing'.format(bins, type(self))
raise ValueError(msg)
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with one column of the lower energy bound and one
column of upper energy bound for each filter bin. The number of
rows in the DataFrame is the same as the total number of bins in the
corresponding tally, with the filter bin appropriately tiled to map
to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
# Extract the lower and upper energy bounds, then repeat and tile
# them as necessary to account for other filters.
lo_bins = np.repeat(self.bins[:-1], self.stride)
hi_bins = np.repeat(self.bins[1:], self.stride)
tile_factor = data_size // len(lo_bins)
lo_bins = np.tile(lo_bins, tile_factor)
hi_bins = np.tile(hi_bins, tile_factor)
# Add the new energy columns to the DataFrame.
df.loc[:, self.short_name.lower() + ' low [eV]'] = lo_bins
df.loc[:, self.short_name.lower() + ' high [eV]'] = hi_bins
return df
[docs]class EnergyoutFilter(EnergyFilter):
"""Bins tally events based on outgoing particle energy.
Parameters
----------
bins : Iterable of Real
A grid of energy values in eV.
Attributes
----------
bins : Iterable of Real
A grid of energy values in eV.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
def _path_to_levels(path):
"""Convert distribcell path to list of levels
Parameters
----------
path : str
Distribcell path
Returns
-------
list
List of levels in path
"""
# Split path into universes/cells/lattices
path_items = path.split('->')
# Pair together universe and cell information from the same level
idx = [i for i, item in enumerate(path_items) if item.startswith('u')]
for i in reversed(idx):
univ_id = int(path_items.pop(i)[1:])
cell_id = int(path_items.pop(i)[1:])
path_items.insert(i, ('universe', univ_id, cell_id))
# Reformat lattice into tuple
idx = [i for i, item in enumerate(path_items) if isinstance(item, str)]
for i in idx:
item = path_items.pop(i)[1:-1]
lat_id, lat_xyz = item.split('(')
lat_id = int(lat_id)
lat_xyz = tuple(int(x) for x in lat_xyz.split(','))
path_items.insert(i, ('lattice', lat_id, lat_xyz))
return path_items
[docs]class DistribcellFilter(Filter):
"""Bins tally event locations on instances of repeated cells.
Parameters
----------
cell : openmc.Cell or Integral
The distributed cell to tally. Either an openmc.Cell or an Integral
cell ID number can be used.
Attributes
----------
bins : Iterable of Integral
An iterable with one element---the ID of the distributed Cell.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
paths : list of str
The paths traversed through the CSG tree to reach each distribcell
instance (for 'distribcell' filters only)
"""
def __init__(self, cell):
self._paths = None
super(DistribcellFilter, self).__init__(cell)
@classmethod
[docs] def from_hdf5(cls, group, **kwargs):
if group['type'].value.decode() != cls.short_name.lower():
raise ValueError("Expected HDF5 data for filter type '"
+ cls.short_name.lower() + "' but got '"
+ group['type'].value.decode() + " instead")
out = cls(group['bins'].value)
out.num_bins = group['n_bins'].value
return out
@property
def bins(self):
return self._bins
@property
def paths(self):
return self._paths
@bins.setter
def bins(self, bins):
# Format the bins as a 1D numpy array.
bins = np.atleast_1d(bins)
# Make sure there is only 1 bin.
if not len(bins) == 1:
msg = 'Unable to add bins "{0}" to a DistribcellFilter since ' \
'only a single distribcell can be used per tally'.format(bins)
raise ValueError(msg)
# Check the type and extract the id, if necessary.
cv.check_type('distribcell bin', bins[0], (Integral, openmc.Cell))
if isinstance(bins[0], openmc.Cell):
bins = np.atleast_1d(bins[0].id)
self._bins = bins
@paths.setter
def paths(self, paths):
cv.check_iterable_type('paths', paths, str)
self._paths = paths
[docs] def can_merge(self, other):
# Distribcell filters cannot have more than one bin
return False
[docs] def get_bin_index(self, filter_bin):
# Filter bins for distribcells are indices of each unique placement of
# the Cell in the Geometry (consecutive integers starting at 0).
return filter_bin
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Keyword arguments
-----------------
paths : bool
If True (default), expand distribcell indices into multi-index
columns describing the path to that distribcell through the CSG
tree. NOTE: This option assumes that all distribcell paths are of
the same length and do not have the same universes and cells but
different lattice cell indices.
Returns
-------
pandas.DataFrame
A Pandas DataFrame with columns describing distributed cells. The
for will be either:
1. a single column with the cell instance IDs (without summary info)
2. separate columns for the cell IDs, universe IDs, and lattice IDs
and x,y,z cell indices corresponding to each (distribcell paths).
The number of rows in the DataFrame is the same as the total number
of bins in the corresponding tally, with the filter bin
appropriately tiled to map to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
level_df = None
paths = kwargs.setdefault('paths', True)
# Create Pandas Multi-index columns for each level in CSG tree
if paths:
# Distribcell paths require linked metadata from the Summary
if self.paths is None:
msg = 'Unable to construct distribcell paths since ' \
'the Summary is not linked to the StatePoint'
raise ValueError(msg)
# Make copy of array of distribcell paths to use in
# Pandas Multi-index column construction
num_offsets = len(self.paths)
paths = [_path_to_levels(p) for p in self.paths]
# Loop over CSG levels in the distribcell paths
num_levels = len(paths[0])
for i_level in range(num_levels):
# Use level key as first index in Pandas Multi-index column
level_key = 'level {}'.format(i_level + 1)
# Create a dictionary for this level for Pandas Multi-index
level_dict = OrderedDict()
# Use the first distribcell path to determine if level
# is a universe/cell or lattice level
path = paths[0]
if path[i_level][0] == 'lattice':
# Initialize prefix Multi-index keys
lat_id_key = (level_key, 'lat', 'id')
lat_x_key = (level_key, 'lat', 'x')
lat_y_key = (level_key, 'lat', 'y')
lat_z_key = (level_key, 'lat', 'z')
# Allocate NumPy arrays for each CSG level and
# each Multi-index column in the DataFrame
level_dict[lat_id_key] = np.empty(num_offsets)
level_dict[lat_x_key] = np.empty(num_offsets)
level_dict[lat_y_key] = np.empty(num_offsets)
if len(path[i_level][2]) == 3:
level_dict[lat_z_key] = np.empty(num_offsets)
else:
# Initialize prefix Multi-index keys
univ_key = (level_key, 'univ', 'id')
cell_key = (level_key, 'cell', 'id')
# Allocate NumPy arrays for each CSG level and
# each Multi-index column in the DataFrame
level_dict[univ_key] = np.empty(num_offsets)
level_dict[cell_key] = np.empty(num_offsets)
# Populate Multi-index arrays with all distribcell paths
for i, path in enumerate(paths):
level = path[i_level]
if level[0] == 'lattice':
# Assign entry to Lattice Multi-index column
level_dict[lat_id_key][i] = level[1]
level_dict[lat_x_key][i] = level[2][0]
level_dict[lat_y_key][i] = level[2][1]
if len(level[2]) == 3:
level_dict[lat_z_key][i] = level[2][2]
else:
# Assign entry to Universe, Cell Multi-index columns
level_dict[univ_key][i] = level[1]
level_dict[cell_key][i] = level[2]
# Tile the Multi-index columns
for level_key, level_bins in level_dict.items():
level_bins = np.repeat(level_bins, self.stride)
tile_factor = data_size // len(level_bins)
level_bins = np.tile(level_bins, tile_factor)
level_dict[level_key] = level_bins
# Initialize a Pandas DataFrame from the level dictionary
if level_df is None:
level_df = pd.DataFrame(level_dict)
else:
level_df = pd.concat([level_df, pd.DataFrame(level_dict)],
axis=1)
# Create DataFrame column for distribcell instance IDs
# NOTE: This is performed regardless of whether the user
# requests Summary geometric information
filter_bins = np.arange(self.num_bins)
filter_bins = np.repeat(filter_bins, self.stride)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
df = pd.DataFrame({self.short_name.lower() : filter_bins})
# Concatenate with DataFrame of distribcell instance IDs
if level_df is not None:
level_df = level_df.dropna(axis=1, how='all')
level_df = level_df.astype(np.int)
df = pd.concat([level_df, df], axis=1)
return df
[docs]class MuFilter(RealFilter):
"""Bins tally events based on particle scattering angle.
Parameters
----------
bins : Iterable of Real or Integral
A grid of scattering angles which events will binned into. Values
represent the cosine of the scattering angle. If an Iterable is given,
the values will be used explicitly as grid points. If a single Integral
is given, the range [-1, 1] will be divided up equally into that number
of bins.
Attributes
----------
bins : Integral
A grid of scattering angles which events will binned into. Values
represent the cosine of the scattering angle. If an Iterable is given,
the values will be used explicitly as grid points. If a single Integral
is given, the range [-1, 1] will be divided up equally into that number
of bins.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
[docs] def check_bins(self, bins):
for edge in bins:
if not isinstance(edge, Real):
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is a non-integer or floating point ' \
'value'.format(edge, type(self))
raise ValueError(msg)
elif edge < -1.:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is less than -1'.format(edge, type(self))
raise ValueError(msg)
elif edge > 1.:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is greater than 1'.format(edge, type(self))
raise ValueError(msg)
# Check that bin edges are monotonically increasing
for index in range(1, len(bins)):
if bins[index] < bins[index-1]:
msg = 'Unable to add bin edges "{0}" to a "{1}" Filter ' \
'since they are not monotonically ' \
'increasing'.format(bins, type(self))
raise ValueError(msg)
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method
for :meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with one column of the lower energy bound and one
column of upper energy bound for each filter bin. The number of
rows in the DataFrame is the same as the total number of bins in the
corresponding tally, with the filter bin appropriately tiled to map
to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
# Extract the lower and upper energy bounds, then repeat and tile
# them as necessary to account for other filters.
lo_bins = np.repeat(self.bins[:-1], self.stride)
hi_bins = np.repeat(self.bins[1:], self.stride)
tile_factor = data_size // len(lo_bins)
lo_bins = np.tile(lo_bins, tile_factor)
hi_bins = np.tile(hi_bins, tile_factor)
# Add the new energy columns to the DataFrame.
df.loc[:, self.short_name.lower() + ' low'] = lo_bins
df.loc[:, self.short_name.lower() + ' high'] = hi_bins
return df
[docs]class PolarFilter(RealFilter):
"""Bins tally events based on the incident particle's direction.
Parameters
----------
bins : Iterable of Real or Integral
A grid of polar angles which events will binned into. Values represent
an angle in radians relative to the z-axis. If an Iterable is given,
the values will be used explicitly as grid points. If a single Integral
is given, the range [0, pi] will be divided up equally into that number
of bins.
Attributes
----------
bins : Iterable of Real or Integral
A grid of polar angles which events will binned into. Values represent
an angle in radians relative to the z-axis. If an Iterable is given,
the values will be used explicitly as grid points. If a single Integral
is given, the range [0, pi] will be divided up equally into that number
of bins.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
[docs] def check_bins(self, bins):
for edge in bins:
if not isinstance(edge, Real):
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is a non-integer or floating point ' \
'value'.format(edge, type(self))
raise ValueError(msg)
elif edge < 0.:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is less than 0'.format(edge, type(self))
raise ValueError(msg)
elif not np.isclose(edge, np.pi) and edge > np.pi:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is greater than pi'.format(edge, type(self))
raise ValueError(msg)
# Check that bin edges are monotonically increasing
for index in range(1, len(bins)):
if bins[index] < bins[index-1]:
msg = 'Unable to add bin edges "{0}" to a "{1}" Filter ' \
'since they are not monotonically ' \
'increasing'.format(bins, type(self))
raise ValueError(msg)
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with a column corresponding to the lower polar
angle bound for each of the filter's bins. The number of rows in
the DataFrame is the same as the total number of bins in the
corresponding tally, with the filter bin appropriately tiled to map
to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
# Extract the lower and upper angle bounds, then repeat and tile
# them as necessary to account for other filters.
lo_bins = np.repeat(self.bins[:-1], self.stride)
hi_bins = np.repeat(self.bins[1:], self.stride)
tile_factor = data_size // len(lo_bins)
lo_bins = np.tile(lo_bins, tile_factor)
hi_bins = np.tile(hi_bins, tile_factor)
# Add the new angle columns to the DataFrame.
df.loc[:, 'polar low'] = lo_bins
df.loc[:, 'polar high'] = hi_bins
return df
[docs]class AzimuthalFilter(RealFilter):
"""Bins tally events based on the incident particle's direction.
Parameters
----------
bins : Iterable of Real or Integral
A grid of azimuthal angles which events will binned into. Values
represent an angle in radians relative to the x-axis and perpendicular
to the z-axis. If an Iterable is given, the values will be used
explicitly as grid points. If a single Integral is given, the range
[-pi, pi) will be divided up equally into that number of bins.
Attributes
----------
bins : Iterable of Real or Integral
A grid of azimuthal angles which events will binned into. Values
represent an angle in radians relative to the x-axis and perpendicular
to the z-axis. If an Iterable is given, the values will be used
explicitly as grid points. If a single Integral is given, the range
[-pi, pi) will be divided up equally into that number of bins.
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
[docs] def check_bins(self, bins):
for edge in bins:
if not isinstance(edge, Real):
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is a non-integer or floating point ' \
'value'.format(edge, type(self))
raise ValueError(msg)
elif not np.isclose(edge, -np.pi) and edge < -np.pi:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is less than -pi'.format(edge, type(self))
raise ValueError(msg)
elif not np.isclose(edge, np.pi) and edge > np.pi:
msg = 'Unable to add bin edge "{0}" to a "{1}" ' \
'since it is greater than pi'.format(edge, type(self))
raise ValueError(msg)
# Check that bin edges are monotonically increasing
for index in range(1, len(bins)):
if bins[index] < bins[index-1]:
msg = 'Unable to add bin edges "{0}" to a "{1}" Filter ' \
'since they are not monotonically ' \
'increasing'.format(bins, type(self))
raise ValueError(msg)
[docs] def get_pandas_dataframe(self, data_size, paths=True):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with a column corresponding to the lower
azimuthal angle bound for each of the filter's bins. The number of
rows in the DataFrame is the same as the total number of bins in the
corresponding tally, with the filter bin appropriately tiled to map
to the corresponding tally bins.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Initialize Pandas DataFrame
import pandas as pd
df = pd.DataFrame()
# Extract the lower and upper angle bounds, then repeat and tile
# them as necessary to account for other filters.
lo_bins = np.repeat(self.bins[:-1], self.stride)
hi_bins = np.repeat(self.bins[1:], self.stride)
tile_factor = data_size // len(lo_bins)
lo_bins = np.tile(lo_bins, tile_factor)
hi_bins = np.tile(hi_bins, tile_factor)
# Add the new angle columns to the DataFrame.
df.loc[:, 'azimuthal low'] = lo_bins
df.loc[:, 'azimuthal high'] = hi_bins
return df
[docs]class DelayedGroupFilter(Filter):
"""Bins fission events based on the produced neutron precursor groups.
Parameters
----------
bins : Integral or Iterable of Integral
The delayed neutron precursor groups. For example, ENDF/B-VII.1 uses
6 precursor groups so a tally with all groups will have bins =
[1, 2, 3, 4, 5, 6].
Attributes
----------
bins : Integral or Iterable of Integral
The delayed neutron precursor groups. For example, ENDF/B-VII.1 uses
6 precursor groups so a tally with all groups will have bins =
[1, 2, 3, 4, 5, 6].
num_bins : Integral
The number of filter bins
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
@property
def bins(self):
return self._bins
@property
def num_bins(self):
return len(self.bins)
@bins.setter
def bins(self, bins):
# Format the bins as a 1D numpy array.
bins = np.atleast_1d(bins)
# Check the bin values.
cv.check_iterable_type('filter bins', bins, Integral)
for edge in bins:
cv.check_greater_than('filter bin', edge, 0, equality=True)
self._bins = bins
@num_bins.setter
def num_bins(self, num_bins): pass
[docs]class EnergyFunctionFilter(Filter):
"""Multiplies tally scores by an arbitrary function of incident energy.
The arbitrary function is described by a piecewise linear-linear
interpolation of energy and y values. Values outside of the given energy
range will be evaluated as zero.
Parameters
----------
energy : Iterable of Real
A grid of energy values in eV.
y : iterable of Real
A grid of interpolant values in eV.
Attributes
----------
energy : Iterable of Real
A grid of energy values in eV.
y : iterable of Real
A grid of interpolant values in eV.
num_bins : Integral
The number of filter bins (always 1 for this filter)
stride : Integral
The number of filter, nuclide and score bins within each of this
filter's bins.
"""
def __init__(self, energy, y):
self.energy = energy
self.y = y
self._stride = None
def __eq__(self, other):
if type(self) is not type(other):
return False
elif not all(self.energy == other.energy):
return False
elif not all(self.y == other.y):
return False
else:
return True
def __gt__(self, other):
if type(self) is not type(other):
if self.short_name in _FILTER_TYPES and \
other.short_name in _FILTER_TYPES:
delta = _FILTER_TYPES.index(self.short_name) - \
_FILTER_TYPES.index(other.short_name)
return delta > 0
else:
return False
else:
return False
def __lt__(self, other):
if type(self) is not type(other):
if self.short_name in _FILTER_TYPES and \
other.short_name in _FILTER_TYPES:
delta = _FILTER_TYPES.index(self.short_name) - \
_FILTER_TYPES.index(other.short_name)
return delta < 0
else:
return False
else:
return False
def __hash__(self):
# For some reason, it seems the __hash__ method is not inherited when we
# overwrite __repr__.
return hash(repr(self))
def __repr__(self):
string = type(self).__name__ + '\n'
string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy)
string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y)
return string
@classmethod
[docs] def from_hdf5(cls, group, **kwargs):
if group['type'].value.decode() != cls.short_name.lower():
raise ValueError("Expected HDF5 data for filter type '"
+ cls.short_name.lower() + "' but got '"
+ group['type'].value.decode() + " instead")
energy = group['energy'].value
y = group['y'].value
return cls(energy, y)
@classmethod
[docs] def from_tabulated1d(cls, tab1d):
"""Construct a filter from a Tabulated1D object.
Parameters
----------
tab1d : openmc.data.Tabulated1D
A linear-linear Tabulated1D object with only a single interpolation
region.
Returns
-------
EnergyFunctionFilter
"""
cv.check_type('EnergyFunctionFilter tab1d', tab1d,
openmc.data.Tabulated1D)
if tab1d.n_regions > 1:
raise ValueError('Only Tabulated1Ds with a single interpolation '
'region are supported')
if tab1d.interpolation[0] != 2:
raise ValueError('Only linear-linar Tabulated1Ds are supported')
return cls(tab1d.x, tab1d.y)
@property
def energy(self):
return self._energy
@property
def y(self):
return self._y
@property
def bins(self):
raise RuntimeError('EnergyFunctionFilters have no bins.')
@property
def num_bins(self):
return 1
@energy.setter
def energy(self, energy):
# Format the bins as a 1D numpy array.
energy = np.atleast_1d(energy)
# Make sure the values are Real and positive.
cv.check_type('filter energy grid', energy, Iterable, Real)
for E in energy:
cv.check_greater_than('filter energy grid', E, 0, equality=True)
self._energy = energy
@y.setter
def y(self, y):
# Format the bins as a 1D numpy array.
y = np.atleast_1d(y)
# Make sure the values are Real.
cv.check_type('filter interpolant values', y, Iterable, Real)
self._y = y
@bins.setter
def bins(self, bins):
raise RuntimeError('EnergyFunctionFilters have no bins.')
[docs] def to_xml_element(self):
element = ET.Element('filter')
element.set('type', self.short_name.lower())
element.set('energy', ' '.join(str(e) for e in self.energy))
element.set('y', ' '.join(str(y) for y in self.y))
return element
[docs] def can_merge(self, other):
return False
[docs] def is_subset(self, other):
return self == other
[docs] def get_bin_index(self, filter_bin):
# This filter only has one bin. Always return 0.
return 0
[docs] def get_bin(self, bin_index):
"""This function is invalid for EnergyFunctionFilters."""
raise RuntimeError('EnergyFunctionFilters have no get_bin() method')
[docs] def get_pandas_dataframe(self, data_size, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : Integral
The total number of bins in the tally corresponding to this filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with a column that is filled with a hash of this
filter. EnergyFunctionFilters have only 1 bin so the purpose of this
DataFrame column is to differentiate the filter from other
EnergyFunctionFilters. The number of rows in the DataFrame is the
same as the total number of bins in the corresponding tally.
Raises
------
ImportError
When Pandas is not installed
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
import pandas as pd
df = pd.DataFrame()
# There is no clean way of sticking all the energy, y data into a
# DataFrame so instead we'll just make a column with the filter name
# and fill it with a hash of the __repr__. We want a hash that is
# reproducible after restarting the interpreter so we'll use hashlib.md5
# rather than the intrinsic hash().
hash_fun = hashlib.md5()
hash_fun.update(repr(self).encode('utf-8'))
out = hash_fun.hexdigest()
# The full 16 bytes make for a really wide column. Just 7 bytes (14
# hex characters) of the digest are probably sufficient.
out = out[:14]
filter_bins = np.repeat(out, self.stride)
tile_factor = data_size // len(filter_bins)
filter_bins = np.tile(filter_bins, tile_factor)
df = pd.concat([df, pd.DataFrame(
{self.short_name.lower(): filter_bins})])
return df