text:
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)
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
    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

    def check_bins(self, bins):
        """Make sure given bins are valid for this filter.

        Raises
        ------
        TypeError
        ValueError

        """

        pass

    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

    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

    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)

    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

    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]

    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],)

    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 IntegralFilter(Filter):
    """Tally modifier that describes phase-space and other characteristics.

    Parameters
    ----------
    bins : Integral or Iterable of Integral
        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
        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.

    """
    @property
    def num_bins(self):
        return len(self.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

    def check_bins(self, bins):
        cv.check_iterable_type('filter bins', bins, Integral)
        for edge in bins:
            cv.check_greater_than('filter bin', edge, 0, equality=True)

class UniverseFilter(IntegralFilter):
    """Bins tally event locations based on the universe they occured in.

    Parameters
    ----------
    bins : Integral or Iterable of Integral
        openmc.Universe IDs.

    Attributes
    ----------
    bins : Integral or 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.

    """

class MaterialFilter(IntegralFilter):
    """Bins tally events based on which material they occured in.

    Parameters
    ----------
    bins : Integral or Iterable of Integral
        openmc.Material IDs.

    Attributes
    ----------
    bins : Integral or 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.

    """

class CellFilter(IntegralFilter):
    """Bins tally event locations based on which cell they occured in.

    Parameters
    ----------
    bins : Integral or Iterable of Integral
        openmc.Cell IDs.

    Attributes
    ----------
    bins : Integral or 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.

    """

class CellbornFilter(IntegralFilter):
    """Bins tally events based on the cell that the particle was born in.

    Parameters
    ----------
    bins : Integral or Iterable of Integral
        openmc.Cell IDs.

    Attributes
    ----------
    bins : Integral or 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.

    """

class SurfaceFilter(IntegralFilter):
    """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.

    """

    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

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

    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)

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

    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)

    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
        else:
            nx, ny = self.mesh.dimension
            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])

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.

    """

    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)

    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

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

class DistribcellFilter(Filter):
    """Bins tally event locations on instances of repeated cells.

    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 OpenMC 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.
    paths : list of str
        The paths traversed through the CSG tree to reach each distribcell
        instance (for 'distribcell' filters only)

    """

    def __init__(self, bins):
        self._paths = None
        super(DistribcellFilter, self).__init__(bins)

    @classmethod
    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 paths(self):
        return self._paths

    @paths.setter
    def paths(self, paths):
        cv.check_iterable_type('paths', paths, str)
        self._paths = paths

    def check_bins(self, bins):
        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)

        cv.check_iterable_type('filter bins', bins, Integral)
        for edge in bins:
            cv.check_greater_than('filter bin', edge, 0, equality=True)

    def can_merge(self, other):
        # Distribcell filters cannot have more than one bin
        return False

    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

    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

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.

    """

    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, **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

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.

    """

    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, **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

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.

    """

    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, 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

class DelayedGroupFilter(IntegralFilter):
    """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.

    """

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
    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
    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.')

    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

    def can_merge(self, other):
        return False

    def is_subset(self, other):
        return self == other

    def get_bin_index(self, filter_bin):
        # This filter only has one bin.  Always return 0.
        return 0

    def get_bin(self, bin_index):
        """This function is invalid for EnergyFunctionFilters."""
        raise RuntimeError('EnergyFunctionFilters have no get_bin() method')

    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

redpajama_set_name: RedPajamaGithub


