Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #186 (2018-06-04 17:45:11)

[DEV] Add base module for objects, parsers, and samplers
Added a `base.py` file to `serpentTools/objects`, `serpentTools/parsers`, and `serpentTools/samplers`. These modules contain the base classes from which containers, readers, and samplers inherit, respectively.

## But why

1. Using the abstract base classes (shoutout to @gridley for pointing them out. Should have listened then :man_shrugging:) is better from an object-oriented programming perspective. Now, no reader can be created unless the read, post-check, and pre-check methods are explicitly written for that reader. This is verified with the simple `test_meta.py` tests
1. This allows us to place things that people actually care about in easier places from which to import. I'm working on a revision that builds off of this that allows the user to import all readers and samplers right from the main package, e.g. `from serpentTools import DepletionReader, DetectorSampler`. This is due to a better, non-circular import chain.
1. In closing #184, I plan on doing a fair amount of subclassing and extending the DetectorBase object, and having the base objects in a separate location was cleaner


Patch:
diff --git a/serpentTools/objects/__init__.py b/serpentTools/objects/__init__.py
index e416a5e..05c59d2 100644
--- a/serpentTools/objects/__init__.py
+++ b/serpentTools/objects/__init__.py
@@ -1,13 +1,6 @@
 """Objects used to support the parsing."""
 
