diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 2f80ee4c0c..3b334319f6 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -196,7 +196,7 @@ def check_less_than(name, value, maximum, equality=False): maximum : object Maximum value to check against equality : bool, optional - Whether equality is allowed. Defaluts to False. + Whether equality is allowed. Defaults to False. """ @@ -223,7 +223,7 @@ def check_greater_than(name, value, minimum, equality=False): minimum : object Minimum value to check against equality : bool, optional - Whether equality is allowed. Defaluts to False. + Whether equality is allowed. Defaults to False. """ diff --git a/openmc/filter.py b/openmc/filter.py index c1b9f8dc92..d59d739a39 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -3,6 +3,7 @@ from collections import Iterable, OrderedDict import copy from functools import reduce import hashlib +from itertools import product from numbers import Real, Integral import operator from xml.etree import ElementTree as ET @@ -192,9 +193,6 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): @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) @@ -221,8 +219,6 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): XML element containing filter data """ - - element = ET.Element('filter') element.set('id', str(self.id)) element.set('type', self.short_name.lower()) @@ -332,7 +328,10 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): 'is not one of the bins'.format(filter_bin) raise ValueError(msg) - return np.where(self.bins == filter_bin)[0][0] + if isinstance(self.bins, np.ndarray): + return np.where(self.bins == filter_bin)[0][0] + else: + return self.bins.index(filter_bin) def get_bin(self, bin_index): """Returns the filter bin for some filter bin index. @@ -423,25 +422,24 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): class WithIDFilter(Filter): - """Abstract parent for filters of types with ids (Cell, Material, etc.).""" - - @Filter.bins.setter - def bins(self, bins): - # Format the bins as a 1D numpy array. + """Abstract parent for filters of types with IDs (Cell, Material, etc.).""" + def __init__(self, bins, filter_id=None): bins = np.atleast_1d(bins) - # Check the bin values. + # Make sure bins are either integers or appropriate objects cv.check_iterable_type('filter bins', bins, (Integral, self.expected_type)) + + # Extract ID values + bins = np.array([b if isinstance(b, Integral) else b.id + for b in bins]) + self.bins = bins + self.id = filter_id + + def check_bins(self, bins): + # Check the bin values. 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 + cv.check_greater_than('filter bin', edge, 0, equality=True) class UniverseFilter(WithIDFilter): @@ -601,12 +599,13 @@ class MeshFilter(Filter): Attributes ---------- - bins : Integral - The Mesh ID mesh : openmc.Mesh The Mesh object that events will be tallied onto id : int Unique identifier for the filter + bins : list of tuple + A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), + ...] num_bins : Integral The number of filter bins @@ -614,7 +613,7 @@ class MeshFilter(Filter): def __init__(self, mesh, filter_id=None): self.mesh = mesh - super().__init__(mesh.id, filter_id) + self.id = filter_id @classmethod def from_hdf5(cls, group, **kwargs): @@ -643,68 +642,14 @@ class MeshFilter(Filter): def mesh(self, mesh): cv.check_type('filter mesh', mesh, openmc.Mesh) self._mesh = mesh - self.bins = mesh.id - - @property - def num_bins(self): - return reduce(operator.mul, self.mesh.dimension) - - 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) + self.bins = list(mesh.indices) def can_merge(self, other): # Mesh filters cannot have more than one bin return False - 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. - n_dim = len(self.mesh.dimension) - if n_dim == 3: - i, j, k = filter_bin - nx, ny, nz = self.mesh.dimension - return (i - 1) + (j - 1)*nx + (k - 1)*nx*ny - elif n_dim == 2: - i, j, *_ = filter_bin - nx, ny = self.mesh.dimension - return (i - 1) + (j - 1)*nx - else: - return filter_bin[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) - - n_dim = len(self.mesh.dimension) - if n_dim == 3: - # Construct 3-tuple of x,y,z cell indices for a 3D mesh - nx, ny, nz = self.mesh.dimension - x = (bin_index % nx) + 1 - y = (bin_index % (nx * ny)) // nx + 1 - z = bin_index // (nx * ny) + 1 - return (x, y, z) - - elif n_dim == 2: - # Construct 2-tuple of x,y cell indices for a 2D mesh - nx, ny = self.mesh.dimension - x = (bin_index % nx) + 1 - y = bin_index // nx + 1 - return (x, y) - else: - return (bin_index + 1,) + return self.bins[bin_index] def get_pandas_dataframe(self, data_size, stride, **kwargs): """Builds a Pandas DataFrame for the Filter's bins. @@ -783,6 +728,19 @@ class MeshFilter(Filter): return df + def to_xml_element(self): + """Return XML Element representing the Filter. + + Returns + ------- + element : xml.etree.ElementTree.Element + XML element containing filter data + + """ + element = super().to_xml_element() + element[0].text = str(self.mesh.id) + return element + class MeshSurfaceFilter(MeshFilter): """Filter events by surface crossings on a regular, rectangular mesh. @@ -802,36 +760,25 @@ class MeshSurfaceFilter(MeshFilter): The Mesh object that events will be tallied onto id : int Unique identifier for the filter + bins : list of tuple + + A list of mesh indices / surfaces for each filter bin, e.g. [(1, 1, + 'x-min out'), (1, 1, 'x-min in'), ...] + num_bins : Integral The number of filter bins """ - @property - def num_bins(self): - n_dim = len(self.mesh.dimension) - return 4*n_dim*reduce(operator.mul, self.mesh.dimension) + @MeshFilter.mesh.setter + def mesh(self, mesh): + cv.check_type('filter mesh', mesh, openmc.Mesh) + self._mesh = mesh - def get_bin_index(self, filter_bin): - # Split bin into mesh/surface parts - *mesh_tuple, surf = filter_bin - - # Get index for mesh alone - mesh_index = super().get_bin_index(mesh_tuple) - - # Combine surface and mesh index - n_dim = len(self.mesh.dimension) - for surf_index, name in enumerate(_CURRENT_NAMES): - if surf == name: - return 4*n_dim*mesh_index + surf_index - else: - raise ValueError("'{}' is not a valid mesh surface.".format(surf)) - - def get_bin(self, bin_index): - n_dim = len(self.mesh.dimension) - mesh_index, surf_index = divmod(bin_index, 4*n_dim) - mesh_bin = super().get_bin(mesh_index) - return mesh_bin + (_CURRENT_NAMES[surf_index],) + # Take the product of mesh indices and current names + n_dim = len(mesh.dimension) + self.bins = [mesh_tuple + (surf,) for mesh_tuple, surf in + product(mesh.indices, _CURRENT_NAMES[:4*n_dim])] def get_pandas_dataframe(self, data_size, stride, **kwargs): """Builds a Pandas DataFrame for the Filter's bins. @@ -920,42 +867,68 @@ class RealFilter(Filter): Parameters ---------- - bins : Iterable of Real - A grid of bin values. + values : iterable of float + A list of values for which each successive pair constitutes a range of + values for a single bin filter_id : int Unique identifier for the filter Attributes ---------- - bins : Iterable of Real - A grid of bin values. + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + values for a single bin id : int Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of values indicating a + filter bin range num_bins : int The number of filter bins """ + def __init__(self, values, filter_id=None): + self.values = np.asarray(values) + self.bins = np.vstack((self.values[:-1], self.values[1:])).T + self.id = filter_id 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] + return self.values[0] >= other.values[-1] else: return super().__gt__(other) - @property - def num_bins(self): - return len(self.bins) - 1 + @Filter.bins.setter + def bins(self, bins): + Filter.bins.__set__(self, np.asarray(bins)) + + def check_bins(self, bins): + for v0, v1 in bins: + # Values should be real + cv.check_type('filter value', v0, Real) + cv.check_type('filter value', v1, Real) + + # Make sure that each tuple has values that are increasing + if v1 < v0: + raise ValueError('Values {} and {} appear to be out of order' + .format(v0, v1)) + + for pair0, pair1 in zip(bins[:-1], bins[1:]): + # Successive pairs should be ordered + if pair1[1] < pair0[1]: + raise ValueError('Values {} and {} appear to be out of order' + .format(pair1[1], pair0[1])) def can_merge(self, other): if type(self) is not type(other): return False - if self.bins[0] == other.bins[-1]: + if self.bins[0, 0] == other.bins[-1][1]: # This low edge coincides with other's high edge return True - elif self.bins[-1] == other.bins[0]: + elif self.bins[-1][1] == other.bins[0, 0]: # This high edge coincides with other's low edge return True else: @@ -968,11 +941,11 @@ class RealFilter(Filter): raise ValueError(msg) # Merge unique filter bins - merged_bins = np.concatenate((self.bins, other.bins)) - merged_bins = np.unique(merged_bins) + merged_values = np.concatenate((self.values, other.values)) + merged_values = np.unique(merged_values) # Create a new filter with these bins and a new auto-generated ID - return type(self)(sorted(merged_bins)) + return type(self)(sorted(merged_values)) def is_subset(self, other): """Determine if another filter is a subset of this filter. @@ -994,80 +967,19 @@ class RealFilter(Filter): if type(self) is not type(other): return False - elif len(self.bins) != len(other.bins): + elif self.num_bins != other.num_bins: return False else: - return np.allclose(self.bins, other.bins) + return np.allclose(self.values, other.values) def get_bin_index(self, filter_bin): - i = np.where(self.bins == filter_bin[1])[0] + i = np.where(self.bins[:, 1] == 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]) - - -class EnergyFilter(RealFilter): - """Bins tally events based on incident particle energy. - - Parameters - ---------- - bins : Iterable of Real - A grid of energy values in eV. - filter_id : int - Unique identifier for the filter - - Attributes - ---------- - bins : Iterable of Real - A grid of energy values in eV. - id : int - Unique identifier for the filter - num_bins : int - The number of filter bins - - """ - - 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) - - 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) + return i[0] def get_pandas_dataframe(self, data_size, stride, **kwargs): """Builds a Pandas DataFrame for the Filter's bins. @@ -1102,35 +1014,103 @@ class EnergyFilter(RealFilter): # 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], stride) - hi_bins = np.repeat(self.bins[1:], stride) + lo_bins = np.repeat(self.bins[:, 0], stride) + hi_bins = np.repeat(self.bins[:, 1], 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 + if hasattr(self, 'units'): + units = ' [{}]'.format(self.units) + else: + units = '' + + df.loc[:, self.short_name.lower() + ' low' + units] = lo_bins + df.loc[:, self.short_name.lower() + ' high' + units] = hi_bins return df + def to_xml_element(self): + """Return XML Element representing the Filter. + + Returns + ------- + element : xml.etree.ElementTree.Element + XML element containing filter data + + """ + element = super().to_xml_element() + element[0].text = ' '.join(str(x) for x in self.values) + return element + + +class EnergyFilter(RealFilter): + """Bins tally events based on incident particle energy. + + Parameters + ---------- + values : Iterable of Real + A list of values for which each successive pair constitutes a range of + energies in [eV] for a single bin + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + energies in [eV] for a single bin + id : int + Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of energies in [eV] + for a single filter bin + num_bins : int + The number of filter bins + + """ + units = 'eV' + + def get_bin_index(self, filter_bin): + # Use lower energy bound to find index for RealFilters + deltas = np.abs(self.bins[:, 1] - filter_bin[1]) / filter_bin[1] + min_delta = np.min(deltas) + if min_delta < 1E-3: + return deltas.argmin() + else: + msg = 'Unable to get the bin index for Filter since "{0}" ' \ + 'is not one of the bins'.format(filter_bin) + raise ValueError(msg) + + def check_bins(self, bins): + super().check_bins(bins) + for v0, v1 in bins: + cv.check_greater_than('filter value', v0, 0., equality=True) + cv.check_greater_than('filter value', v1, 0., equality=True) + class EnergyoutFilter(EnergyFilter): """Bins tally events based on outgoing particle energy. Parameters ---------- - bins : Iterable of Real - A grid of energy values in eV. + values : Iterable of Real + A list of values for which each successive pair constitutes a range of + energies in [eV] for a single bin filter_id : int Unique identifier for the filter Attributes ---------- - bins : Iterable of Real - A grid of energy values in eV. + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + energies in [eV] for a single bin id : int Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of energies in [eV] + for a single filter bin num_bins : int The number of filter bins @@ -1412,98 +1392,41 @@ class MuFilter(RealFilter): 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. + values : int or Iterable of Real + 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 int is + given, the range [-1, 1] will be divided up equally into that number of + bins. filter_id : int Unique identifier for the filter 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. + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + scattering angle cosines for a single bin id : int Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of scattering angle + cosines for a single filter bin num_bins : Integral The number of filter bins """ + def __init__(self, values, filter_id=None): + if isinstance(values, Integral): + values = np.linspace(-1., 1., values + 1) + super().__init__(values, filter_id) 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) - - def get_pandas_dataframe(self, data_size, stride, **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 : int - The total number of bins in the tally corresponding to this filter - stride : int - Stride in memory for the 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. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - 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], stride) - hi_bins = np.repeat(self.bins[1:], 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 + super().check_bins(bins) + for x in np.ravel(bins): + if not np.isclose(x, -1.): + cv.check_greater_than('filter value', x, -1., equality=True) + if not np.isclose(x, 1.): + cv.check_less_than('filter value', x, 1., equality=True) class PolarFilter(RealFilter): @@ -1511,98 +1434,44 @@ class PolarFilter(RealFilter): 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. + values : int or Iterable of Real + 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 int is given, + the range [0, pi] will be divided up equally into that number of bins. filter_id : int Unique identifier for the filter 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. + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + polar angles in [rad] for a single bin + id : int + Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of polar angles for a + single filter bin id : int Unique identifier for the filter num_bins : Integral The number of filter bins """ + units = 'rad' + + def __init__(self, values, filter_id=None): + if isinstance(values, Integral): + values = np.linspace(0., np.pi, values + 1) + super().__init__(values, filter_id) 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) - - def get_pandas_dataframe(self, data_size, stride, **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 : int - The total number of bins in the tally corresponding to this filter - stride : int - Stride in memory for the 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. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - 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], stride) - hi_bins = np.repeat(self.bins[1:], 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 + super().check_bins(bins) + for x in np.ravel(bins): + if not np.isclose(x, 0.): + cv.check_greater_than('filter value', x, 0., equality=True) + if not np.isclose(x, np.pi): + cv.check_less_than('filter value', x, np.pi, equality=True) class AzimuthalFilter(RealFilter): @@ -1610,98 +1479,43 @@ class AzimuthalFilter(RealFilter): Parameters ---------- - bins : Iterable of Real or Integral - A grid of azimuthal angles which events will binned into. Values + values : int or Iterable of Real + 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 + to the z-axis. If an iterable is given, the values will be used + explicitly as grid points. If a single int is given, the range [-pi, pi) will be divided up equally into that number of bins. filter_id : int Unique identifier for the filter 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. + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + azimuthal angles in [rad] for a single bin id : int Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of azimuthal angles + for a single filter bin num_bins : Integral The number of filter bins """ + units = 'rad' + + def __init__(self, values, filter_id=None): + if isinstance(values, Integral): + values = np.linspace(-np.pi, np.pi, values + 1) + super().__init__(values, filter_id) 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) - - def get_pandas_dataframe(self, data_size, stride, 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 : int - The total number of bins in the tally corresponding to this filter - stride : int - Stride in memory for the 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. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - 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], stride) - hi_bins = np.repeat(self.bins[1:], 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 + super().check_bins(bins) + for x in np.ravel(bins): + if not np.isclose(x, -np.pi): + cv.check_greater_than('filter value', x, -np.pi, equality=True) + if not np.isclose(x, np.pi): + cv.check_less_than('filter value', x, np.pi, equality=True) class DelayedGroupFilter(Filter): @@ -1709,7 +1523,7 @@ class DelayedGroupFilter(Filter): Parameters ---------- - bins : Integral or Iterable of Integral + bins : iterable of int 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]. @@ -1718,7 +1532,7 @@ class DelayedGroupFilter(Filter): Attributes ---------- - bins : Integral or Iterable of Integral + bins : iterable of int 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]. @@ -1728,17 +1542,10 @@ class DelayedGroupFilter(Filter): The number of filter bins """ - @Filter.bins.setter - def bins(self, bins): - # Format the bins as a 1D numpy array. - bins = np.atleast_1d(bins) - + def check_bins(self, 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 + for g in bins: + cv.check_greater_than('delayed group', g, 0) class EnergyFunctionFilter(Filter): @@ -1751,18 +1558,18 @@ class EnergyFunctionFilter(Filter): Parameters ---------- energy : Iterable of Real - A grid of energy values in eV. + A grid of energy values in [eV] y : iterable of Real - A grid of interpolant values in eV. + A grid of interpolant values in [eV] filter_id : int Unique identifier for the filter Attributes ---------- energy : Iterable of Real - A grid of energy values in eV. + A grid of energy values in [eV] y : iterable of Real - A grid of interpolant values in eV. + A grid of interpolant values in [eV] id : int Unique identifier for the filter num_bins : Integral @@ -1903,6 +1710,14 @@ class EnergyFunctionFilter(Filter): raise RuntimeError('EnergyFunctionFilters have no bins.') def to_xml_element(self): + """Return XML Element representing the Filter. + + Returns + ------- + element : xml.etree.ElementTree.Element + XML element containing filter data + + """ element = ET.Element('filter') element.set('id', str(self.id)) element.set('type', self.short_name.lower()) diff --git a/openmc/filter_legendre.py b/openmc/filter_legendre.py index 075fa02dbc..18998d5cc8 100644 --- a/openmc/filter_legendre.py +++ b/openmc/filter_legendre.py @@ -34,6 +34,7 @@ class LegendreFilter(Filter): def __init__(self, order, filter_id=None): self.order = order + self.bins = ['P{}'.format(i) for i in range(order + 1)] self.id = filter_id def __hash__(self): @@ -57,10 +58,6 @@ class LegendreFilter(Filter): cv.check_greater_than('Legendre order', order, 0, equality=True) self._order = order - @property - def num_bins(self): - return self._order + 1 - @classmethod def from_hdf5(cls, group, **kwargs): if group['type'].value.decode() != cls.short_name.lower(): @@ -74,44 +71,6 @@ class LegendreFilter(Filter): return out - def get_pandas_dataframe(self, data_size, stride, **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 - stride : int - Stride in memory for the filter - - Returns - ------- - pandas.DataFrame - A Pandas DataFrame with a column that is filled with strings - indicating Legendre orders. The number of rows in the DataFrame is - the same as the total number of bins in the corresponding tally. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - - bins = np.array(['P{}'.format(i) for i in range(self.order + 1)]) - filter_bins = np.repeat(bins, 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 - def to_xml_element(self): """Return XML Element representing the filter. diff --git a/openmc/filter_spatial_legendre.py b/openmc/filter_spatial_legendre.py index 690447d9cb..726a3bfdfe 100644 --- a/openmc/filter_spatial_legendre.py +++ b/openmc/filter_spatial_legendre.py @@ -44,6 +44,7 @@ class SpatialLegendreFilter(Filter): self.axis = axis self.minimum = minimum self.maximum = maximum + self.bins = ['P{}'.format(i) for i in range(order + 1)] self.id = filter_id def __hash__(self): @@ -118,42 +119,6 @@ class SpatialLegendreFilter(Filter): return cls(order, axis, min_, max_, filter_id) - def get_pandas_dataframe(self, data_size, stride, **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 strings - indicating Legendre orders. The number of rows in the DataFrame is - the same as the total number of bins in the corresponding tally. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - - bins = np.array(['P{}'.format(i) for i in range(self.order + 1)]) - filter_bins = np.repeat(bins, 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 - def to_xml_element(self): """Return XML Element representing the filter. diff --git a/openmc/filter_spherical_harmonics.py b/openmc/filter_spherical_harmonics.py index 3b2e32b140..f95c09c303 100644 --- a/openmc/filter_spherical_harmonics.py +++ b/openmc/filter_spherical_harmonics.py @@ -34,6 +34,9 @@ class SphericalHarmonicsFilter(Filter): def __init__(self, order, filter_id=None): self.order = order self.id = filter_id + self.bins = ['Y{},{}'.format(n, m) + for n in range(order + 1) + for m in range(-n, n + 1)] self._cosine = 'particle' def __hash__(self): @@ -87,49 +90,6 @@ class SphericalHarmonicsFilter(Filter): return out - def get_pandas_dataframe(self, data_size, stride, **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 - stride : int - Stride in memory for the filter - - Returns - ------- - pandas.DataFrame - A Pandas DataFrame with a column that is filled with strings - indicating spherical harmonics orders. The number of rows in the - DataFrame is the same as the total number of bins in the - corresponding tally. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - - bins = [] - for n in range(self.order + 1): - bins.extend('Y{},{}'.format(n, m) for m in range(-n, n + 1)) - bins = np.array(bins) - - filter_bins = np.repeat(bins, 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 - def to_xml_element(self): """Return XML Element representing the filter. diff --git a/openmc/filter_zernike.py b/openmc/filter_zernike.py index 3b4efe5d58..a7697f5d0e 100644 --- a/openmc/filter_zernike.py +++ b/openmc/filter_zernike.py @@ -48,6 +48,9 @@ class ZernikeFilter(Filter): self.x = x self.y = y self.r = r + self.bins = ['Z{},{}'.format(n, m) + for n in range(order + 1) + for m in range(-n, n + 1, 2)] self.id = filter_id def __hash__(self): @@ -116,48 +119,6 @@ class ZernikeFilter(Filter): return cls(order, x, y, r, filter_id) - def get_pandas_dataframe(self, data_size, stride, **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 strings - indicating Zernike orders. The number of rows in the DataFrame is - the same as the total number of bins in the corresponding tally. - - See also - -------- - Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() - - """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - - # Create list of strings for each order - orders = [] - for n in range(self.order + 1): - for m in range(-n, n + 1, 2): - orders.append('Z{},{}'.format(n, m)) - - bins = np.array(orders) - filter_bins = np.repeat(bins, 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 - def to_xml_element(self): """Return XML Element representing the filter. diff --git a/openmc/mesh.py b/openmc/mesh.py index cd8eb3c5ed..aed7a46d8c 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -38,6 +38,9 @@ class Mesh(IDManagerMixin): are given, it is assumed that the mesh is an x-y mesh. width : Iterable of float The width of mesh cells in each direction. + indices : list of tuple + A list of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, + 1), ...] """ diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 4932feff1e..5bc00cbc95 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -1164,12 +1164,14 @@ class MGXS(metaclass=ABCMeta): if not isinstance(tally_filter, (openmc.EnergyFilter, openmc.EnergyoutFilter)): continue - elif len(tally_filter.bins) != len(fine_edges): + elif len(tally_filter.bins) != len(fine_edges) - 1: continue - elif not np.allclose(tally_filter.bins, fine_edges): + elif not np.allclose(tally_filter.bins[:, 0], fine_edges[:-1]): continue else: - tally_filter.bins = coarse_groups.group_edges + cedge = coarse_groups.group_edges + tally_filter.values = cedge + tally_filter.bins = np.vstack((cedge[:-1], cedge[1:])).T mean = np.add.reduceat(mean, energy_indices, axis=i) std_dev = np.add.reduceat(std_dev**2, energy_indices, axis=i) @@ -2738,7 +2740,7 @@ class TransportXS(MGXS): if self._rxn_rate_tally is None: # Switch EnergyoutFilter to EnergyFilter. old_filt = self.tallies['scatter-1'].filters[-1] - new_filt = openmc.EnergyFilter(old_filt.bins) + new_filt = openmc.EnergyFilter(old_filt.values) self.tallies['scatter-1'].filters[-1] = new_filt self._rxn_rate_tally = \ @@ -2757,7 +2759,7 @@ class TransportXS(MGXS): # Switch EnergyoutFilter to EnergyFilter. old_filt = self.tallies['scatter-1'].filters[-1] - new_filt = openmc.EnergyFilter(old_filt.bins) + new_filt = openmc.EnergyFilter(old_filt.values) self.tallies['scatter-1'].filters[-1] = new_filt # Compute total cross section diff --git a/openmc/tallies.py b/openmc/tallies.py index 6c055d1913..08db20e399 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -218,10 +218,9 @@ class Tally(IDManagerMixin): f = h5py.File(self._sp_filename, 'r') # Extract Tally data from the file - data = f['tallies/tally {0}/results'.format( - self.id)].value - sum = data[:,:,0] - sum_sq = data[:,:,1] + data = f['tallies/tally {0}/results'.format(self.id)].value + sum = data[:, :, 0] + sum_sq = data[:, :, 1] # Reshape the results arrays sum = np.reshape(sum, self.shape) @@ -273,8 +272,8 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._mean = \ - sps.lil_matrix(self._mean.flatten(), self._mean.shape) + self._mean = sps.lil_matrix(self._mean.flatten(), + self._mean.shape) if self.sparse: return np.reshape(self._mean.toarray(), self.shape) @@ -295,8 +294,8 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._std_dev = \ - sps.lil_matrix(self._std_dev.flatten(), self._std_dev.shape) + self._std_dev = sps.lil_matrix(self._std_dev.flatten(), + self._std_dev.shape) self.with_batch_statistics = True @@ -436,17 +435,16 @@ class Tally(IDManagerMixin): # Convert NumPy arrays to SciPy sparse LIL matrices if sparse and not self.sparse: if self._sum is not None: - self._sum = \ - sps.lil_matrix(self._sum.flatten(), self._sum.shape) + self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) if self._sum_sq is not None: - self._sum_sq = \ - sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) + self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), + self._sum_sq.shape) if self._mean is not None: - self._mean = \ - sps.lil_matrix(self._mean.flatten(), self._mean.shape) + self._mean = sps.lil_matrix(self._mean.flatten(), + self._mean.shape) if self._std_dev is not None: - self._std_dev = \ - sps.lil_matrix(self._std_dev.flatten(), self._std_dev.shape) + self._std_dev = sps.lil_matrix(self._std_dev.flatten(), + self._std_dev.shape) self._sparse = True @@ -776,11 +774,11 @@ class Tally(IDManagerMixin): other_sum = other_copy.get_reshaped_data(value='sum') if join_right: - merged_sum = \ - np.concatenate((self_sum, other_sum), axis=merge_axis) + merged_sum = np.concatenate((self_sum, other_sum), + axis=merge_axis) else: - merged_sum = \ - np.concatenate((other_sum, self_sum), axis=merge_axis) + merged_sum = np.concatenate((other_sum, self_sum), + axis=merge_axis) merged_tally._sum = np.reshape(merged_sum, merged_tally.shape) @@ -790,11 +788,11 @@ class Tally(IDManagerMixin): other_sum_sq = other_copy.get_reshaped_data(value='sum_sq') if join_right: - merged_sum_sq = \ - np.concatenate((self_sum_sq, other_sum_sq), axis=merge_axis) + merged_sum_sq = np.concatenate((self_sum_sq, other_sum_sq), + axis=merge_axis) else: - merged_sum_sq = \ - np.concatenate((other_sum_sq, self_sum_sq), axis=merge_axis) + merged_sum_sq = np.concatenate((other_sum_sq, self_sum_sq), + axis=merge_axis) merged_tally._sum_sq = np.reshape(merged_sum_sq, merged_tally.shape) @@ -804,11 +802,11 @@ class Tally(IDManagerMixin): other_mean = other_copy.get_reshaped_data(value='mean') if join_right: - merged_mean = \ - np.concatenate((self_mean, other_mean), axis=merge_axis) + merged_mean = np.concatenate((self_mean, other_mean), + axis=merge_axis) else: - merged_mean = \ - np.concatenate((other_mean, self_mean), axis=merge_axis) + merged_mean = np.concatenate((other_mean, self_mean), + axis=merge_axis) merged_tally._mean = np.reshape(merged_mean, merged_tally.shape) @@ -818,11 +816,11 @@ class Tally(IDManagerMixin): other_std_dev = other_copy.get_reshaped_data(value='std_dev') if join_right: - merged_std_dev = \ - np.concatenate((self_std_dev, other_std_dev), axis=merge_axis) + merged_std_dev = np.concatenate((self_std_dev, other_std_dev), + axis=merge_axis) else: - merged_std_dev = \ - np.concatenate((other_std_dev, self_std_dev), axis=merge_axis) + merged_std_dev = np.concatenate((other_std_dev, self_std_dev), + axis=merge_axis) merged_tally._std_dev = np.reshape(merged_std_dev, merged_tally.shape) @@ -1003,35 +1001,6 @@ class Tally(IDManagerMixin): return filter_found - def get_filter_index(self, filter_type, filter_bin): - """Returns the index in the Tally's results array for a Filter bin - - Parameters - ---------- - filter_type : openmc.FilterMeta - Type of the filter, e.g. MeshFilter - filter_bin : int or tuple - The bin is an 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 a (x,y,z) - 3-tuple for 'mesh' filters corresponding to the mesh cell of - interest. - - Returns - ------- - The index in the Tally data array for this filter bin - - """ - - # Find the equivalent Filter in this Tally's list of Filters - filter_found = self.find_filter(filter_type) - - # Get the index for the requested bin from the Filter and return it - filter_index = filter_found.get_bin_index(filter_bin) - return filter_index - def get_nuclide_index(self, nuclide): """Returns the index in the Tally's results array for a Nuclide bin @@ -1151,48 +1120,28 @@ class Tally(IDManagerMixin): # Loop over all of the Tally's Filters for i, self_filter in enumerate(self.filters): - user_filter = False - # If a user-requested Filter, get the user-requested bins for j, test_filter in enumerate(filters): if type(self_filter) is test_filter: bins = filter_bins[j] - user_filter = True break + else: + # If not a user-requested Filter, get all bins + if isinstance(self_filter, openmc.DistribcellFilter): + # Create list of cell instance IDs for distribcell Filters + bins = list(range(self_filter.num_bins)) - # If not a user-requested Filter, get all bins - if not user_filter: - # Create list of 2- or 3-tuples tuples for mesh cell bins - if isinstance(self_filter, openmc.MeshFilter): - bins = list(self_filter.mesh.indices) - - # Create list of 2-tuples for energy boundary bins - elif isinstance(self_filter, (openmc.EnergyFilter, - openmc.EnergyoutFilter, openmc.MuFilter, - openmc.PolarFilter, openmc.AzimuthalFilter)): - bins = [] - for k in range(self_filter.num_bins): - bins.append((self_filter.bins[k], self_filter.bins[k+1])) - - # Create list of cell instance IDs for distribcell Filters - elif isinstance(self_filter, openmc.DistribcellFilter): - bins = [b for b in range(self_filter.num_bins)] - - # EnergyFunctionFilters don't have bins so just add a None elif isinstance(self_filter, openmc.EnergyFunctionFilter): + # EnergyFunctionFilters don't have bins so just add a None bins = [None] - # Create list of IDs for bins for all other filter types else: + # Create list of IDs for bins for all other filter types bins = self_filter.bins - # Initialize a NumPy array for the Filter bin indices - filter_indices.append(np.zeros(len(bins), dtype=np.int)) - # Add indices for each bin in this Filter to the list - for j, bin in enumerate(bins): - filter_index = self.get_filter_index(type(self_filter), bin) - filter_indices[i][j] = filter_index + indices = np.array([self_filter.get_bin_index(b) for b in bins]) + filter_indices.append(indices) # Account for stride in each of the previous filters for indices in filter_indices[:i]: @@ -1956,14 +1905,14 @@ class Tally(IDManagerMixin): elif isinstance(filter1, openmc.EnergyFunctionFilter): filter1_bins = [None] else: - filter1_bins = [filter1.get_bin(i) for i in range(filter1.num_bins)] + filter1_bins = filter1.bins if isinstance(filter2, openmc.DistribcellFilter): filter2_bins = [b for b in range(filter2.num_bins)] elif isinstance(filter2, openmc.EnergyFunctionFilter): filter2_bins = [None] else: - filter2_bins = [filter2.get_bin(i) for i in range(filter2.num_bins)] + filter2_bins = filter2.bins # Create variables to store views of data in the misaligned structure mean = {} @@ -2604,7 +2553,8 @@ class Tally(IDManagerMixin): new_tally = self * -1 return new_tally - def get_slice(self, scores=[], filters=[], filter_bins=[], nuclides=[]): + def get_slice(self, scores=[], filters=[], filter_bins=[], nuclides=[], + squeeze=False): """Build a sliced tally for the specified filters, scores and nuclides. This method constructs a new tally to encapsulate a subset of the data @@ -2615,26 +2565,26 @@ class Tally(IDManagerMixin): Parameters ---------- scores : list of str - A list of one or more score strings - (e.g., ['absorption', 'nu-fission']; default is []) + A list of one or more score strings (e.g., ['absorption', + 'nu-fission'] filters : Iterable of openmc.FilterMeta - An iterable of filter types - (e.g., [MeshFilter, EnergyFilter]; default is []) + An iterable of filter types (e.g., [MeshFilter, EnergyFilter]) filter_bins : list of Iterables - A list of tuples of filter bins corresponding to the filter_types - parameter (e.g., [(1,), ((0., 0.625e-6),)]; default is []). Each - tuple contains bins to slice for the corresponding filter type in - the filters parameter. Each bins is the integer ID for 'material', + A list of iterables of filter bins corresponding to the specified + filter types (e.g., [(1,), ((0., 0.625e-6),)]). Each iterable + contains bins to slice for the corresponding filter type in the + filters parameter. Each bin is the integer ID for 'material', 'surface', 'cell', 'cellborn', and 'universe' Filters. Each bin is an integer for the cell instance ID for 'distribcell' Filters. Each 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. The order of the bins in the list must - correspond to the filter_types parameter. + correspond to the `filters` argument. nuclides : list of str - A list of nuclide name strings - (e.g., ['U235', 'U238']; default is []) + A list of nuclide name strings (e.g., ['U235', 'U238']) + squeeze : bool + Whether to remove filters with only a single bin in the sliced tally Returns ------- @@ -2714,32 +2664,29 @@ class Tally(IDManagerMixin): # Determine the filter indices from any of the requested filters for i, filter_type in enumerate(filters): - find_filter = new_tally.find_filter(filter_type) + f = new_tally.find_filter(filter_type) + + # Remove filters with only a single bin if requested + if squeeze: + if len(filter_bins[i]) == 1: + new_tally.filters.remove(f) + continue + else: + raise RuntimeError('Cannot remove sliced filter with ' + 'more than one bin.') # Remove and/or reorder filter bins to user specifications - bin_indices = [] + bin_indices = [f.get_bin_index(b) + for b in filter_bins[i]] + bin_indices = np.unique(bin_indices) - for filter_bin in filter_bins[i]: - bin_index = find_filter.get_bin_index(filter_bin) - if issubclass(filter_type, openmc.RealFilter): - bin_indices.extend([bin_index, bin_index+1]) - else: - bin_indices.append(bin_index) - - # Set bins for mesh/distribcell filters apart from others - if filter_type is openmc.MeshFilter: - bins = find_filter.mesh - elif filter_type is openmc.DistribcellFilter: - bins = find_filter.bins - else: - bins = np.unique(find_filter.bins[bin_indices]) - - # Create new filter - new_filter = filter_type(bins) + # Set bins for sliced filter + new_filter = copy.copy(f) + new_filter.bins = [f.bins[i] for i in bin_indices] # Set number of bins manually for mesh/distribcell filters if filter_type is openmc.DistribcellFilter: - new_filter._num_bins = find_filter._num_bins + new_filter._num_bins = f._num_bins # Replace existing filter with new one for j, test_filter in enumerate(new_tally.filters): diff --git a/tests/regression_tests/tally_slice_merge/test.py b/tests/regression_tests/tally_slice_merge/test.py index e52d0fde49..7146c0d098 100644 --- a/tests/regression_tests/tally_slice_merge/test.py +++ b/tests/regression_tests/tally_slice_merge/test.py @@ -87,12 +87,14 @@ class TallySliceMergeTestHarness(PyAPITestHarness): # Slice the tallies by cell filter bins cell_filter_prod = itertools.product(tallies, self.cell_filters) tallies = map(lambda tf: tf[0].get_slice(filters=[type(tf[1])], - filter_bins=[tf[1].get_bin(0)]), cell_filter_prod) + filter_bins=[tf[1].get_bin(0)]), + cell_filter_prod) # Slice the tallies by energy filter bins energy_filter_prod = itertools.product(tallies, self.energy_filters) tallies = map(lambda tf: tf[0].get_slice(filters=[type(tf[1])], - filter_bins=[(tf[1].get_bin(0),)]), energy_filter_prod) + filter_bins=[tf[1].get_bin(0)]), + energy_filter_prod) # Slice the tallies by nuclide nuclide_prod = itertools.product(tallies, self.nuclides)