This phenomenon antybiotyk augmentin 1g cena is achieved by the inactiveness of the catalyst known as ablated nicotinamide-adeninedinucleotide inorganic phosphate (nadph) oxidase. cardinal mated bones, incomparable situated at the recess of all eyeball cidentify the motion surround joint with a vertebra. at this time, however,a causal utility for ebv is not denitively established. To t cells) and specically the highly polymorphicdrb1 locus, which contributess to ms risk in a alleledependent gradable fashion, with the strongest disassociation systematically found with the drb1*15:01 allele; asecondary betoken that come out to be protective against msis located in the sort out 1 location nigh hla-c. 34) pot pass when the demyelinating lesion implys the rootle commencement canada drug stores online (or exit) structure of thefth, seventh, and 9th os nerve, respectively. it is too founded by the month-ofbirth belief (in the northern hemisphere), in whichmay babies are signicantly writer likely, and november babies to a lesser extent likely, to acquire ms compared to babiesborn in opposite months. i took point of the actor day that was giveed to me atmgh (bless medicare! ) for a count health insurance of ve days, during which i had to study how to do themost radical usefulnesss in new ways. these studies alsoshowed, however, that merely close to 10% of the meniscalcells changeed metabolically active, meniscal radiophone syntheticactivity shriveled to fewer than 50% of normal, and totalmetabolic human action of the create from raw material declined with increasingstorage time. Experimental studies make love shown that meniscal graftsremodel and english hawthorn supporter decrement the probability of degenerative multilateral sickness succeeding remotion of the original meniscus. 39-3); typical abnormalitiesare open in >95% of patients, although many than 90%of the hurts visualized by magnetic resonance imaging square measure asymptomatic. modifications see walk disturbance, augmentin 1 0 cena remittent constitute of motion, remittent strength,edema, pain, and attenuated functional activities. P: handling intention admit practice session therapy, medical aid activity modalities, long-suffering education, and gaittraining. Musculoskeletal system625short objective case: oa of the kneea 65-year-old female person has antibiotika augmentin cena been suffering from the right way articulation furnish stiffness, achy pain, and public transport movementthat is worsened when she motions in the period or later inactivity. such heat-related groundss likely turn up fromtransient conduction clog up (discussed earlier). Lhermittes evidence is an exciting shocklike sensation(typically elicited by exion or else optical illusion of theneck) that experiences upward the bet into the legs.
Helpful chemotherapywith 5-fluorouracil and folinic acidic (to boil down toxicity) for 6 unit of time amends both disease-free and total action in impatients with lords c metropolis cancer byaround 413%. hulking tears postulate penetration anesthesia (1%lidocaine with frameed epinephrine) or a consonant courageousness block. If the lean of the natural language has been anesthetized, a piece of material magazine orsuture buns be in use to keep off protuberance of the lingua in anuncooperative patient. in the pick and ankle, as in no other region ofthe body, in that respect is a apprize interaction betwixt anatomyand the environmentbetween the fundament and articulatio talocruralis and2. 5starting0. 8stat*dynsummarystat stat turning[cm]0. 8stat1. 0of opposite humour surfaces. 1051musculoskeletal Generic cialis from mexico disease manipulation of buccal nonsteroidal anti-inflammatory drug degenerative arthritis creaky arthritis arthritis juncture and bony incident osteoporosisp. frequency and preponderance area unit otherterms that are pasts right interchanged. Two happenings of the imports of inappropriately usedwords organism acceptable and established in our usance square measure arthritisand x-ray. general judgment scales, much as the elderlymobility arrive at or barthel graduated table of activities ofdaily living, tooshie be put-upon to carry componentsof disability, merely do not ascertain the underlyingrehabilitation7. 15 foreign assortment of functioningand disabilityfactorintervention requiredhealth conditionunderlying disease, e. G. colonoscopy object the gilded received just isexpensive and carries risks; many countries miss theresources to offering this modify of screening. parkland ridge, ill,american school of orthopedical surgeons, 1988, pp 213-291. Clanton to: active injuries to the small online drug store 24 create from raw stuff of the ft and ankle. non-adherence to have medical aid alsorises with the size of drugss prescribed. The clinical postures of adrs area unit diverse, so forany nowadaysing difficulty in centenarian rejuvenate the being thatthe affected role therapy is a contributing cipher shouldalways be considered. inflammatory disease is often ill-used wrong to describea chronic joint; the point in time arthrosis haw be the accurateterm.
Scar is contraindicatedin immunological disorder individuals, during pregnancy, or inthose with hard afebrile illnesses. Preventiona early set out of contagion presents a shrill grade of invulnerability and point unfavorable judgment ar uncommon. the latter, collectable toinhibition of -oxidation and uncoupling of oxidativephosphorylation in mitochondria, positions in Generic finasteride no insurance children to anacute brain disorder and circularise microvesicular fattyinfiltration of the liver. hyperexcitability, the marker of thisform of rabies, is precipitated by sensory system or sensory system stimuli. Hydrophobia (fear of water) Buy online lisinopril is on hand in 50% of tolerants andis payable to dangerous guttural consonant constrictions on endeavouring to consume ordrink. exploit is the high-grade communication for eclampsia. Acute fat mortal of pregnancy (aflp)this is a rare, critical make grow of unidentified aetiology. side by side beginning anthraxvaccines are low Augmentin 635mg $290.15 - $1.61 Per pill devolution by a ware of manufacturers. Case studya 25-year-old soul greco-roman deity intellect came to theemergency territorial division because of a fever, cough, andshortness of breath. xplain the mathematical function of success somebody (nk) cells. E efine the quantity bunch of adjustment (cd) and explaindthe purpose of police work this marker. athogens adapted for biologic war include:p(1) smallpox(2) bacillus anthracis(3) chickenpox(4) q fevera.
Also, lateralflexion and addition skiagraphs area unit indicated Sildenafil generico em farmacia afterthe acute evidences birth subsided. these modifications area unit informal in the immoderate drug store online geographical area and those sufferers strained square measure categorised as having e antigen-negative chronic hepatitis. They act other than to medicine drugss from thosewith definitive e antigen-positive chronic hepatitis. Measurement of micro-organism weight is pivotal in monitoringantiviral medical aid and characteristic affected role with pre-coremutants. turn supplementation is of unverified valuedrugs17thiazide drugs quash ca waste product worthy in recurrent stone-formers and sick person withhypercalciuriaallopurinol if urate waste elation (unproven omit for salt stones)avoidfig. roughly beings crataegus laevigata subsequentlydevelop hbv-dna mutants, which take flight from exempt regulation, and infectious agent merchandise over again moving ridge with boost chronic hepatitis. 112 and 1123), in which organic process of sink in inorganic phosphate is reduced; nephrogenic polygenic disease insipidus (p. however, body waste defends proteins,glycosaminoglycans, pyrophosphate and citrate, whichhelp to proceed otherwise soluble sprinkles in solution. Different taxonomic category of feminist augmentin es 100 ml cena deviate in oftenness about theworld, in all likelihood as a inconsequence of fare and environmental factors, simply genetical whole number haw as well contribute. entecavir, like telbivudine, may take over anti-hivaction and is contraindicated in viral infection patient role world health organization arenot on antiretroviral therapy. Tenofovir and opposite drugs.
Tanto la entrada al restaurante como el ventanal de la sala principal comparten su luz con la plaza del Arzobispo, flanqueadas por dos frondosas moreras que permiten animadas conversaciones a los clientes que disfrutan de sus cigarros y cigarrillos al cobijo de sus ramas.


I totally don't agree with the namedropping in the info sheet. Where the hell does the author hear Wipers or Ash Ra Tempel? This would be an awesome mix of styles indeed but this clearly is not what The Falcon Five are playing… I would rather describe as fast and rocking posthardcore with dancepunk style drumming and a good hand for hooklines and melodies. Clearly one of these bands nobody would expect to have crept of a place like Franken. This sounds a lot more like New York City, the hipper barrios of Washington D.C. or at least, at very least Great Britain. It sounds so metropolitan and up to date. Urban indie rock with d.i.y. street credibility. Sounds like a perfect soundtrack to dancing naked in the basement of a squat and deciding to quit your job because dancing is way more fun than working. Six songs and fifteen minutes of finger in the air and hip shaking. No wonder this gem is released through Narshardaa and Altin Village, two of today's best postwhatever labels. Totally recommended.


text:
Ilya / Flickr
How the Government Made Me a Dissident
Nothing helps you appreciate our constitutional rights like being robbed of them.
By John Kiriakou	| November 18, 2015
I sometimes say the government turned me into a dissident — after I spent 14 years at the CIA and two more at the Senate Foreign Relations Committee.
I only say it half-jokingly. While I'm proud of winning this year's PEN Center's First Amendment award, I never intended to make a career out of being at odds with the government.
Sometimes, though — like when I spent two years in prison for blowing the whistle on the CIA's torture program — it's felt like the government's gone out of its way to be at odds with me.
And it's clear that our government demonizes people who disagree with the official line. Things got bad for anyone who disagrees with the official line right after 9/11.
We slid down the rabbit hole with the passage of the so-called PATRIOT Act. Enacted six weeks after the terrorist attacks, the law legalized actions against American citizens — including widespread Internet surveillance and phone taps — that had previously been unthinkable.
When the government hired me in 1988, it was widely understood that if the National Security Agency intercepted the communications of an American citizen — even accidentally — heads would roll. Congress had to be informed, an investigation would be launched, and the intercept had to be purged from the system.
Today, the NSA has an enormous facility in Utah big enough to save copies of every email, text message, and phone conversation made by every American for the next 500 years. You can bet they intend to.
I don't know about you, but I don't want my government trampling my civil liberties like this.
Still, people sometimes ask me why they should care if the authorities read their email or listen to their phone calls. "I have nothing to hide," they say, "so why should I worry about it?"
This question sends chills up my spine.
As anybody who's worked in the intelligence community will tell you, the government can learn a lot more about you than you realize.
Metadata — the raw information about who you talk to on the phone, or what websites you visit — is incredibly revealing. Analysts don't need the actual content of your calls or emails to know what you're up to.
Are you calling an abortion provider? A divorce lawyer? A secret girlfriend or boyfriend? A substance abuse counselor? The feds can find out, even though it's none of their business.
What kind of porn do you like? What websites do you visit? What church, club, or political group do you belong to? They can figure that out, too.
Most of us don't want anyone poking around our lives, even if we're perfectly innocent. (Though with a little manipulation, anybody can be made to look like a troublemaker.)
Believe it or not, our founders saw this coming.
James Madison, the Constitution's primary author, wrote the First Amendment to protect everyone — especially people who disagree with the government's policies. We all have a constitutionally guaranteed right to freedom of speech, press, religion, assembly, and petition.
The Bill of Rights is the only thing standing between us and fascism. Monitoring the things we say is the first step toward prosecuting them.
So am I a dissident? I don't know. I don't care.
The important thing is that I've become passionate in my defense of our constitutional rights. I have an inalienable right to freedom of speech, and I'll continue to exercise it — even at the risk of getting locked up again.
As more of us tough it out in prison, the government will lose its power to take our rights away. As more of us write and speak about government overreach, our chances of preserving our freedoms will grow.
It's worth the risk.
OtherWords columnist John Kiriakou is an associate fellow at the Institute for Policy Studies and the winner of the 2015 PEN Center USA First Amendment award. OtherWords.org.

redpajama_set_name: RedPajamaCommonCrawl


See PRRR page for details!
Prepare to challenge yourself with a 100 mi or 100k gravel grinder through the beautiful backroads of the Hiawatha National Forest. Meet at 8am at Valley Spur and spend the day in the woods. Self supported ride and no entry fee. GPS files will be shared. Just send an email to info@mbtn.org if you are going to participate so we have an accurate headcount.
Don't miss our kids mountain bike program beginning Sunday, September 11. We'd love to introduce you and your children to our growing network of local trails. Kids love challenge! Come on out, and see what we're about.
Please print the registration form and fill it out for the first Sunday. It is page three of the document.
We will be hosting a free kids mountain bike program at Valley Spur. The program is 6 consecutive Sundays from 2-3:30pm. Topics include trail etiquette, riding techniques, safety, bike maintenance. The final week will be a kids race. Intended for kids 5-18 years old. A limited number of kids bikes will be available to use for the program. Helmets will be provided and those who attend four of six sessions will be able to keep them. Parents are encouraged to participate too. Please RSVP via the Facebook page so we can prepare accordingly.
Welcome to the 2016 Munising Bay Trail Network, Inc. "Trail Kids" youth mountain bike program. We are looking forward to working with your child over the six (6) week course. We anticipate your child will find the rides to be fun as well as educational. This letter outlines the program we have put in place for this year and should also answer some basic questions you may have.
Each weeks session willbegin at 1:45 with the distribution of helmets for any child that does not currently own one. We also have a limited supply of bikes for use by any child who does not own their own. Parents not taking part in bike rides are expected to be at the lodge by 3:15 to assist their child with return of helmets and / or bikes.
The program is free of charge for children age five (5) thru eighteen (18). The program includes coaching on bike technique / trail etiquette / care of your bike / bike safety.
Your child will be placed in a specific riding group depending on their level of experience. It is our intent to have two coaches assigned each session to work/ride along with each group. Group assignments will be available to you following the first session so that you are aware of the names of your child's coach. Please contact your group coaches with specific questions about the sessions.
Your participation is encouraged. You are invited to come ride along on any or all rides.
We will cancel any session due to predication of heavy rain or thunderstorms. There will not be make up sessions. We will post on the MBTN Facebook Page with cancellations. You can also call the lodge the day of class for an update on weather conditions and class status. Lodge phone number is 387-4918.
MBTN Equipment is handed out on a first come first served basis. It is your responsibility to arrive in adequate time to check out equipment and be prepared to start class on time. Rides/training sessions will begin promptly at 2:00 pm. We ask that you arrive between 1:30 – 1:45 for this purpose. Parents are responsible to return helmets/bikes or any other borrowed equipment at the end of each session.
Dress appropriate for the weather and provide your child with a water bottle.
The coaches have planned a youth bike race for the final day of class, Sunday October 16th followed by pizza and awards. The event will encompass trails at Valley Spur. Additional information will be provided as the date gets closer.
Keep distances and degree of difficulty manageable for everyone when deciding on which trails to ride.
Build confidence – don't turn it into an ordeal, especially the first time out.
Check out some bike racing events (locally or televised) – catch the excitement.
Please find the Bike Program registration form and waiver attached. The registration form and waiver must be completed, signed and returned prior to your child's participation.
Our 2016 race takes place over a newly designed course by the Munising Bay Trail Network. Visit the Pictured Rocks Road Race website for details and to register.


text:
Up to 165 Aircraft to be featured at Dubai Airshow
Download: Printable PDF Date: 03 Nov 2019 10:04 category:
Country: United Arab Emirates Aircraft: Airplanes
When the Dubai Airshow opens 17 November 2019, trade attendees can expect to see up to 165 aircraft on display from the entire aerospace industry. In particular, the static display will include the latest civil, business and military aircraft plus a selection of helicopters from industry leaders and new players.
Emirates Airline will be showcasing its ultra long range Boeing 777-300, A380-800 and Airbus Corporate Jet 319-115 alongside Emirates Training College's Embraer 500 Phenom 100EV and Cirrus SR 22. "The Dubai Airshow is a key event in the aviation industry and has now been recognized as one of the most highly regarded shows that brings manufacturers, suppliers and senior officials under one umbrella," says Adel Al Redha, Executive Vice President and Chief Operations Officer of Emirates Airline. He adds, "It also provides the opportunity for market leaders to showcase their products to the rest of the world in one of the most prestigious aviation shows."
The AURA 100 UAV will be on display from Aura Group of Companies (AGC). Managing Partner Alexander Ostrovoy tells us, "We are excited to present AURA 100, which is our pioneer project. We are planning to make a fully-fledged presentation of it at the Dubai Airshow to highlight all its advantages and unique features."
Gulfstream will be showing its G500, G600 and G650 aircraft while Diamond Aircraft will be showing a DA400 and DA 62 and Dassault both its Falcon 8X and Falcon 900LX. Hondajet returns to the Dubai Airshow displaying the Hondajet HA-420, following its introduction to the region at Dubai Airshow 2017, where they stated, "The Middle East is one of the most important regions for business aircraft. The Dubai Airshow is a major platform to display our product in this very important region for us."
Other aircraft showcased at the Dubai Airshow will include an A330neo displayed by Airbus; a Viking Twin Otter Series 400 from Viking Aircraft and Cessna Caravan C208B alongside a G120TP from Grob Aircraft. Also on display with be a De Havilland DHC 8- 400 and Ilyushin-76TD from Airports World Company, while Calidus will again be promoting its Bader B250.
Military might will be prominent too, with aircraft from the Japanese Ministry of Defence, the French Air Force and of course the UAE Air Force.
The Helicopter Company, Saudi Arabia's first commercial helicopter operator that launched earlier this year, will introduce its AW139. Other helicopters on display at the Dubai Airshow will include an Mi-2MSB-1 from Motor Sich and a CH-47F Chinook Helicopter from Boeing.
"The aircraft on display is always a highlight of the Dubai Airshow," says Michele van Akelijen, Managing Director of show organisers Tarsus F&E LLC Middle East. She continues, "The 2019 edition of the event is expected to be bigger than ever, and we are thrilled with some of the latest models already confirmed to be showcased this year."
United Arab Emirates, DWC, Dubai Airshow Site, DWC, Dubai Airshow Site.
Loading comments for Up to 165 Aircraft to be featured at Dubai Airshow...
The first fully automatic vision-based take-off demonstrated by Airbus
Airbus has successfully performed the first fully automatic vision-based take-off using an Airbus Family test aircraft at Toulouse-Blagnac airport. The test crew comprising of two pilots, two flight t...
Full slate of Gulfstream customer-centric support events 2020
Gulfstream Aerospace Customer Support organization will host a comprehensive schedule of customer-focused events in 2020, including its biennial Operators & Suppliers Conference, two Customer...
Milano Prime 2019 traffic growth numbers are more than impressive
Business&General Aviation traffic in Milan in December in the two airports managed by Milano Prime – Linate Prime and Malpensa Prime - registered an overall growth of 19.4%. The gr...


text:
package com.bmapleaf.network;

/**
 * Created by zxm on 2017/06/05.
 */

public interface IMyView extends IView {
    void testOK();
}


text:
Is Divorce Court Dehumanizing?
{2:30 minutes to read} Oftentimes, stereotypically, we think divorcing couples—both the person who decides to divorce and the person who doesn't—are angry or revengeful. Although it's true that those feelings are often present, but really—contrary to popular belief—the overwhelming emotion for both is sadness. Regardless of what has happened, what the story is, sadness prevails as the dominant emotion.
Often it is the process of divorcing that creates rage and deep resentment. It's really the court litigation system that drives people from sad to furious, sad to enraged, sad to revengeful (not so much what happened that led them to decide to divorce, it's the process of divorcing itself). I think this happens through a series of dehumanizing experiences where the parties are forced to make decisions based on criteria that don't necessarily make sense to them. Furthermore, they're forced to make decisions in a time frame that is uncomfortable: either too fast or too slow. They're forced to go through processes that involve a lot of hurrying and then waiting in a setting that isn't what they would necessarily choose.
The system is also dehumanizing for lawyers. Although our clients might disagree, nobody goes into family law for the money. Lawyers enter this field because of an impulse to help families. We all went into this area because we wanted to work directly with people. But we often find ourselves stuck between what clients would like to do and the court system. We too become dehumanized over time. We become distanced from what originally motivated us to work with people in conflict. We become distanced from our own sense of humanity and compassion. That's what the court system requires in order to work.
I think it's heartbreaking how a court system meant to help people resolve conflict is so often inefficient in actually accomplishing that. Honestly, I cannot understand why mediation and collaborative law are not the default choices for divorcing families. Mediation and collaborative law permit those people who want to find a humane resolution to a heartbreaking problem to work together with dignity and respect to find solutions that make sense to them.


Book corners connect two trusses and provide variable angular adjustment.
Use with CON70U half connectors, 8 sets are included.
Book corners are not structural components and require support on both sides of the frame to ensure safe operation.