-
-class NamedObject(object):
-    """Class for named objects like materials and detectors."""
-
-    def __init__(self, name):
-        self.name = name
-
-    def __str__(self):
-        return '<{} {}>'.format(self.__class__.__name__, self.name)
+from serpentTools.objects.containers import (
+        HomogUniv, Detector, BranchContainer)
 
 
diff --git a/serpentTools/objects/base.py b/serpentTools/objects/base.py
new file mode 100644
index 0000000..149be60
--- /dev/null
+++ b/serpentTools/objects/base.py
@@ -0,0 +1,413 @@
+"""
+Collection of base classes from which other objects inherit.
+"""
+from abc import ABCMeta, abstractmethod
+
+from six import add_metaclass
+
+from numpy import arange, hstack, log, divide
+from matplotlib.pyplot import axes
+
+from serpentTools.messages import debug, warning, SerpentToolsException
+from serpentTools.plot import (
+        plot, magicPlotDocDecorator, formatPlot, cartMeshPlot)
+
+
+class NamedObject(object):
+    """Class for named objects like materials and detectors."""
+
+    def __init__(self, name):
+        self.name = name
+
+    def __str__(self):
+        return '<{} {}>'.format(self.__class__.__name__, self.name)
+
+
+@add_metaclass(ABCMeta)
+class DetectorBase(NamedObject):
+    """
+    Base class for classes that store detector data
+
+    Parameters
+    ----------
+    {params:s}
+
+    Attributes
+    ----------
+    {attrs:s}
+    """
+
+    baseParams = """name: str
+        Name of this detector"""
+    baseAttrs = """grids: dict
+        Dictionary with additional data describing energy grids or mesh points
+    tallies: None or numpy.array
+        Reshaped tally data to correspond to the bins used
+    errors: None or numpy.array
+        Reshaped relative error data corresponding to bins used
+    scores: None or numpy.array
+        Reshaped array of tally scores. SERPENT 1 only
+    indexes: None or OrderedDict
+        Collection of unique indexes for each requested bin"""
+    __doc__ = __doc__.format(params=baseParams, attrs=baseAttrs)
+
+    def __init__(self, name):
+        NamedObject.__init__(self, name)
+        self.tallies = None
+        self.errors = None
+        self.scores = None
+        self.grids = {}
+        self.indexes = None
+        self._map = None
+
+    @abstractmethod
+    def _isReshaped(self):
+        raise NotImplementedError
+
+    def slice(self, fixed, data='tallies'):
+        """
+        Return a view of the reshaped array where certain axes are fixed
+
+        Parameters
+        ----------
+        fixed: dict
+            dictionary to aid in the restriction on the multidimensional
+            array. Keys correspond to the various grids present in
+            ``indexes`` while the values are used to
+        data: {'tallies', 'errors', 'scores'}
+            Which data set to slice
+
+        Returns
+        -------
+        numpy.array
+            View into the respective data where certain dimensions
+            have been removed
+
+        Raises
+        ------
+        SerpentToolsException
+            If the data has not been reshaped or is None [e.g. scores]
+        KeyError
+            If the data set to slice not in the allowed selection
+
+        """
+        if not self._isReshaped():
+            raise SerpentToolsException(
+                'Slicing requires detector to be reshaped')
+        if data not in self._map:
+            raise KeyError(
+                'Data argument {} not in allowed options'
+                '\n{}'.format(data, ', '.join(self._map.keys())))
+        work = self._map[data]
+        if work is None:
+            raise SerpentToolsException(
+                '{} data for detector {} is None. '
+                'Cannot perform slicing'.format(data, self.name))
+        if not fixed:
+            return work
+        return work[self._getSlices(fixed)]
+
+    def _getSlices(self, fixed):
+        """
+        Return a list of slice operators for each axis in reshaped data
+
+        Parameters
+        ----------
+        fixed: dict
+            Dictionary where keys are strings pointing to dimensions in
+        """
+        fixed = fixed if fixed is not None else {}
+        keys = set(fixed.keys())
+        slices = []
+        for key in self.indexes:
+            if key in keys:
+                slices.append(fixed[key])
+                keys.remove(key)
+            else:
+                slices.append(slice(0, len(self.indexes[key])))
+        if any(keys):
+            warning(
+                'Could not find arguments in index that match the following'
+                ' requested slice keys: {}'.format(', '.join(keys)))
+        return slices
+
+    @magicPlotDocDecorator
+    def spectrumPlot(self, fixed=None, ax=None, normalize=True, xlabel=None,
+                     ylabel=None, steps=True, logx=True, logy=False, 
+                     loglog=False, sigma=3, labels=None, legend=False, ncol=1, 
+                     title=None, **kwargs):
+        """
+        Quick plot of the detector value as a function of energy.
+
+        Parameters
+        ----------
+        fixed: None or dict
+            Dictionary controlling the reduction in data
+        {ax}
+        normalize: bool
+            Normalize quantities per unit lethargy
+        {xlabel}
+        {ylabel}
+        steps: bool
+            Plot tally as constant inside bin
+        {logx}
+        {logy}
+        {loglog}
+        {sigma}
+        {labels}
+        {legend}
+        {ncol}
+        {title}
+        {kwargs} :py:func:`matplotlib.pyplot.plot` or 
+            :py:func:`matplotlib.pyplot.errorbar`
+
+        Returns
+        -------
+        {rax}
+
+        Raises
+        ------
+        SerpentToolsException
+            if number of rows in data not equal to
+            number of energy groups
+
+        See Also
+        --------
+        :py:meth:`~serpentTools.objects.containers.Detector.slice`
+        """
+        slicedTallies = self.slice(fixed, 'tallies').copy()
+        if len(slicedTallies.shape) > 2:
+            raise SerpentToolsException(
+                'Sliced data cannot exceed 2-D for spectrum plot, not '
+                '{}'.format(slicedTallies.shape)
+            )
+        elif len(slicedTallies.shape) == 1:
+            slicedTallies = slicedTallies.reshape(slicedTallies.size, 1)
+        lowerE = self.grids['E'][:, 0]
+        if normalize:
+            lethBins = log(
+                divide(self.grids['E'][:, -1], lowerE))
+            for indx in range(slicedTallies.shape[1]):
+                scratch = divide(slicedTallies[:, indx], lethBins)
+                slicedTallies[:, indx] = scratch / scratch.max() 
+
+        if steps:
+            if 'drawstyle' in kwargs:
+                debug('Defaulting to drawstyle specified in kwargs as {}'
+                      .format(kwargs['drawstyle']))
+            else:
+                kwargs['drawstyle'] = 'steps-post'
+
+        if sigma:
+            slicedErrors = sigma * self.slice(fixed, 'errors').copy()
+            slicedErrors = slicedErrors.reshape(slicedTallies.shape) 
+            slicedErrors *= slicedTallies
+        else:
+            slicedErrors = None
+        ax = plot(lowerE, slicedTallies, ax=ax, labels=labels, 
+                  yerr=slicedErrors, **kwargs)     
+        if ylabel is None:
+            ylabel = 'Tally data'
+            ylabel += ' normalized per unit lethargy' if normalize else ''
+            ylabel += ' $\pm${}$\sigma$'.format(sigma) if sigma else ''
+
+        ax = formatPlot(ax, loglog=loglog, logx=logx, ncol=ncol,
+                        xlabel=xlabel or "Energy [MeV]", ylabel=ylabel, 
+                        legend=legend, title=title)
+        return ax
+
+    @magicPlotDocDecorator
+    def plot(self, xdim=None, what='tallies', sigma=None, fixed=None, ax=None,
+             xlabel=None, ylabel=None, steps=False, labels=None, logx=False, 
+             logy=False, loglog=False, legend=False, ncol=1, title=None,
+             **kwargs):
+        """
+        Simple plot routine for 1- or 2-D data
+
+        Parameters
+        ----------
+        xdim: None or str
+            If not None, use the array under this key in ``indexes`` as
+            the x axis
+        what: {'tallies', 'errors', 'scores'}
+            Primary data to plot
+        {sigma}
+        fixed: None or dict
+            Dictionary controlling the reduction in data down to one dimension
+        {ax}
+        {xlabel} If ``xdim`` is given and ``xlabel`` is ``None``, then 
+            ``xdim`` will be applied to the x-axis.
+        {ylabel}
+        steps: bool
+            If true, plot the data as constant inside the respective bins.
+            Sets ``drawstyle`` to be ``steps-post`` unless ``drawstyle``
+            given in ``kwargs``
+        {labels}
+        {logx}
+        {logy}
+        {loglog}
+        {legend}
+        {ncol}
+        {title}
+        {kwargs} :py:func:`~matplotlib.pyplot.plot` or
+            :py:func:`~matplotlib.pyplot.errorbar` function.
+
+        Returns
+        -------
+        {rax}
+
+        Raises
+        ------
+        SerpentToolsException
+            If data contains more than 2 dimensions
+
+        See Also
+        --------
+        * :py:meth:`~serpentTools.objects.containers.Detector.slice`
+        * :py:meth:`~serpentTools.objects.containers.Detector.spectrumPlot`
+           better options for plotting energy spectra
+        """
+
+        data = self.slice(fixed, what)
+        if len(data.shape) > 2:
+            raise SerpentToolsException(
+                'Data must be constrained to 1- or 2-D, not {}'
+                .format(data.shape))
+        elif len(data.shape) == 1:
+            data = data.reshape(data.size, 1)
+
+        if sigma:
+            if what != 'errors':
+                yerr = (self.slice(fixed, 'errors').reshape(data.shape)
+                        * data * sigma)
+            else:
+                warning(
+                    'Will not plot error bars on the error plot. Data to be '
+                    'plotted: {}.  Sigma: {}'.format(what, sigma))
+                yerr = None
+        else: 
+            yerr = None
+
+        xdata, autoX = self._getPlotXData(xdim, data)
+        xlabel = xlabel or autoX
+        ylabel = ylabel or "Tally data" 
+        ax = ax or axes()
+
+        if steps:
+            if 'drawstyle' in kwargs:
+                debug('Defaulting to drawstyle specified in kwargs as {}'
+                      .format(kwargs['drawstyle']))
+            else:
+                kwargs['drawstyle'] = 'steps-post'
+        ax = plot(xdata, data, ax, labels, yerr, **kwargs)
+        ax = formatPlot(ax, loglog=loglog, logx=logx, logy=logy, ncol=ncol,
+                        xlabel=xlabel, ylabel=ylabel, legend=legend, 
+                        title=title)
+        return ax
+
+    @magicPlotDocDecorator
+    def meshPlot(self, xdim, ydim, what='tallies', fixed=None, ax=None,
+                 cmap=None, logColor=False, xlabel=None, ylabel=None, 
+                 logx=False, logy=False, loglog=False, title=None, **kwargs):
+        """
+        Plot tally data as a function of two mesh dimensions
+
+        Parameters
+        ----------
+        xdim: str
+            Primary dimension - will correspond to x-axis on plot
+        ydim: str
+            Secondary dimension - will correspond to y-axis on plot
+        what: {'tallies', 'errors', 'scores'}
+            Color meshes from tally data, uncertainties, or scores
+        fixed: None or dict
+            Dictionary controlling the reduction in data down to one dimension
+        {ax}
+        {cmap}
+        logColor: bool
+            If true, apply a logarithmic coloring to the data positive 
+            data
+        {xlabel}
+        {ylabel}
+        {logx}
+        {logy}
+        {loglog}
+        {title}
+        {kwargs} :py:func:`~matplotlib.pyplot.pcolormesh`
+
+        Returns
+        -------
+        {rax}
+
+        Raises
+        ------
+        SerpentToolsException
+            If data to be plotted, with or without constraints, is not 1D
+        KeyError
+            If the data set by ``what`` not in the allowed selection
+        ValueError
+            If the data contains negative quantities and ``logColor`` is
+            ``True``
+
+        See Also
+        --------
+        * :py:meth:`~serpentTools.objects.containers.Detector.slice`
+        * :py:func:`matplotlib.pyplot.pcolormesh`
+        * :py:func:`~serpentTools.plot.cartMeshPlot`
+        """
+        if fixed:
+            for qty, name in zip((xdim, ydim), ('x', 'y')):
+                if qty in fixed:
+                    raise SerpentToolsException(
+                        'Requested {} dimension {} is one of the axis to be '
+                        'constrained. '
+                        .format(name, qty))
+
+        data = self.slice(fixed, what)
+        dShape = data.shape
+        if len(dShape) != 2:
+            raise SerpentToolsException(
+                'Data must be 2D for mesh plot, currently is {}.\nConstraints:'
+                '{}'.format(dShape, fixed)
+            )
+        xgrid = self._getGrid(xdim)
+        ygrid = self._getGrid(ydim)
+        if data.shape != (ygrid.size - 1, xgrid.size - 1):
+            data = data.T
+
+        ax = cartMeshPlot(data, xgrid, ygrid, ax, cmap, logColor, **kwargs)
+        ax = formatPlot(ax, loglog=loglog, logx=logx, logy=logy,
+                        xlabel=xlabel or xdim, ylabel=ylabel or ydim,
+                        title=title)
+        return ax
+
+    def _getGrid(self, qty):
+        grids = self._inGridsAs(qty)
+        if grids is not None:
+            lowBounds = grids[:, 0]
+            return hstack((lowBounds, grids[-1, 1]))
+        if qty not in self.indexes:
+            raise KeyError("No index {} found on detector. Bin indexes: {}"
+                           .format(qty, ', '.join(self.indexes.keys())))
+        bins = self.indexes[qty]
+        return hstack((bins, len(bins)))
+
+    def _dimInGrids(self, key):
+        return key[0].upper() in self.grids
+
+    def _inGridsAs(self, qty):
+        if self._dimInGrids(qty):
+            return self.grids[qty[0].upper()]
+        return None
+
+    def _getPlotXData(self, qty, ydata):
+        fallback = arange(len(ydata)), 'Bin Index'
+        if qty is None:
+            return fallback
+        xdata = self._inGridsAs(qty)
+        if xdata is not None:
+            return xdata[:, 0], qty
+        if qty in self.indexes:
+            return self.indexes[qty], qty
+        return fallback
+
diff --git a/serpentTools/objects/containers.py b/serpentTools/objects/containers.py
index 75f7e6f..1ecaf1a 100644
--- a/serpentTools/objects/containers.py
+++ b/serpentTools/objects/containers.py
@@ -5,7 +5,6 @@ Contents
 --------
 * :py:class:`~serpentTools.objects.containers.HomogUniv`
 * :py:class:`~serpentTools.objects.containers.BranchContainer`
-* :py:class:`~serpentTools.objects.containers.DetectorBase`
 * :py:class:`~serpentTools.objects.containers.Detector`
 
 """
@@ -21,7 +20,7 @@ from numpy import (array, arange, unique, log, divide, ones_like, hstack,
 from serpentTools.settings import rc
 from serpentTools.plot import (cartMeshPlot, plot, magicPlotDocDecorator,
                                formatPlot)
-from serpentTools.objects import NamedObject
+from serpentTools.objects.base import NamedObject, DetectorBase
 from serpentTools.utils import convertVariableName
 from serpentTools.messages import warning, SerpentToolsException, debug, info
 
@@ -449,387 +448,6 @@ class HomogUniv(NamedObject):
 
     hasData = __bool__
 
-class DetectorBase(NamedObject):
-    """
-    Base class for classes that store detector data
-
-    Parameters
-    ----------
-    {params:s}
-
-    Attributes
-    ----------
-    {attrs:s}
-    """
-
-    baseParams = """name: str
-        Name of this detector"""
-    baseAttrs = """grids: dict
-        Dictionary with additional data describing energy grids or mesh points
-    tallies: None or numpy.array
-        Reshaped tally data to correspond to the bins used
-    errors: None or numpy.array
-        Reshaped relative error data corresponding to bins used
-    scores: None or numpy.array
-        Reshaped array of tally scores. SERPENT 1 only
-    indexes: None or OrderedDict
-        Collection of unique indexes for each requested bin"""
-    __doc__ = __doc__.format(params=baseParams, attrs=baseAttrs)
-
-    def __init__(self, name):
-        NamedObject.__init__(self, name)
-        self.tallies = None
-        self.errors = None
-        self.scores = None
-        self.grids = {}
-        self.indexes = None
-        self._map = None
-
-    def _isReshaped(self):
-        raise NotImplementedError
-
-    def slice(self, fixed, data='tallies'):
-        """
-        Return a view of the reshaped array where certain axes are fixed
-
-        Parameters
-        ----------
-        fixed: dict
-            dictionary to aid in the restriction on the multidimensional
-            array. Keys correspond to the various grids present in
-            ``indexes`` while the values are used to
-        data: {'tallies', 'errors', 'scores'}
-            Which data set to slice
-
-        Returns
-        -------
-        numpy.array
-            View into the respective data where certain dimensions
-            have been removed
-
-        Raises
-        ------
-        SerpentToolsException
-            If the data has not been reshaped or is None [e.g. scores]
-        KeyError
-            If the data set to slice not in the allowed selection
-
-        """
-        if not self._isReshaped():
-            raise SerpentToolsException(
-                'Slicing requires detector to be reshaped')
-        if data not in self._map:
-            raise KeyError(
-                'Data argument {} not in allowed options'
-                '\n{}'.format(data, ', '.join(self._map.keys())))
-        work = self._map[data]
-        if work is None:
-            raise SerpentToolsException(
-                '{} data for detector {} is None. '
-                'Cannot perform slicing'.format(data, self.name))
-        if not fixed:
-            return work
-        return work[self._getSlices(fixed)]
-
-    def _getSlices(self, fixed):
-        """
-        Return a list of slice operators for each axis in reshaped data
-
-        Parameters
-        ----------
-        fixed: dict
-            Dictionary where keys are strings pointing to dimensions in
-        """
-        fixed = fixed if fixed is not None else {}
-        keys = set(fixed.keys())
-        slices = []
-        for key in self.indexes:
-            if key in keys:
-                slices.append(fixed[key])
-                keys.remove(key)
-            else:
-                slices.append(slice(0, len(self.indexes[key])))
-        if any(keys):
-            warning(
-                'Could not find arguments in index that match the following'
-                ' requested slice keys: {}'.format(', '.join(keys)))
-        return slices
-    
-    @magicPlotDocDecorator
-    def spectrumPlot(self, fixed=None, ax=None, normalize=True, xlabel=None,
-                     ylabel=None, steps=True, logx=True, logy=False, loglog=False, 
-                     sigma=3, labels=None, legend=False, ncol=1, title=None, 
-                     **kwargs):
-        """
-        Quick plot of the detector value as a function of energy.
-
-        Parameters
-        ----------
-        fixed: None or dict
-            Dictionary controlling the reduction in data
-        {ax}
-        normalize: bool
-            Normalize quantities per unit lethargy
-        {xlabel}
-        {ylabel}
-        steps: bool
-            Plot tally as constant inside bin
-        {logx}
-        {logy}
-        {loglog}
-        {sigma}
-        {labels}
-        {legend}
-        {ncol}
-        {title}
-        {kwargs} :py:func:`matplotlib.pyplot.plot` or 
-            :py:func:`matplotlib.pyplot.errorbar`
-
-        Returns
-        -------
-        {rax}
-
-        Raises
-        ------
-        SerpentToolsException
-            if number of rows in data not equal to
-            number of energy groups
-
-        See Also
-        --------
-        :py:meth:`~serpentTools.objects.containers.Detector.slice`
-        """
-        slicedTallies = self.slice(fixed, 'tallies').copy()
-        if len(slicedTallies.shape) > 2:
-            raise SerpentToolsException(
-                'Sliced data cannot exceed 2-D for spectrum plot, not '
-                '{}'.format(slicedTallies.shape)
-            )
-        elif len(slicedTallies.shape) == 1:
-            slicedTallies = slicedTallies.reshape(slicedTallies.size, 1)
-        lowerE = self.grids['E'][:, 0]
-        if normalize:
-            lethBins = log(
-                divide(self.grids['E'][:, -1], lowerE))
-            for indx in range(slicedTallies.shape[1]):
-                scratch = divide(slicedTallies[:, indx], lethBins)
-                slicedTallies[:, indx] = scratch / scratch.max() 
-        
-        if steps:
-            if 'drawstyle' in kwargs:
-                debug('Defaulting to drawstyle specified in kwargs as {}'
-                      .format(kwargs['drawstyle']))
-            else:
-                kwargs['drawstyle'] = 'steps-post'
-        
-        if sigma:
-            slicedErrors = sigma * self.slice(fixed, 'errors').copy()
-            slicedErrors = slicedErrors.reshape(slicedTallies.shape) * slicedTallies
-        else:
-            slicedErrors = None
-        ax = plot(lowerE, slicedTallies, ax=ax, labels=labels, yerr=slicedErrors, **kwargs)     
-        if ylabel is None:
-            ylabel = 'Tally data'
-            ylabel += ' normalized per unit lethargy' if normalize else ''
-            ylabel += ' $\pm${}$\sigma$'.format(sigma) if sigma else ''
-        
-        ax = formatPlot(ax, loglog=loglog, logx=logx, ncol=ncol,
-                        xlabel=xlabel or "Energy [MeV]", ylabel=ylabel, 
-                        legend=legend, title=title)
-        return ax
-    
-    @magicPlotDocDecorator
-    def plot(self, xdim=None, what='tallies', sigma=None, fixed=None, ax=None,
-             xlabel=None, ylabel=None, steps=False, labels=None, logx=False, 
-             logy=False, loglog=False, legend=False, ncol=1, title=None,
-             **kwargs):
-        """
-        Simple plot routine for 1- or 2-D data
-
-        Parameters
-        ----------
-        xdim: None or str
-            If not None, use the array under this key in ``indexes`` as
-            the x axis
-        what: {'tallies', 'errors', 'scores'}
-            Primary data to plot
-        {sigma}
-        fixed: None or dict
-            Dictionary controlling the reduction in data down to one dimension
-        {ax}
-        {xlabel} If ``xdim`` is given and ``xlabel`` is ``None``, then 
-            ``xdim`` will be applied to the x-axis.
-        {ylabel}
-        steps: bool
-            If true, plot the data as constant inside the respective bins.
-            Sets ``drawstyle`` to be ``steps-post`` unless ``drawstyle``
-            given in ``kwargs``
-        {labels}
-        {logx}
-        {logy}
-        {loglog}
-        {legend}
-        {ncol}
-        {title}
-        {kwargs} :py:func:`~matplotlib.pyplot.plot` or
-            :py:func:`~matplotlib.pyplot.errorbar` function.
-
-        Returns
-        -------
-        {rax}
-
-        Raises
-        ------
-        SerpentToolsException
-            If data contains more than 2 dimensions
-
-        See Also
-        --------
-        * :py:meth:`~serpentTools.objects.containers.Detector.slice`
-        * :py:meth:`~serpentTools.objects.containers.Detector.spectrumPlot`
-           better options for plotting energy spectra
-        """
-
-        data = self.slice(fixed, what)
-        if len(data.shape) > 2:
-            raise SerpentToolsException(
-                'Data must be constrained to 1- or 2-D, not {}'.format(data.shape))
-        elif len(data.shape) == 1:
-            data = data.reshape(data.size, 1)
-
-        if sigma:
-            if what != 'errors':
-                yerr = self.slice(fixed, 'errors').reshape(data.shape) * data * sigma
-            else:
-                warning(
-                    'Will not plot error bars on the error plot. Data to be '
-                    'plotted: {}.  Sigma: {}'.format(what, sigma))
-                yerr = None
-        else: 
-           yerr = None
-
-        xdata, autoX = self._getPlotXData(xdim, data)
-        xlabel = xlabel or autoX
-        ylabel = ylabel or "Tally data" 
-        ax = ax or pyplot.axes()
-        
-        if steps:
-            if 'drawstyle' in kwargs:
-                debug('Defaulting to drawstyle specified in kwargs as {}'
-                      .format(kwargs['drawstyle']))
-            else:
-                kwargs['drawstyle'] = 'steps-post'
-        ax = plot(xdata, data, ax, labels, yerr,**kwargs)
-        ax = formatPlot(ax, loglog=loglog, logx=logx, logy=logy, ncol=ncol,
-                        xlabel=xlabel, ylabel=ylabel, legend=legend, 
-                        title=title)
-        return ax
-
-    @magicPlotDocDecorator
-    def meshPlot(self, xdim, ydim, what='tallies', fixed=None, ax=None,
-                 cmap=None, logColor=False, xlabel=None, ylabel=None, 
-                 logx=False, logy=False, loglog=False, title=None, **kwargs):
-        """
-        Plot tally data as a function of two mesh dimensions
-
-        Parameters
-        ----------
-        xdim: str
-            Primary dimension - will correspond to x-axis on plot
-        ydim: str
-            Secondary dimension - will correspond to y-axis on plot
-        what: {'tallies', 'errors', 'scores'}
-            Color meshes from tally data, uncertainties, or scores
-        fixed: None or dict
-            Dictionary controlling the reduction in data down to one dimension
-        {ax}
-        {cmap}
-        logColor: bool
-            If true, apply a logarithmic coloring to the data positive 
-            data
-        {xlabel}
-        {ylabel}
-        {logx}
-        {logy}
-        {loglog}
-        {title}
-        {kwargs} :py:func:`~matplotlib.pyplot.pcolormesh`
-
-        Returns
-        -------
-        {rax}
-
-        Raises
-        ------
-        SerpentToolsException
-            If data to be plotted, with or without constraints, is not 1D
-        KeyError
-            If the data set by ``what`` not in the allowed selection
-        ValueError
-            If the data contains negative quantities and ``logColor`` is
-            ``True``
-
-        See Also
-        --------
-        * :py:meth:`~serpentTools.objects.containers.Detector.slice`
-        * :py:func:`matplotlib.pyplot.pcolormesh`
-        * :py:func:`~serpentTools.plot.cartMeshPlot`
-        """
-        if fixed:
-            for qty, name in zip((xdim, ydim), ('x', 'y')):
-                if qty in fixed:
-                    raise SerpentToolsException(
-                        'Requested {} dimension {} is one of the axis to be constrained. '
-                        .format(name, qty))
-
-        data = self.slice(fixed, what)
-        dShape = data.shape
-        if len(dShape) != 2:
-            raise SerpentToolsException(
-                'Data must be 2D for mesh plot, currently is {}.\nConstraints:'
-                '{}'.format(dShape, fixed)
-            )
-        xgrid = self._getGrid(xdim)
-        ygrid = self._getGrid(ydim)
-        if data.shape != (ygrid.size - 1, xgrid.size - 1):
-            data = data.T
-
-        ax = cartMeshPlot(data, xgrid, ygrid, ax, cmap, logColor, **kwargs)
-        ax = formatPlot(ax, loglog=loglog, logx=logx, logy=logy,
-                        xlabel=xlabel or xdim, ylabel=ylabel or ydim,
-                        title=title)
-        return ax
-
-    def _getGrid(self, qty):
-        grids = self._inGridsAs(qty)
-        if grids is not None:
-            lowBounds = grids[:, 0]
-            return hstack((lowBounds, grids[-1, 1]))
-        if qty not in self.indexes:
-            raise KeyError("No index {} found on detector. Bin indexes: {}"
-                           .format(qty, ', '.join(self.indexes.keys())))
-        bins = self.indexes[qty]
-        return hstack((bins, len(bins)))
-
-    def _dimInGrids(self, key):
-        return key[0].upper() in self.grids
-
-    def _inGridsAs(self, qty):
-        if self._dimInGrids(qty):
-            return self.grids[qty[0].upper()]
-        return None
-
-    def _getPlotXData(self, qty, ydata):
-        fallback = arange(len(ydata)), 'Bin Index'
-        if qty is None:
-            return fallback
-        xdata = self._inGridsAs(qty)
-        if xdata is not None:
-            return xdata[:, 0], qty
-        if qty in self.indexes:
-            return self.indexes[qty], qty
-        return fallback
-
 
 class Detector(DetectorBase):
     """
diff --git a/serpentTools/objects/materials.py b/serpentTools/objects/materials.py
index e0f4ff5..55afc80 100644
--- a/serpentTools/objects/materials.py
+++ b/serpentTools/objects/materials.py
@@ -5,7 +5,7 @@ from matplotlib import pyplot
 
 from serpentTools.messages import warning, debug
 from serpentTools.plot import magicPlotDocDecorator, formatPlot, DEPLETION_PLOT_LABELS
-from serpentTools.objects import NamedObject
+from serpentTools.objects.base import NamedObject
 from serpentTools.utils import convertVariableName
 
 
diff --git a/serpentTools/objects/xsdata.py b/serpentTools/objects/xsdata.py
index f45a2d7..ba33337 100644
--- a/serpentTools/objects/xsdata.py
+++ b/serpentTools/objects/xsdata.py
@@ -3,7 +3,7 @@ import numpy as np
 from matplotlib import pyplot
 
 from serpentTools import messages
-from serpentTools.objects import NamedObject
+from serpentTools.objects.base import NamedObject
 from serpentTools.utils import convertVariableName
 from serpentTools.plot import magicPlotDocDecorator, formatPlot 
 
diff --git a/serpentTools/parsers/__init__.py b/serpentTools/parsers/__init__.py
index 12837f8..df7eea2 100644
--- a/serpentTools/parsers/__init__.py
+++ b/serpentTools/parsers/__init__.py
@@ -121,7 +121,7 @@ def read(filePath, reader='infer'):
 
     Returns
     -------
-    serpentTools.objects.readers.BaseReader
+    serpentTools.objects.base.BaseReader
         Correct subclass corresponding to the file type
 
     Raises
diff --git a/serpentTools/objects/readers.py b/serpentTools/parsers/base.py
similarity index 89%
rename from serpentTools/objects/readers.py
rename to serpentTools/parsers/base.py
index 0f5c997..d0721b3 100644
--- a/serpentTools/objects/readers.py
+++ b/serpentTools/parsers/base.py
@@ -1,7 +1,20 @@
+"""
+Base module for readers.
+
+Contains the abstract base class upon which all readers
+are built.
+"""
+
+from abc import ABCMeta, abstractmethod
+
+from six import add_metaclass
+
+from serpentTools.messages import debug, info
 from serpentTools.settings import rc
-from serpentTools.messages import info
 
-class BaseReader:
+
+@add_metaclass(ABCMeta)
+class BaseReader(object):
     """Parent class from which all parsers will inherit.
 
     Parameters
@@ -35,6 +48,7 @@ class BaseReader:
         info("  - done")
         self._postcheck()
 
+    @abstractmethod
     def _read(self):
         """Read the file and store the data.
 
@@ -43,14 +57,16 @@ class BaseReader:
             This read function has not been implemented yet
 
         """
-        raise NotImplementedError
+        pass
 
+    @abstractmethod
     def _precheck(self):
         """Pre-checking, e.g., make sure Serpent did not
         exit abnormally, or disk ran out of space while parsing.
         """
         pass
 
+    @abstractmethod
     def _postcheck(self):
         """Make sure data looks reasonable. Could possibly check for
         negative cross sections, negative material densitites, etc (which
@@ -92,3 +108,4 @@ class XSReader(BaseReader):
                 self.settings['variables']):
             return True
         return False
+
diff --git a/serpentTools/parsers/branching.py b/serpentTools/parsers/branching.py
index 3d85b93..a91b083 100644
--- a/serpentTools/parsers/branching.py
+++ b/serpentTools/parsers/branching.py
@@ -5,7 +5,7 @@ from numpy import array
 
 from serpentTools.utils import splitValsUncs
 from serpentTools.objects.containers import BranchContainer
-from serpentTools.objects.readers import XSReader
+from serpentTools.parsers.base import XSReader
 from serpentTools.messages import debug, info, error, warning, willChange
 
 class BranchingReader(XSReader):
diff --git a/serpentTools/parsers/bumat.py b/serpentTools/parsers/bumat.py
index b335dd9..d0413da 100644
--- a/serpentTools/parsers/bumat.py
+++ b/serpentTools/parsers/bumat.py
@@ -1,6 +1,6 @@
 """Parser responsible for reading the ``*bumat<n>.m`` files"""
 
-from serpentTools.objects.readers import MaterialReader
+from serpentTools.parsers.base import MaterialReader
 
 
 class BumatReader(MaterialReader):
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 7effff4..9669317 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -8,7 +8,7 @@ from six import iteritems
 from serpentTools.plot import magicPlotDocDecorator, formatPlot, DEPLETION_PLOT_LABELS
 from serpentTools.utils import convertVariableName, str2vec
 from serpentTools.engines import KeywordParser
-from serpentTools.objects.readers import MaterialReader
+from serpentTools.parsers.base import MaterialReader
 from serpentTools.objects.materials import DepletedMaterial
 
 from serpentTools.messages import (warning, info, debug, error,
diff --git a/serpentTools/parsers/detector.py b/serpentTools/parsers/detector.py
index 17b8aec..5670496 100644
--- a/serpentTools/parsers/detector.py
+++ b/serpentTools/parsers/detector.py
@@ -5,7 +5,7 @@ from numpy import asfortranarray, empty
 
 from serpentTools.engines import KeywordParser
 from serpentTools.objects.containers import Detector
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 from serpentTools.messages import error, debug, info
 
 NUM_COLS = 12
@@ -29,7 +29,8 @@ class DetectorReader(BaseReader):
     """
     docAttrs = """detectors: dict
         Dictionary where key, value pairs correspond to detector names
-        and their respective ``DetectorObject``"""
+        and their respective :class:`~serpentTools.objects.containers.Detector`
+        representations."""
     __doc__ = __doc__.format(attrs=docAttrs)
 
     def __init__(self, filePath):
diff --git a/serpentTools/parsers/fissionMatrix.py b/serpentTools/parsers/fissionMatrix.py
index 3d9d1f9..5e86d82 100644
--- a/serpentTools/parsers/fissionMatrix.py
+++ b/serpentTools/parsers/fissionMatrix.py
@@ -1,6 +1,6 @@
 """Parser responsible for reading the ``*fmtx<n>.m`` files"""
 
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 
 
 class FissionMatrixReader(BaseReader):
diff --git a/serpentTools/parsers/history.py b/serpentTools/parsers/history.py
index f881ce5..e8b5d7f 100644
--- a/serpentTools/parsers/history.py
+++ b/serpentTools/parsers/history.py
@@ -9,7 +9,7 @@ file
 from numpy import empty, asfortranarray, array
 
 from serpentTools.utils import convertVariableName
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 from serpentTools.engines import KeywordParser
 from serpentTools.messages import warning, error
 
diff --git a/serpentTools/parsers/microxs.py b/serpentTools/parsers/microxs.py
index 5022653..a4b22fd 100644
--- a/serpentTools/parsers/microxs.py
+++ b/serpentTools/parsers/microxs.py
@@ -5,7 +5,7 @@ from numpy import array
 
 from serpentTools.engines import KeywordParser
 from serpentTools.utils import splitValsUncs, str2vec
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 
 from serpentTools.messages import SerpentToolsException
 
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index 8bb2430..70f375a 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -8,7 +8,7 @@ from numpy import array, vstack
 from serpentTools.settings import rc
 from serpentTools.utils import convertVariableName
 from serpentTools.objects.containers import HomogUniv
-from serpentTools.objects.readers import XSReader
+from serpentTools.parsers.base import XSReader
 from serpentTools.utils import str2vec, splitValsUncs
 from serpentTools.messages import (warning, debug, SerpentToolsException)
 
diff --git a/serpentTools/parsers/sensitivity.py b/serpentTools/parsers/sensitivity.py
index f985b33..510163a 100644
--- a/serpentTools/parsers/sensitivity.py
+++ b/serpentTools/parsers/sensitivity.py
@@ -12,7 +12,7 @@ from serpentTools.plot import magicPlotDocDecorator, formatPlot
 from serpentTools.engines import KeywordParser
 from serpentTools.messages import warning, SerpentToolsException, critical
 from serpentTools.utils import convertVariableName, str2vec
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 
 
 class SensitivityReader(BaseReader):
diff --git a/serpentTools/parsers/xsplot.py b/serpentTools/parsers/xsplot.py
index 71fc4cf..856fea2 100644
--- a/serpentTools/parsers/xsplot.py
+++ b/serpentTools/parsers/xsplot.py
@@ -2,7 +2,7 @@
 import numpy as np
 from serpentTools.engines import KeywordParser
 from serpentTools.messages import warning, info, debug, error
-from serpentTools.objects.readers import BaseReader
+from serpentTools.parsers.base import BaseReader
 from serpentTools.objects.xsdata import XSData
 
 def _cleanChunk(string):
diff --git a/serpentTools/samplers/__init__.py b/serpentTools/samplers/__init__.py
index b969225..e642beb 100644
--- a/serpentTools/samplers/__init__.py
+++ b/serpentTools/samplers/__init__.py
@@ -1,269 +1,3 @@
-"""
-Package responsible for reading multiple files
-of the same type and obtaining true standard deviations
-"""
-from glob import glob
-from os.path import exists
-
-from six import iteritems
-
-from serpentTools.settings import rc
-from serpentTools.messages import (warning, debug, MismatchedContainersError,
-                                   error, SamplerError, info)
-from serpentTools.objects.readers import BaseReader
-
-MISSING_KEY_FLAG = "<missing>"
-
-SPREAD_PLOT_KWARGS = {
-    'c': 'k',
-    'alpha': 0.5
-}
-
-
-def extendFiles(files):
-    """Return a set of files where some may contain * globs"""
-    out = set()
-    if isinstance(files, str):
-        files = [files]
-    for ff in files:
-        if '*' in ff:
-            unGlob = glob(ff)
-            if not unGlob:
-                warning("No files matched with pattern {}".format(ff))
-                continue
-            for globbed in unGlob:
-                out.add(globbed)
-        else:
-            out.add(ff)
-    return out
-
-
-class Sampler(object):
-    docFiles = """files: str or iterable
-            Single file or iterable (list) of files from which to read.
-            Supports file globs, ``*det0.m`` expands to all files that
-            end with ``det0.m``"""
-    docAttrs = """files: set
-        Unordered set containing full paths of unique files read
-    settings: dict
-        Dictionary of sampler-wide settings
-    parsers: set
-        Unordered set of all parsers that were successful
-    map: dict
-        Dictionary where key, value pairs are files and their corresponding
-        parsers"""
-
-    __doc__ = """
-    Base class for reading multiple files of of a similar type
-
-    Parameters
-    ----------
-    {files:s}
-    parser: subclass of BaseReader
-        Class that will be used to read all files
-
-    Attributes
-    ----------
-    {attrs:s}
-
-    Raises
-    ------
-    serpentTools.messages.SamplerError
-        If ``parser`` is not a subclass of ``BaseReader``
-    """.format(files=docFiles, attrs=docAttrs)
-
-    docSkipChecks = """These tests can be skipped by setting
-    ``<sampler.skipPrecheck>`` to be ``False``"""
-
-    def __init__(self, files, parser):
-        if not issubclass(parser, BaseReader):
-            raise SamplerError(
-                "parser argument must be a subclass of BaseReader, not "
-                "{}".format(parser.__name__))
-        self.settings = rc.getReaderSettings('sampler')
-        self.files = extendFiles(files)
-        if not self.files:
-            raise SamplerError("No files stored on Sampler.")
-        self.__parserCls = parser
-        self.parsers = set()
-        self.map = {}
-        self.read()
-
-    def read(self):
-        """Read all the files and create parser objects"""
-        self._readAll()
-        if not self.settings['skipPrecheck']:
-            self._precheck()
-        else:
-            debug('Skipping pre-check')
-        self.process()
-        if self.settings['freeAll']:
-            info("Removing all parsers and containers from memory since "
-                 "setting <sampler.freeAll> is ``True``")
-            self.free()
-
-    def _readAll(self):
-        self.map = {}
-        self.parsers = set()
-        allExistFlag = self.settings['allExist']
-        missing = []
-        raiseErrors = self.settings['raiseErrors']
-        for filePath in self.files:
-            if not exists(filePath):
-                if allExistFlag:
-                    raise OSError('File {} does not exist'
-                                  .format(filePath))
-                else:
-                    missing.append(filePath)
-                continue
-            try:
-                parser = self.__parserCls(filePath)
-                parser.read()
-            except Exception as ee:
-                if raiseErrors:
-                    raise ee
-                else:
-                    error('The following error occurred while reading file {} '
-                          'and was suppressed since setting <raiseErrors> is '
-                          'True}:\n{}'.format(filePath, str(ee)))
-                    continue
-            self.parsers.add(parser)
-            self.map[filePath] = parser
-        if missing:
-            warning('The following files did not exist and were not processed '
-                    '\n\t{}'.format(', '.join(missing)))
-
-    def _checkParserDictKeys(self, attrName, objName=None, sort=True):
-        objName = objName or attrName
-        matches = {}
-        for parser in self.parsers:
-            if not hasattr(parser, attrName):
-                if MISSING_KEY_FLAG not in matches:
-                    matches[MISSING_KEY_FLAG] = {parser.filePath}
-                else:
-                    matches[MISSING_KEY_FLAG].add(parser.filePath)
-                continue
-            d = getattr(parser, attrName)
-            keys = tuple(d.keys())
-            if sort:
-                keys = tuple(sorted(keys))
-            if keys not in matches:
-                matches[keys] = {parser.filePath}
-            else:
-                matches[keys].add(parser.filePath)
-        if len(matches) > 1:
-            self._raiseErrorMsgFromDict(matches, attrName, objName)
-
-    @staticmethod
-    def _raiseErrorMsgFromDict(misMatches, header, objName):
-        msg = 'Files do not contain a consistent set of {}'.format(objName)
-        critMsgs = [msg, header + ": Parser files"]
-        for key, values in iteritems(misMatches):
-            critMsgs.append('{}: {}'.format(key, ', '.join(values)))
-        error('\n'.join(str(item) for item in critMsgs))
-        raise MismatchedContainersError(msg)
-
-    def __iter__(self):
-        for parser in self.parsers:
-            yield parser
-
-    def __len__(self):
-        return len(self.parsers)
-
-    def __contains__(self, item):
-        return item in self.files
-
-    def process(self):
-        """Process the repeated files to obtain true uncertainties"""
-        debug('Processing data from {} parsers'.format(len(self.parsers)))
-        self._process()
-        debug('Done')
-
-    def _process(self):
-        raise NotImplementedError
-
-    def _precheck(self):
-        """Run through a series of checks to make sure data is consistent"""
-        pass
-
-    def free(self):
-        """Remove all parsers and individual containers from memory"""
-        self._free()  # call subclass specific before wiping all parsers
-        self.parsers = set()
-        self.map = {}
-
-    def _free(self):
-        raise NotImplementedError
-
-
-class SampledContainer(object):
-    """
-    Base class for containers that produce averages and deviations from
-    multiple files
-
-    Parameters
-    ----------
-    N: int
-        Number of containers to expect
-    expectedContainer: class
-        What class to expect for all incoming containers
-    """
-
-    docFree = """If ``<sampler.freeAll>`` 
-        is True, then ``free`` will be called after all files have been read 
-        and processed."""
-
-    def __init__(self, N, expectedContainer):
-        self.N = N
-        self._index = 0
-        self.__expectedClass = expectedContainer
-
-    def free(self):
-        pass
-
-    def loadFromContainer(self, container):
-        """
-        Copy data from a similar container.
-
-        Parameters
-        ----------
-        container:
-            Incoming container from which to take data.
-
-        Raises
-        ------
-        serpentTools.messages.MismatchedContainersError
-            If the incoming container is not the expected type
-            declared at construction
-        serpentTools.messages.SamplerError
-            If more containers have been loaded than specified at
-            construction
-
-        """
-        if not isinstance(container, self.__expectedClass):
-            raise MismatchedContainersError(
-                'Sampled container expects {} type containers, not '
-                '{}'.format(self.__expectedClass, type(container)))
-        if self._index == self.N:
-            name = self.name if hasattr(self, 'name') else ''
-            otherName = container.__class__.__name__
-            msg = ("{} {} has already loaded {} {} objects and cannot exceed "
-                   "this bound ".format(self.__class__.__name__, name, self.N,
-                                        otherName))
-            raise SamplerError(msg)
-        self._loadFromContainer(container)
-        self._index += 1
-
-    def _loadFromContainer(self, container):
-        raise NotImplementedError
-
-    def finalize(self):
-        """Produce final uncertainties from all aggregated runs"""
-        if self._index != self.N:
-            warning("Data from only {} of {} files has been loaded".format(
-                self._index, self.N))
-        self._finalize()
-
-    def _finalize(self):
-        raise NotImplementedError
+from serpentTools.samplers.depletion import DepletionSampler
+from serpentTools.samplers.detector import DetectorSampler
 
diff --git a/serpentTools/samplers/base.py b/serpentTools/samplers/base.py
new file mode 100644
index 0000000..a19eafc
--- /dev/null
+++ b/serpentTools/samplers/base.py
@@ -0,0 +1,269 @@
+"""
+Package responsible for reading multiple files
+of the same type and obtaining true standard deviations
+"""
+from glob import glob
+from os.path import exists
+
+from six import iteritems
+
+from serpentTools.settings import rc
+from serpentTools.messages import (warning, debug, MismatchedContainersError,
+                                   error, SamplerError, info)
+from serpentTools.parsers.base import BaseReader
+
+MISSING_KEY_FLAG = "<missing>"
+
+SPREAD_PLOT_KWARGS = {
+    'c': 'k',
+    'alpha': 0.5
+}
+
+
+def extendFiles(files):
+    """Return a set of files where some may contain * globs"""
+    out = set()
+    if isinstance(files, str):
+        files = [files]
+    for ff in files:
+        if '*' in ff:
+            unGlob = glob(ff)
+            if not unGlob:
+                warning("No files matched with pattern {}".format(ff))
+                continue
+            for globbed in unGlob:
+                out.add(globbed)
+        else:
+            out.add(ff)
+    return out
+
+
+class Sampler(object):
+    docFiles = """files: str or iterable
+            Single file or iterable (list) of files from which to read.
+            Supports file globs, ``*det0.m`` expands to all files that
+            end with ``det0.m``"""
+    docAttrs = """files: set
+        Unordered set containing full paths of unique files read
+    settings: dict
+        Dictionary of sampler-wide settings
+    parsers: set
+        Unordered set of all parsers that were successful
+    map: dict
+        Dictionary where key, value pairs are files and their corresponding
+        parsers"""
+
+    __doc__ = """
+    Base class for reading multiple files of of a similar type
+
+    Parameters
+    ----------
+    {files:s}
+    parser: subclass of BaseReader
+        Class that will be used to read all files
+
+    Attributes
+    ----------
+    {attrs:s}
+
+    Raises
+    ------
+    serpentTools.messages.SamplerError
+        If ``parser`` is not a subclass of ``BaseReader``
+    """.format(files=docFiles, attrs=docAttrs)
+
+    docSkipChecks = """These tests can be skipped by setting
+    ``<sampler.skipPrecheck>`` to be ``False``"""
+
+    def __init__(self, files, parser):
+        if not issubclass(parser, BaseReader):
+            raise SamplerError(
+                "parser argument must be a subclass of BaseReader, not "
+                "{}".format(parser.__name__))
+        self.settings = rc.getReaderSettings('sampler')
+        self.files = extendFiles(files)
+        if not self.files:
+            raise SamplerError("No files stored on Sampler.")
+        self.__parserCls = parser
+        self.parsers = set()
+        self.map = {}
+        self.read()
+
+    def read(self):
+        """Read all the files and create parser objects"""
+        self._readAll()
+        if not self.settings['skipPrecheck']:
+            self._precheck()
+        else:
+            debug('Skipping pre-check')
+        self.process()
+        if self.settings['freeAll']:
+            info("Removing all parsers and containers from memory since "
+                 "setting <sampler.freeAll> is ``True``")
+            self.free()
+
+    def _readAll(self):
+        self.map = {}
+        self.parsers = set()
+        allExistFlag = self.settings['allExist']
+        missing = []
+        raiseErrors = self.settings['raiseErrors']
+        for filePath in self.files:
+            if not exists(filePath):
+                if allExistFlag:
+                    raise OSError('File {} does not exist'
+                                  .format(filePath))
+                else:
+                    missing.append(filePath)
+                continue
+            try:
+                parser = self.__parserCls(filePath)
+                parser.read()
+            except Exception as ee:
+                if raiseErrors:
+                    raise ee
+                else:
+                    error('The following error occurred while reading file {} '
+                          'and was suppressed since setting <raiseErrors> is '
+                          'True}:\n{}'.format(filePath, str(ee)))
+                    continue
+            self.parsers.add(parser)
+            self.map[filePath] = parser
+        if missing:
+            warning('The following files did not exist and were not processed '
+                    '\n\t{}'.format(', '.join(missing)))
+
+    def _checkParserDictKeys(self, attrName, objName=None, sort=True):
+        objName = objName or attrName
+        matches = {}
+        for parser in self.parsers:
+            if not hasattr(parser, attrName):
+                if MISSING_KEY_FLAG not in matches:
+                    matches[MISSING_KEY_FLAG] = {parser.filePath}
+                else:
+                    matches[MISSING_KEY_FLAG].add(parser.filePath)
+                continue
+            d = getattr(parser, attrName)
+            keys = tuple(d.keys())
+            if sort:
+                keys = tuple(sorted(keys))
+            if keys not in matches:
+                matches[keys] = {parser.filePath}
+            else:
+                matches[keys].add(parser.filePath)
+        if len(matches) > 1:
+            self._raiseErrorMsgFromDict(matches, attrName, objName)
+
+    @staticmethod
+    def _raiseErrorMsgFromDict(misMatches, header, objName):
+        msg = 'Files do not contain a consistent set of {}'.format(objName)
+        critMsgs = [msg, header + ": Parser files"]
+        for key, values in iteritems(misMatches):
+            critMsgs.append('{}: {}'.format(key, ', '.join(values)))
+        error('\n'.join(str(item) for item in critMsgs))
+        raise MismatchedContainersError(msg)
+
+    def __iter__(self):
+        for parser in self.parsers:
+            yield parser
+
+    def __len__(self):
+        return len(self.parsers)
+
+    def __contains__(self, item):
+        return item in self.files
+
+    def process(self):
+        """Process the repeated files to obtain true uncertainties"""
+        debug('Processing data from {} parsers'.format(len(self.parsers)))
+        self._process()
+        debug('Done')
+
+    def _process(self):
+        raise NotImplementedError
+
+    def _precheck(self):
+        """Run through a series of checks to make sure data is consistent"""
+        pass
+
+    def free(self):
+        """Remove all parsers and individual containers from memory"""
+        self._free()  # call subclass specific before wiping all parsers
+        self.parsers = set()
+        self.map = {}
+
+    def _free(self):
+        raise NotImplementedError
+
+
+class SampledContainer(object):
+    """
+    Base class for containers that produce averages and deviations from
+    multiple files
+
+    Parameters
+    ----------
+    N: int
+        Number of containers to expect
+    expectedContainer: class
+        What class to expect for all incoming containers
+    """
+
+    docFree = """If ``<sampler.freeAll>`` 
+        is True, then ``free`` will be called after all files have been read 
+        and processed."""
+
+    def __init__(self, N, expectedContainer):
+        self.N = N
+        self._index = 0
+        self.__expectedClass = expectedContainer
+
+    def free(self):
+        pass
+
+    def loadFromContainer(self, container):
+        """
+        Copy data from a similar container.
+
+        Parameters
+        ----------
+        container:
+            Incoming container from which to take data.
+
+        Raises
+        ------
+        serpentTools.messages.MismatchedContainersError
+            If the incoming container is not the expected type
+            declared at construction
+        serpentTools.messages.SamplerError
+            If more containers have been loaded than specified at
+            construction
+
+        """
+        if not isinstance(container, self.__expectedClass):
+            raise MismatchedContainersError(
+                'Sampled container expects {} type containers, not '
+                '{}'.format(self.__expectedClass, type(container)))
+        if self._index == self.N:
+            name = self.name if hasattr(self, 'name') else ''
+            otherName = container.__class__.__name__
+            msg = ("{} {} has already loaded {} {} objects and cannot exceed "
+                   "this bound ".format(self.__class__.__name__, name, self.N,
+                                        otherName))
+            raise SamplerError(msg)
+        self._loadFromContainer(container)
+        self._index += 1
+
+    def _loadFromContainer(self, container):
+        raise NotImplementedError
+
+    def finalize(self):
+        """Produce final uncertainties from all aggregated runs"""
+        if self._index != self.N:
+            warning("Data from only {} of {} files has been loaded".format(
+                self._index, self.N))
+        self._finalize()
+
+    def _finalize(self):
+        raise NotImplementedError
+
diff --git a/serpentTools/samplers/depletion.py b/serpentTools/samplers/depletion.py
index 5819a41..eab7d69 100644
--- a/serpentTools/samplers/depletion.py
+++ b/serpentTools/samplers/depletion.py
@@ -13,7 +13,7 @@ from serpentTools.messages import SamplerError, warning
 from serpentTools.plot import magicPlotDocDecorator, formatPlot, DEPLETION_PLOT_LABELS
 from serpentTools.objects.materials import DepletedMaterialBase
 from serpentTools.parsers.depletion import DepletionReader, DepPlotMixin
-from serpentTools.samplers import Sampler, SampledContainer, SPREAD_PLOT_KWARGS
+from serpentTools.samplers.base import Sampler, SampledContainer, SPREAD_PLOT_KWARGS
 
 CONSTANT_MDATA = ('names', 'zai')
 """metadata that should be invariant throughout repeated runs"""
diff --git a/serpentTools/samplers/detector.py b/serpentTools/samplers/detector.py
index 013725f..25271ee 100644
--- a/serpentTools/samplers/detector.py
+++ b/serpentTools/samplers/detector.py
@@ -11,8 +11,8 @@ from serpentTools.messages import (MismatchedContainersError, warning,
                                    SamplerError, SerpentToolsException)
 from serpentTools.plot import magicPlotDocDecorator
 from serpentTools.parsers.detector import DetectorReader
-from serpentTools.objects.containers import DetectorBase
-from serpentTools.samplers import Sampler, SampledContainer, SPREAD_PLOT_KWARGS
+from serpentTools.objects.base import DetectorBase
+from serpentTools.samplers.base import Sampler, SampledContainer, SPREAD_PLOT_KWARGS
 
 class DetectorSampler(Sampler):
     __doc__ = """


Test patch:
diff --git a/serpentTools/tests/test_meta.py b/serpentTools/tests/test_meta.py
new file mode 100644
index 0000000..38f2a9f
--- /dev/null
+++ b/serpentTools/tests/test_meta.py
@@ -0,0 +1,48 @@
+"""
+Test the abstract base class approach upon
+which our objects are built.
+
+Most of this is trivial for a set python build,
+but since we support flavors of 2 and 3, 
+we take advantage of :func:`six.add_metaclass`
+to properly create the abstract 
+base classes.
+
+"""
+
+from unittest import TestCase
+
+from serpentTools.parsers.base import BaseReader
+
+class BadSubclass(BaseReader):
+    """Class that does not overwrite abstract methods."""
+
+    def __init__(self):
+        BaseReader.__init__(self, None, '')
+
+class ProperSubclass(BaseReader):
+    """Class that does overwrite abstract methods."""
+
+    def __init__(self):
+        BaseReader.__init__(self, None, '')
+
+    def _read(self):
+        pass
+
+    def _postcheck(self):
+        pass
+    
+    def _precheck(self):
+        pass
+
+class MetaTester(TestCase):
+    """Class that tests the construction of base classes."""
+
+    def test_badsubclass(self):
+        """Verify that one cannot instantiate incomplete base classes."""
+        with self.assertRaises(TypeError):
+            BadSubclass()
+
+    def test_properSubclass(self):
+        """Verify that one can instantiate completed base classes."""
+        ProperSubclass()