Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #196 (2018-06-18 16:31:15)

[API] Compare results reader and HomogUniv objects
Lots to unpack here, but I will attempt to explain what we have here. 

## Summary
Addresses two items from #150 and provides a lot of infrastructure for future comparisons.

Implemented sub-methods that are used to compare `ResultsReader` and `HomogUniv` objects. These sub-methods call upon new utilities to compare the contents of dictionaries. New logging utilities in `messages.py` report these results to the user. 

## Output files
Below are two files created with the results reader comparison method. Both were generated with the following script:
```
from numpy import arange
import serpentTools
r = serpentTools.read('serpentTools/tests/pwr_res.m')
o = serpentTools.read(r.filePath)

o.metadata.pop('pop')
o.metadata['cycles'] *= 100
o.resdata['impKeff'] = arange(11)
o.resdata['anaKeff'] *= 3
o.resdata['anaAlf'] *= 1.03
r.compare(o, verbosity=<>)
```
with verbosity set to `debug` (show the result of every comparison) and `info` (show the result of everything except where items are exactly identical)
[compare_debug.txt](https://github.com/CORE-GATECH-GROUP/serpent-tools/files/2112031/compare_debug.txt)
[compare_info.txt](https://github.com/CORE-GATECH-GROUP/serpent-tools/files/2112032/compare_info.txt)

The results of comparisons are reported in a way that is *hopefully* easy to understand. A snippet of the outputs is provided below
```
INFO    : serpentTools: Comparing > against < with the following tolerances:
	> <ResultsReader reading serpentTools/tests/pwr_res.m>
	< <ResultsReader reading serpentTools/tests/pwr_res.m>
	Lower tolerance: 0.000 [%]
	Upper tolerance: 10.000 [%]
	Confidence interval for statistical values: 2 sigma or 95 %
ERROR   : serpentTools: metadata from first and second contain different items
	Items present in first but not in second:
		pop
INFO    : serpentTools: The following items will be skipped in the comparison
	completeDate, inputFileName, seed, startDate, title, workingDirectory
ERROR   : serpentTools: Values for cycles are outside acceptable tolerances.
	> [100.]
	< [10000.]
ERROR   : serpentTools: Items from first and second results have different shapes
	impKeff: (2, 2) - (11,)
...
```
## More detail
### Sub-methods

The results reader objects have three public methods that are used in the comparison routine:
* `compareMetadata` - evaluate if *some* metadata are identical. Skipped items are reported using an `info` statement and are things like `seed`, `completeDate`, and `inputFile`, that one might expect to be different w/o breaking the test.
* `compareResults` - evaluate if contents of `resdata` are identical or within statistical tolerance
* `compareUniverses` - for each universe stored in `universes`, call the `universe.compare` with the same input tolerances

For `HomogUniv` objects, we have 
* `compareAttributes` - evaluate if attributes like group structure are identical
* `compare[B1|GC|Inf]Data` - evaluate the contents of `b1`, `gc`, and `inf` expected value and uncertainty dictionaries to see if contents are within tolerances

### Comparison utilities
I separated out the comparison utilities into two categories: functions that simply compare quantities and return boolean or integer status codes, and functions that used these returned values to report to the user. This way, we can perform unit tests (of which I added a lot) without having a lot of extra information presented to the user running the tests. 

The comparison utilities include:
* checking for overlapping statistical confidence intervals
* comparing strings, booleans, scalars, or arrays without uncertainties and using the `lower` and `upper` thresholds for numeric values
* checking the contents of two dictionaries and returning
  * items that are not present in both sets
  * items that point to values of different types
  * items that point to arrays of different shapes

I also include some high-level utilities that combine the comparison and logging into one function. 

### Documentation
I also provided some additional documentation into the developer guide about these comparison and logging functions added.

### Testing
Added ~20 new tests that test a good bit of the inner workings of this PR. I have some hesitations about adding tests for the full comparison method right now, mainly because that would induce a lot of content printed at the moment, but I can extend the above script into some tests if deemed appropriate for this PR.


Patch:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 814f99c..0ccf60b 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -24,5 +24,4 @@ API
    sampler.rst
    sensitivity.rst
    xsplot.rst
-   utils.rst
    microxs.rst
diff --git a/docs/develop/comparisons.rst b/docs/develop/comparisons.rst
index 6e221d2..6170586 100644
--- a/docs/develop/comparisons.rst
+++ b/docs/develop/comparisons.rst
@@ -39,26 +39,11 @@ value being returned from the method.
 
 The ``lower`` and ``upper`` arguments should be used to compare values
 that do not have uncertainties. Both will be ``float`` values, with 
-``lower`` less than or equal to ``upper``. These should be be used
-when comparing the relative differences between quantities from ``self``
-and ``other`` with something like::
-
-   # selfV is the "reference" value from self
-   # otherV is the value from ``other``
-   >>> relDiff = abs(otherV - selfV)
-   >>> if selfV > 1E-6:
-   ...     relDiff /= selfV
-   >>> if relDiff < lower:
-   ...     # close enough, move along
-   >>> elif relDiff >= upper:
-   ...     # consider test a failure; print an error
-   >>> else:
-   ...     # close but not close enough; print a warning
-   ...     # but don't treat as a failure
-
-It is likely that the ``BaseObject`` or :mod:`serpentTools.utils` module
-will have some methods/functions for expediting this process, but the 
-procedure for all cases should follow this behavior.
+``lower`` less than or equal to ``upper``.  This functionality is
+implemented with the :func:`serpentTools.utils.directCompare` function,
+while the result is reported with :func:`serpentTools.utils.logDirectCompare`.
+
+.. _dev-comp-message:
 
 Use of messaging module
 =======================
@@ -74,9 +59,72 @@ using our :ref:`verbosity` setting.
 * Two items are outside of the ``sigma`` confidence intervals - |error|
 * Two items without uncertainties have relative difference
 
-    * less than ``lower`` - |info|
+    * less than ``lower`` - |debug|
     * greater than or equal to ``upper`` - |error|
     * otherwise - |warn|
 
-* Two items are in good agreement - |info|
+* Two items are identical - |debug|
 * Two arrays are not of similar size - |error|
+
+
+.. _dev-comp-utils:
+
+High-level Logging and Comparison Utilities
+===========================================
+
+The :mod:`~serpentTools.utils` module contains a collection of functions
+that can be used to compare quantities and automatically log results.
+When possible, these routines should be favored over hand-writing 
+comparison routines. If the situation calls for custom comparison 
+functions, utilize or extend logging routines from :ref:`dev-comp-log`
+appropriately.
+
+.. autofunction:: serpentTools.utils.compareDocDecorator
+
+.. autofunction:: serpentTools.utils.getCommonKeys
+
+.. autofunction:: serpentTools.utils.directCompare
+
+.. autofunction:: serpentTools.utils.logDirectCompare
+
+.. autofunction:: serpentTools.utils.splitdictByKeys
+
+.. autofunction:: serpentTools.utils.getKeyMatchingShapes
+
+.. autofunction:: serpentTools.utils.getOverlaps
+
+.. autofunction:: serpentTools.utils.getLogOverlaps
+
+.. _dev-comp-log:
+
+Low-level Logging Utilities
+===========================
+
+The :mod:`~serpentTools.messages` module contains a collection of functions
+that can be used to notify the user about the results of a comparison
+routine. 
+
+.. autofunction:: serpentTools.messages.logIdentical
+
+.. autofunction:: serpentTools.messages.logNotIdentical
+
+.. autofunction:: serpentTools.messages.logAcceptableLow
+
+.. autofunction:: serpentTools.messages.logAcceptableHigh
+
+.. autofunction:: serpentTools.messages.logOutsideTols
+
+.. autofunction:: serpentTools.messages.logIdenticalWithUncs
+
+.. autofunction:: serpentTools.messages.logInsideConfInt
+
+.. autofunction:: serpentTools.messages.logOutsideConfInt
+
+.. autofunction:: serpentTools.messages.logDifferentTypes
+
+.. autofunction:: serpentTools.messages.logMissingKeys
+
+.. autofunction:: serpentTools.messages.logBadTypes
+
+.. autofunction:: serpentTools.messages.logBadShapes
+
diff --git a/docs/develop/index.rst b/docs/develop/index.rst
index d386f58..e8eb10a 100644
--- a/docs/develop/index.rst
+++ b/docs/develop/index.rst
@@ -15,6 +15,7 @@ without any loss of comprehension.
     contributing.rst
     datamodel.rst
     codestyle.rst
+    utils.rst
     plots.rst
     documentation.rst
     checklist.rst
diff --git a/docs/api/utils.rst b/docs/develop/utils.rst
similarity index 100%
rename from docs/api/utils.rst
rename to docs/develop/utils.rst
diff --git a/serpentTools/messages.py b/serpentTools/messages.py
index 715c699..fa63331 100644
--- a/serpentTools/messages.py
+++ b/serpentTools/messages.py
@@ -11,6 +11,7 @@ import functools
 import warnings
 import logging
 from logging.config import dictConfig
+from collections import Callable
 
 
 class SerpentToolsException(Exception):
@@ -27,14 +28,6 @@ class MismatchedContainersError(SamplerError):
     """Attempting to sample from dissimilar containers"""
     pass
 
-class ComparisonError(SerpentToolsException):
-    """Base exception for errors raised while comparing objects."""
-    pass
-
-class MissingDataError(ComparisonError):
-    """One object is missing data contained on the other."""
-    pass
-
 
 LOG_OPTS = ['critical', 'error', 'warning', 'info', 'debug']
 
@@ -134,3 +127,181 @@ def _updateFilterAlert(msg, category):
     warnings.simplefilter('always', category)
     warnings.warn(msg, category=category, stacklevel=3)
     warnings.simplefilter('default', category)
+
+# =========================================================
+# Functions for notifying the user about comparison results
+# =========================================================
+
+
+def _prefaceNotice(obj, leader):
+    msg = '\n\t{} '.format(leader) + ''.join(str(obj).split('\n'))
+    return msg
+
+
+def _notify(func, quantity, header, obj0, obj1):
+    msg = header.format(quantity)
+    msg += _prefaceNotice(obj0, '>')
+    if obj1 is not None:
+        msg += _prefaceNotice(obj1, '<')
+    func(msg)
+
+
+def logIdentical(obj0, obj1, quantity):
+    """Two objects are identical."""
+    _notify(debug, quantity, 'Values for {} are identical', obj0, None)
+
+
+def logNotIdentical(obj0, obj1, quantity):
+    """Values should be identical but aren't."""
+    _notify(error, quantity, "Values for {} are not identical",
+            obj0, obj1)
+
+
+def logAcceptableLow(obj0, obj1, quantity):
+    """Two values differ, but inside nominal and acceptable ranges."""
+    _notify(info, quantity, "Values for {} are not identical, but close",
+            obj0, obj1)
+
+
+def logAcceptableHigh(obj0, obj1, quantity):
+    """Two values differ, enough to merit a warning but not an error."""
+    _notify(warning, quantity,
+            "Values for {} are different, but within tolerances", obj0, obj1)
+
+
+def logOutsideTols(obj0, obj1, quantity):
+    """Two values differ outside acceptable tolerances."""
+    _notify(error, quantity,
+            "Values for {} are outside acceptable tolerances.", obj0, obj1)
+
+
+def _notifyWithUncs(func, quantity, msg, value0, unc0, value1, unc1):
+    logMsg = msg.format(quantity)
+    logMsg += _prefaceNotice(value0, '>V')
+    logMsg += _prefaceNotice(unc0, '>U')
+    if value1 is not None:
+        logMsg += _prefaceNotice(value1, '<V')
+    logMsg += _prefaceNotice(unc0, '<U')
+    func(logMsg)
+
+
+def logIdenticalWithUncs(value, unc0, unc1, quantity):
+    """Notify that two values have identical expected values."""
+    _notifyWithUncs(debug, quantity,
+                    'Expected values for {} are identical',
+                    value, unc0, None, unc1)
+
+
+def logInsideConfInt(value0, unc0, value1, unc1, quantity):
+    """Two values are within acceptable statistical limits."""
+    _notifyWithUncs(debug, quantity, 'Confidence intervals for {} overlap',
+                    value0, unc0, value1, unc1)
+
+
+def logOutsideConfInt(value0, unc0, value1, unc1, quantity):
+    """Two values are outside acceptable statistical limits."""
+    _notifyWithUncs(error, quantity,
+                    "Values for {} are outside acceptable statistical limits",
+                    value0, unc0, value1, unc1)
+
+
+def logDifferentTypes(type0, type1, quantity):
+    """Two values are of different types."""
+    _notify(error, quantity, "Types for {} are different.",
+            type0, type1)
+
+
+MISSING_MSG_HEADER = "{} from {} and {} contain different items"
+MISSING_MSG_SUBJ = "\n\tItems present in {} but not in {}:\n\t\t{}"
+
+
+def _checkHerald(herald):
+    if not isinstance(herald, Callable):
+        critical("Heralding object {} is not callable. Falling back to error."
+                 .format(herald))
+        return error
+    return herald
+
+
+def logMissingKeys(quantity, desc0, desc1, in0, in1, herald=error):
+    """
+    Log a warning message that two objects contain different items
+
+    Parameters
+    ----------
+    quantity: str
+        Indicator as to what is being compared, e.g. ``'metadata'``
+    desc0: str
+    desc1: str
+        Descriptions of the two originators
+    in0: set or iterable
+    in1: set or iterable
+        Items that are unique to originators ``0`` and ``1``, respectively
+    herald: callable
+        Callable function that accepts a single string. This will be called
+        with the error message. If not given, defaults to :func:`error`
+    """
+    if not any(in0) and not any(in1):
+        return
+    herald = _checkHerald(herald)
+    msg = MISSING_MSG_HEADER.format(quantity, desc0, desc1)
+    if any(in0):
+        msg += MISSING_MSG_SUBJ.format(desc0, desc1,
+                                       ', '.join([str(xx) for xx in in0]))
+    if any(in1):
+        msg += MISSING_MSG_SUBJ.format(desc1, desc0,
+                                       ', '.join([str(xx) for xx in in1]))
+    herald(msg)
+
+
+BAD_TYPES_HEADER = "Items from {d0} and {d1} {q} have different types"
+BAD_SHAPES_HEADER = "Items from {d0} and {d1} {q} have different shapes"
+BAD_OBJ_SUBJ = "\n\t{key}: {t0} - {t1}"
+
+
+def logBadTypes(quantity, desc0, desc1, types):
+    """
+    Log an error message for containers with mismatched types
+
+    Parameters
+    ----------
+    quantity: str
+        Indicator as to what is being compared, e.g. ``'metadata'``
+    desc0: str
+    desc1: str
+        Descriptions of the two originators
+    types: dict
+        Dictionary where the keys represent the locations of
+        items with mismatched types. Corresponding keys should
+        be a list or tuple of the types for objects from
+        ``desc0`` and ``desc1`` stored under ``key``
+    """
+    msg = BAD_TYPES_HEADER.format(q=quantity, d0=desc0, d1=desc1)
+    for key in sorted(list(types.keys())):
+        t0, t1 = types[key]
+        msg += BAD_OBJ_SUBJ.format(key=key, t0=t0, t1=t1)
+    error(msg)
+
+
+def logBadShapes(quantity, desc0, desc1, shapes):
+    """
+    Log an error message for containers with mismatched shapes
+
+    Parameters
+    ----------
+    quantity: str
+        Indicator as to what is being compared, e.g. ``'metadata'``
+    desc0: str
+    desc1: str
+        Descriptions of the two originators
+    shapes: dict
+        Dictionary where the keys represent the locations of
+        items with mismatched shapes. Corresponding keys should
+        be a list or tuple of the shapes for objects from
+        ``desc0`` and ``desc1`` stored under ``key``
+    """
+    msg = BAD_SHAPES_HEADER.format(q=quantity, d0=desc0, d1=desc1)
+    for key in sorted(list(shapes.keys())):
+        t0, t1 = shapes[key]
+        msg += BAD_OBJ_SUBJ.format(key=key, t0=t0, t1=t1)
+    error(msg)
diff --git a/serpentTools/objects/base.py b/serpentTools/objects/base.py
index 00f951d..6d59c3a 100644
--- a/serpentTools/objects/base.py
+++ b/serpentTools/objects/base.py
@@ -8,15 +8,26 @@ 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.messages import debug, warning, SerpentToolsException, info
+from serpentTools.settings import rc
+from serpentTools.utils import compareDocDecorator
 from serpentTools.plot import (
         plot, magicPlotDocDecorator, formatPlot, cartMeshPlot)
 
+#
+# Defaults for comparison
+#
+DEF_COMP_LOWER = 0
+DEF_COMP_UPPER = 10
+DEF_COMP_SIGMA = 2
+
 
 class BaseObject(object):
     """Most basic class shared by all other classes."""
 
-    def compare(self, other, lower=0, upper=10, sigma=3):
+    @compareDocDecorator
+    def compare(self, other, lower=DEF_COMP_LOWER, upper=DEF_COMP_UPPER,
+                sigma=DEF_COMP_SIGMA, verbosity=None):
         """
         Compare the results of this reader to another.
 
@@ -37,36 +48,27 @@ class BaseObject(object):
         other:
             Other reader instance against which to compare.
             Must be a similar class as this one.
-        lower: float or int
-            Lower limit for relative tolerances.
-            Differences below this will be considered allowable
-        upper: float or int
-            Upper limit for relative tolerances. Differences
-            above this will be considered failure and errors
-            messages will be raised
-        sigma: int
-            Size of confidence interval to apply to
-            quantities with uncertainties. Quantities that do not
-            have overlapping confidence intervals will fail.
+        {compLimits}
+        {sigma}
+        verbosity: None or str
+            If given, update the verbosity just for this comparison.
 
         Returns
         -------
         bool:
-            ``True`` if the readers are in agreement with
+            ``True`` if the objects are in agreement with
             each other according to the parameters specified
 
         Raises
         ------
-        TypeError
-            If ``other`` is not of the same class as this class
-            nor a subclass of this class
+        {compTypeErr}
         ValueError
             If upper > lower,
             If sigma, lower, or upper are negative
         """
         upper = float(upper)
         lower = float(lower)
-        sigma = float(sigma)
+        sigma = int(sigma)
         if upper < lower:
             raise ValueError("Upper limit must be greater than lower. "
                              "{} is not greater than {}"
@@ -76,6 +78,29 @@ class BaseObject(object):
             if item < 0:
                 raise ValueError("{} must be non-negative, is {}"
                                  .format(key, item))
+
+        self._checkCompareObj(other)
+
+        previousVerb = None
+        if verbosity is not None:
+            previousVerb = rc['verbosity']
+            rc['verbosity'] = verbosity
+
+        self._compareLogPreMsg(other, lower, upper, sigma)
+
+        areSimilar = self._compare(other, lower, upper, sigma)
+
+        if previousVerb is not None:
+            rc['verbosity'] = previousVerb
+
+        return areSimilar
+
+    def _compare(self, other, lower, upper, sigma):
+        """Actual comparison method for similar classes."""
+        raise NotImplementedError
+
+    def _checkCompareObj(self, other):
+        """Verify that the two objects are same class or subclasses."""
         if not (isinstance(other, self.__class__) or
                 issubclass(other.__class__, self.__class__)):
             oName = other.__class__.__name__
@@ -84,11 +109,25 @@ class BaseObject(object):
                     "Cannot compare against {} - not instance nor subclass "
                     "of {}".format(oName, name))
 
-        return self._compare(other, lower, upper, sigma)
-
-    def _compare(self, other, lower, upper, sigma):
-        """Actual comparison method for similar classes."""
-        raise NotImplementedError
+    def _compareLogPreMsg(self, other, lower=None, upper=None, sigma=None,
+                          quantity=None):
+        """Log an INFO message about this specific comparison."""
+        leader = "Comparing {}> against < with the following tolerances:"
+        tols = [leader.format((quantity + ' from ') if quantity else ''), ]
+        for leader, obj in zip(('>', '<'), (self, other)):
+            tols.append("{} {}".format(leader, obj))
+        for title, val in zip(('Lower', 'Upper'), (lower, upper)):
+            if val is None:
+                continue
+            tols.append("{} tolerance: {:5.3F} [%]".format(title, val))
+        if sigma is not None:
+            sigmaStr = ("Confidence interval for statistical values: {:d} "
+                        "sigma or {} %")
+            sigmaDict = {1: 68, 2: 95}
+            tols.append(
+                sigmaStr.format(sigma, sigmaDict.get(sigma, '>= 99.7')
+                                if sigma else 0))
+        info('\n\t'.join(tols))
 
 
 class NamedObject(BaseObject):
@@ -212,8 +251,8 @@ class DetectorBase(NamedObject):
 
     @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, 
+                     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.
@@ -237,7 +276,7 @@ class DetectorBase(NamedObject):
         {legend}
         {ncol}
         {title}
-        {kwargs} :py:func:`matplotlib.pyplot.plot` or 
+        {kwargs} :py:func:`matplotlib.pyplot.plot` or
             :py:func:`matplotlib.pyplot.errorbar`
 
         Returns
@@ -268,7 +307,7 @@ class DetectorBase(NamedObject):
                 divide(self.grids['E'][:, -1], lowerE))
             for indx in range(slicedTallies.shape[1]):
                 scratch = divide(slicedTallies[:, indx], lethBins)
-                slicedTallies[:, indx] = scratch / scratch.max() 
+                slicedTallies[:, indx] = scratch / scratch.max()
 
         if steps:
             if 'drawstyle' in kwargs:
@@ -279,25 +318,25 @@ class DetectorBase(NamedObject):
 
         if sigma:
             slicedErrors = sigma * self.slice(fixed, 'errors').copy()
-            slicedErrors = slicedErrors.reshape(slicedTallies.shape) 
+            slicedErrors = slicedErrors.reshape(slicedTallies.shape)
             slicedErrors *= slicedTallies
         else:
             slicedErrors = None
-        ax = plot(lowerE, slicedTallies, ax=ax, labels=labels, 
-                  yerr=slicedErrors, **kwargs)     
+        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 ''
+            ylabel += r' $\pm${}$\sigma$'.format(sigma) if sigma else ''
 
         ax = formatPlot(ax, loglog=loglog, logx=logx, ncol=ncol,
-                        xlabel=xlabel or "Energy [MeV]", ylabel=ylabel, 
+                        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, 
+             xlabel=None, ylabel=None, steps=False, labels=None, logx=False,
              logy=False, loglog=False, legend=False, ncol=1, title=None,
              **kwargs):
         """
@@ -314,7 +353,7 @@ class DetectorBase(NamedObject):
         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 
+        {xlabel} If ``xdim`` is given and ``xlabel`` is ``None``, then
             ``xdim`` will be applied to the x-axis.
         {ylabel}
         steps: bool
@@ -364,12 +403,12 @@ class DetectorBase(NamedObject):
                     'Will not plot error bars on the error plot. Data to be '
                     'plotted: {}.  Sigma: {}'.format(what, sigma))
                 yerr = None
-        else: 
+        else:
             yerr = None
 
         xdata, autoX = self._getPlotXData(xdim, data)
         xlabel = xlabel or autoX
-        ylabel = ylabel or "Tally data" 
+        ylabel = ylabel or "Tally data"
         ax = ax or axes()
 
         if steps:
@@ -380,13 +419,13 @@ class DetectorBase(NamedObject):
                 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, 
+                        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, 
+                 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 bin types on a cartesian mesh.
@@ -404,7 +443,7 @@ class DetectorBase(NamedObject):
         {ax}
         {cmap}
         logColor: bool
-            If true, apply a logarithmic coloring to the data positive 
+            If true, apply a logarithmic coloring to the data positive
             data
         {xlabel}
         {ylabel}
diff --git a/serpentTools/objects/containers.py b/serpentTools/objects/containers.py
index 1982d2d..0b92b0b 100644
--- a/serpentTools/objects/containers.py
+++ b/serpentTools/objects/containers.py
@@ -17,8 +17,25 @@ from numpy import array, arange, hstack, ndarray, zeros_like
 from serpentTools.settings import rc
 from serpentTools.plot import magicPlotDocDecorator, formatPlot
 from serpentTools.objects.base import NamedObject, BaseObject
-from serpentTools.utils import convertVariableName
-from serpentTools.messages import warning, SerpentToolsException, debug, info
+from serpentTools.utils import (
+    COMPARE_DOC_SIGMA,
+    COMPARE_DOC_TYPE_ERR,
+    convertVariableName,
+    getKeyMatchingShapes,
+    logDirectCompare,
+    getLogOverlaps,
+    compareDocDecorator,
+)
+from serpentTools.objects.base import (DEF_COMP_LOWER,
+                                       DEF_COMP_UPPER, DEF_COMP_SIGMA)
+from serpentTools.messages import (
+    warning,
+    SerpentToolsException,
+    debug,
+    info,
+    critical,
+    error,
+)
 
 SCATTER_MATS = set()
 SCATTER_ORDERS = 8
@@ -439,6 +456,130 @@ class HomogUniv(NamedObject):
 
     hasData = __bool__
 
+    def _compare(self, other, lower, upper, sigma):
+        similar = self.compareAttributes(other, lower, upper, sigma)
+        similar &= self.compareInfData(other, sigma)
+        similar &= self.compareB1Data(other, sigma)
+        similar &= self.compareGCData(other, sigma)
+        
+        return similar
+
+    @compareDocDecorator
+    def compareAttributes(self, other, lower=DEF_COMP_LOWER,
+                          upper=DEF_COMP_UPPER, sigma=DEF_COMP_SIGMA):
+        """
+        Compare attributes like group structure and burnup. Return the result
+
+        Parameters
+        ----------
+        other: :class:`HomogUniv`
+            Universe against which to compare
+        {compLimits}
+        {sigma}
+
+        Returns
+        -------
+        bools:
+            ``True`` if the attributes agree within specifications
+
+        Raises
+        ------
+        {compTypeErr}
+        """
+
+        self._checkCompareObj(other)
+
+        myMeta = {}
+        otherMeta = {}
+        
+        for key in {'bu', 'step', 'groups', 'microGroups', 'reshaped'}:
+            for meta, obj in zip((myMeta, otherMeta), (self, other)):
+                try:
+                    meta[key] = getattr(obj, key)
+                except AttributeError:
+                    meta[key] = None
+
+        matchingKeys = getKeyMatchingShapes(myMeta, otherMeta, 'metadata')
+        similar = len(matchingKeys) == len(myMeta)
+
+        for key in sorted(matchingKeys):
+            similar &= logDirectCompare(myMeta[key], otherMeta[key],
+                                        lower, upper, key)
+
+        return similar
+
+
+    __docCompare = """
+    Return ``True`` if contents of ``{qty}Exp`` and ``{qty}Unc`` agree
+
+    Parameters
+    ----------
+    other: :class:`HomogUniv`
+        Object from which to grab group constant dictionaries
+    

+
+    Returns
+    bool
+        If the dictionaries contain identical values with uncertainties,
+        and if those values have overlapping confidence intervals
+    Raises
+    ------
+    <e>
+    """.replace('
', COMPARE_DOC_SIGMA).replace('<e>', COMPARE_DOC_TYPE_ERR)
+
+    def _helpCompareGCDict(self, other, attrBase, sigma):
+        """
+        Method that actually compare group constant dictionaries.
+        
+        ``attrBase`` is used to find dictionaries by appending 
+        ``'Exp'`` and ``'Unc'`` to ``attrBase``
+        """
+        self._checkCompareObj(other)
+
+        valName = (attrBase + 'Exp') if attrBase != 'gc' else 'gc'
+        uncName = attrBase + 'Unc'
+        try:
+            myVals = getattr(self, valName)
+            myUncs = getattr(self, uncName)
+            otherVals = getattr(other, valName)
+            otherUncs = getattr(other, uncName)
+        except Exception as ee:
+            critical("The following error was raised extracting {} and "
+                    "{} from universes {} and {}:\n\t{}"
+                    .format(valName, uncName, self, other, ee))
+            return False
+
+        keys = getKeyMatchingShapes(myVals, otherVals, valName)
+        similar = len(keys) == len(myVals) == len(otherVals)
+        
+        for key in keys:
+            if key not in myUncs or key not in otherUncs:
+                loc = self if key in otherUncs else other
+                error("Uncertainty data for {} missing from {}"
+                      .format(key, loc))
+                similar = False
+                continue
+            myVal = myVals[key]
+            myUnc = myUncs[key]
+            otherVal = otherVals[key]
+            otherUnc = otherUncs[key]
+
+            similar &= getLogOverlaps(key, myVal, otherVal, myUnc, otherUnc,
+                                      sigma, relative=True)
+        return similar
+
+    def compareInfData(self, other, sigma):
+        return self._helpCompareGCDict(other, 'inf', sigma)
+
+    def compareB1Data(self, other, sigma):
+        return self._helpCompareGCDict(other, 'b1', sigma)
+
+    def compareGCData(self, other, sigma):
+        return self._helpCompareGCDict(other, 'gc', sigma)
+
+    compareInfData.__doc__ = __docCompare.format(qty='inf')
+    compareB1Data.__doc__ = __docCompare.format(qty='b1')
+    compareGCData.__doc__ = __docCompare.format(qty='gc')
 
 class BranchContainer(BaseObject):
     """
diff --git a/serpentTools/parsers/_collections.py b/serpentTools/parsers/_collections.py
new file mode 100644
index 0000000..0bf0dc8
--- /dev/null
+++ b/serpentTools/parsers/_collections.py
@@ -0,0 +1,92 @@
+"""
+Collections of objects that are helpful for the parsers
+"""
+
+RES_DATA_NO_UNCS = {
+    "resMemsize",
+    "totNuclides",
+    "fissionProductInhTox",
+    "ingestionToxicity",
+    "totSfRate",
+    "electronDecaySource",
+    "uresDiluCut",
+    "implNxn",
+    "neutronErgTol",
+    "useDbrc",
+    "actinideActivity",
+    "actinideInhTox",
+    "photonDecaySource",
+    "te132Activity",
+    "implCapt",
+    "alphaDecaySource",
+    "totActivity",
+    "fissionProductActivity",
+    "simulationCompleted",
+    "sourcePopulation",
+    "useUres",
+    "lostParticles",
+    "iniFmass",
+    "totPhotonNuclides",
+    "totTransmuRea",
+    "uresEmax",
+    "uresUsed",
+    "i132Activity",
+    "cpuUsage",
+    "xsMemsize",
+    "memsize",
+    "totDecayNuclides",
+    "tmsMode",
+    "actinideIngTox",
+    "totDosimetryNuclides",
+    "cs134Activity",
+    "uresEmin",
+    "totCells",
+    "neutronErgNe",
+    "fissionProductIngTox",
+    "sampleCapt",
+    "actinideDecayHeat",
+    "runningTime",
+    "uresAvail",
+    "cycleIdx",
+    "neutronEmin",
+    "neutronDecaySource",
+    "totDecayHeat",
+    "dopplerPreprocessor",
+    "matMemsize",
+    "inhalationToxicity",
+    "sampleFiss",
+    "totFmass",
+    "useDelnu",
+    "cs137Activity",
+    "availMem",
+    "neutronEmax",
+    "miscMemsize",
+    "sampleScatt",
+    "unusedMemsize",
+    "unionCells",
+    "sr90Activity",
+    "totCpuTime",
+    "implFiss",
+    "allocMemsize",
+    "unknownMemsize",
+    "ompParallelFrac",
+    "fissionProductDecayHeat",
+    "totReaChannels",
+    "totTransportNuclides",
+    "ifcMemsize",
+    "i131Activity",
+    "balaSrcNeutronTot",
+    "balaSrcNeutronFiss",
+    "balaSrcNeutronNxn",
+    "balaLossNeutronFiss",
+    "balaLossNeutronTot",
+    "balaLossNeutronCapt",
+    "balaLossNeutronLeak",
+    "transportCycleTime",
+    "processTime",
+    "initTime",
+    }
+"""
+Set containing keys for objects stored in :attr:`ResultsReader.resdata`
+that do not contain uncertainties.
+"""
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index 70f375a..af8c6e6 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -1,7 +1,7 @@
 """Parser responsible for reading the ``*res.m`` files"""
 import re
 
-import six
+from six import iteritems
 
 from numpy import array, vstack
 
@@ -9,8 +9,24 @@ from serpentTools.settings import rc
 from serpentTools.utils import convertVariableName
 from serpentTools.objects.containers import HomogUniv
 from serpentTools.parsers.base import XSReader
-from serpentTools.utils import str2vec, splitValsUncs
-from serpentTools.messages import (warning, debug, SerpentToolsException)
+from serpentTools.parsers._collections import RES_DATA_NO_UNCS
+from serpentTools.objects.base import (DEF_COMP_LOWER, 
+                                       DEF_COMP_SIGMA, DEF_COMP_UPPER)
+from serpentTools.utils import (
+    str2vec, 
+    splitValsUncs,
+    getCommonKeys,
+    logDirectCompare,
+    compareDocDecorator,
+    getKeyMatchingShapes,
+    getLogOverlaps,
+)
+from serpentTools.messages import (
+        warning, debug, SerpentToolsException,
+        info,
+        logInsideConfInt,
+        logOutsideConfInt,
+)
 
 
 MapStrVersions = {'2.1.29': {'meta': 'VERSION ', 'rslt': 'MIN_MACROXS', 'univ': 'GC_UNIVERSE_NAME',
@@ -22,6 +38,10 @@ Assigns search strings for different Serpent versions
 Versions 2.1.29 and 2.1.30 are supported
 """
 
+
+__all__ = ['ResultsReader', ]
+
+
 class ResultsReader(XSReader):
     """
     Parser responsible for reading and working with result files.
@@ -59,6 +79,16 @@ class ResultsReader(XSReader):
     IOError: file is unexpectedly closes while reading
     """
 
+    __METADATA_COMP_SKIPS = {
+        'title', 
+        'inputFileName', 
+        'workingDirectory',
+        'startDate',
+        'completeDate',
+        'seed',
+        }
+    """Metadata keys that will not be compared."""
+
     def __init__(self, filePath):
         XSReader.__init__(self, filePath, 'xs')
         self.__serpentVersion = rc['serpentVersion']
@@ -229,7 +259,7 @@ class ResultsReader(XSReader):
             raise KeyError(
                 'Index read is {}, however only integers above zero are allowed'
                     .format(searchValue))
-        for key, dictUniv in six.iteritems(self.universes):
+        for key, dictUniv in iteritems(self.universes):
             if key[0] == univ and key[searchIndex] == searchValue:
                 debug('Found universe that matches with keys {}'
                       .format(key))
@@ -280,8 +310,137 @@ class ResultsReader(XSReader):
                 "{} time-points, and {} overall result points ".format(self.filePath,
                 self._counter['univ'], self._counter['rslt'], self._counter['meta']))
         if not self.resdata and not self.metadata:
-            for keys, dictUniv in six.iteritems(self.universes):
+            for keys, dictUniv in iteritems(self.universes):
                 if not dictUniv.hasData():
                     raise SerpentToolsException("metadata, resdata and universes are all empty "
-                                        "from {}".format(self.filePath))
+                                                "from {}".format(self.filePath))
+
+    def _compare(self, other, lower, upper, sigma):
+        similar = self.compareMetadata(other)
+        similar &= self.compareResults(other, lower, upper, sigma)
+        similar &= self.compareUniverses(other, lower, upper, sigma)
+        return similar
+
+    @compareDocDecorator
+    def compareMetadata(self, other, header=False):
+        """
+        Return True if the metadata (settings) are identical.
+
+        Parameters
+        ----------
+        other: :class:`ResultsReader`
+            Class against which to compare
+        {header}
+
+        Returns
+        -------
+        bool:
+            If the metadata are identical
+
+        Raises
+        ------
+        {compTypeErr}
+        """
+
+        self._checkCompareObj(other)
+        if header:
+            self._compareLogPreMsg(other, quantity='metadata')
+        myKeys = set(self.metadata.keys())
+        otherKeys = set(other.metadata.keys())
+        similar = not any(myKeys.symmetric_difference(otherKeys))
+        commonKeys = getCommonKeys(myKeys, otherKeys, 'metadata')
+        skips = commonKeys.intersection(self.__METADATA_COMP_SKIPS)
+        if any(skips):
+            info("The following items will be skipped in the comparison\n\t{}"
+                  .format(', '.join(sorted(skips))))
+        for key in sorted(commonKeys):
+            if key in self.__METADATA_COMP_SKIPS:
+                continue
+            selfV = self.metadata[key]
+            otherV = other.metadata[key]
+            similar &= logDirectCompare(selfV, otherV, 0., 0., key)
+
+        return similar
+
+    @compareDocDecorator
+    def compareResults(self, other, lower=DEF_COMP_LOWER,
+                       upper=DEF_COMP_UPPER, sigma=DEF_COMP_SIGMA,
+                       header=False):
+        """
+        Compare the contents of the results dictionary
+
+        Parameters
+        ----------
+        other: :class:`ResultsReader`
+            Class against which to compare
+        {compLimits}
+        {sigma}
+        {header}
+
+        Returns
+        -------
+        bool:
+            If the results data agree to given tolerances
+
+        Raises
+        ------
+        {compTypeErr}
+        """
+        self._checkCompareObj(other)
+        if header:
+            self._compareLogPreMsg(other, lower, upper, sigma, 'results')
+        myRes = self.resdata
+        otherR = other.resdata
+
+        commonTypeKeys = getKeyMatchingShapes(myRes, otherR, 'results')
+
+        similar = len(commonTypeKeys) == len(myRes) == len(otherR)
+
+        for key in sorted(commonTypeKeys):
+            mine = myRes[key]
+            theirs = otherR[key]
+            if key in RES_DATA_NO_UNCS:
+                similar &= logDirectCompare(mine, theirs, lower, upper, key)
+                continue
+            myVals, myUncs = splitValsUncs(mine)
+            theirVals, theirUncs = splitValsUncs(theirs)
+            similar &= getLogOverlaps(key, myVals, theirVals, myUncs, 
+                                      theirUncs, sigma, relative=True)
+        return similar
+
+    @compareDocDecorator
+    def compareUniverses(self, other, lower=DEF_COMP_LOWER, 
+                         upper=DEF_COMP_UPPER, sigma=DEF_COMP_SIGMA):
+        """
+        Compare the contents of the ``universes`` dictionary
+
+        Parameters
+        ----------
+        other: :class:`ResultsReader`
+            Reader by which to compare
+        {compLimits}
+        {sigma}
+
+        Returns
+        -------
+        bool:
+            If the contents of the universes agree to given tolerances
+
+        Raises
+        ------
+        {compTypeErr}
+        """
+        self._checkCompareObj(other)
+        myUniverses = self.universes
+        otherUniverses = other.universes
+        keyGoodTypes = getKeyMatchingShapes(myUniverses, otherUniverses,
+                                            'universes')
+
+        similar = len(keyGoodTypes) == len(myUniverses) == len(otherUniverses)
+
+        for univKey in keyGoodTypes:
+            myUniv = myUniverses[univKey]
+            otherUniv = otherUniverses[univKey]
+            similar &= myUniv.compare(otherUniv, lower, upper, sigma)
 
+        return similar
diff --git a/serpentTools/plot.py b/serpentTools/plot.py
index f936189..f1ef76b 100644
--- a/serpentTools/plot.py
+++ b/serpentTools/plot.py
@@ -125,7 +125,7 @@ def magicPlotDocDecorator(f):
     def decorated(*args, **kwargs):
         return f(*args, **kwargs)
     doc = dedent(f.__doc__)
-    for magic, replace in PLOT_MAGIC_STRINGS.items():
+    for magic, replace in iteritems(PLOT_MAGIC_STRINGS):
         lookF = '{' + magic + '}'
         if lookF in doc:
             doc = doc.replace(lookF, replace)
diff --git a/serpentTools/utils.py b/serpentTools/utils.py
index 3a4e004..c9c5add 100644
--- a/serpentTools/utils.py
+++ b/serpentTools/utils.py
@@ -2,9 +2,31 @@
 Commonly used functions and utilities
 """
 
-from numpy import array, ndarray
+from textwrap import dedent
+from functools import wraps
+
+from six import iteritems
+from numpy import array, ndarray, fabs, where, zeros_like
+
+from serpentTools.messages import (
+    error,
+    logIdentical,
+    logNotIdentical,
+    logAcceptableLow,
+    logAcceptableHigh,
+    logOutsideTols,
+    logDifferentTypes,
+    logMissingKeys,
+    logBadTypes,
+    logBadShapes,
+    logIdenticalWithUncs,
+    logInsideConfInt,
+    logOutsideConfInt,
+    )
+
+LOWER_LIM_DIVISION = 1E-8
+"""Lower limit for denominator for division"""
 
-from serpentTools.messages import critical
 
 def str2vec(iterable, of=float, out=array):
     """
@@ -20,13 +42,13 @@ def str2vec(iterable, of=float, out=array):
     of: type
         Convert each value in ``iterable`` to this data type.
     out: type
-        Return data type. Will be passed the iterable of 
+        Return data type. Will be passed the iterable of
         converted items of data dtype ``of``.
 
     Returns
     -------
     vector
-        Iterable of all values of ``iterable``, or split variant, 
+        Iterable of all values of ``iterable``, or split variant,
         converted to type ``of``.
 
     Examples
@@ -45,7 +67,7 @@ def str2vec(iterable, of=float, out=array):
         array([1., 2., 3., 4.,])
 
     """
-    vec = (iterable.split() if isinstance(iterable, str) 
+    vec = (iterable.split() if isinstance(iterable, str)
            else iterable)
     return out([of(xx) for xx in vec])
 
@@ -55,13 +77,13 @@ def splitValsUncs(iterable, copy=False):
     Return even and odd indexed values from iterable
 
     Designed to extract expected values and uncertainties from
-    SERPENT vectors of the form 
+    SERPENT vectors of the form
     ``[x1, u1, x2, u2, ...]``
 
     Parameters
     ----------
     iterable: ndarray or iterable
-        Initial arguments to be processed. If not 
+        Initial arguments to be processed. If not
         :py:class:`numpy.ndarray`, then will be converted
         by calling :py:func:`serpentTools.utils.str2vec`
     copy: bool
@@ -92,8 +114,8 @@ def splitValsUncs(iterable, copy=False):
 
     if not isinstance(iterable, ndarray):
         iterable = str2vec(iterable)
-    vals = iterable[0::2]
-    uncs = iterable[1::2]
+    vals = iterable[..., 0::2]
+    uncs = iterable[..., 1::2]
     if copy:
         return vals.copy(), uncs.copy()
     return vals, uncs
@@ -102,7 +124,7 @@ def splitValsUncs(iterable, copy=False):
 def convertVariableName(variable):
     """
     Return the mixedCase version of a SERPENT variable.
-    
+
     Parameters
     ----------
     variable: str
@@ -144,7 +166,7 @@ def linkToWiki(subLink, text=None):
     Parameters
     ----------
     subLink: str
-        Desired path inside the SERPENT wiki - following the 
+        Desired path inside the SERPENT wiki - following the
         ``index.php``
     text: None or str
         If given, use this as the shown text for the full link.
@@ -152,14 +174,14 @@ def linkToWiki(subLink, text=None):
     Returns
     -------
     str:
-        String that can be used as an rst hyperlink to the 
+        String that can be used as an rst hyperlink to the
         SERPENT wiki
-    
+
     Examples
     --------
     >>> linkToWiki('Input_syntax_manual')
     http://serpent.vtt.fi/mediawiki/index.php/Input_syntax_manual
-    >>> linkToWiki('Description_of_output_files#Burnup_calculation_output', 
+    >>> linkToWiki('Description_of_output_files#Burnup_calculation_output',
     ...            "Depletion Output")
     `Depletion Output <http://serpent.vtt.fi/mediawiki/index.php/
     Description_of_output_files#Burnup_calculation_output>`_
@@ -168,3 +190,521 @@ def linkToWiki(subLink, text=None):
     if not text:
         return fullLink
     return "`{} <{}>`_".format(text, fullLink)
+
+
+COMPARE_DOC_DESC = """
+    desc0: dict or None
+    desc1: dict or None
+        Description of the origin of each value set. Only needed
+        if ``quiet`` evalues to ``True``."""
+COMPARE_DOC_HERALD = """herald: callable
+        Function that accepts a single string argument used to
+        notify that differences were found. If
+        the function is not a callable object, a
+        :func:`serpentTools.messages.critical` message
+        will be printed and :func:`serpentTools.messages.error`
+        will be used."""
+COMPARE_DOC_LIMITS = """
+    lower: float or int
+        Lower limit for relative tolerances in percent
+        Differences below this will be considered allowable
+    upper: float or int
+        Upper limit for relative tolerances in percent. Differences
+        above this will be considered failure and errors
+        messages will be raised"""
+COMPARE_DOC_SIGMA = """sigma: int
+        Size of confidence interval to apply to
+        quantities with uncertainties. Quantities that do not
+        have overlapping confidence intervals will fail"""
+COMPARE_DOC_TYPE_ERR = """TypeError
+        If ``other`` is not of the same class as this class
+        nor a subclass of this class"""
+COMPARE_DOC_HEADER = """header: bool
+        Print/log an ``info`` message about this comparison."""
+COMPARE_DOC_MAPPING = {
+    'herald': COMPARE_DOC_HERALD,
+    'desc': COMPARE_DOC_DESC,
+    'compLimits': COMPARE_DOC_LIMITS,
+    'sigma': COMPARE_DOC_SIGMA,
+    'compTypeErr': COMPARE_DOC_TYPE_ERR,
+    'header': COMPARE_DOC_HEADER,
+}
+
+COMPARE_FAIL_MSG = "Values {desc0} and {desc1} are not identical:\n\t"
+COMPARE_WARN_MSG = ("Values {desc0} and {desc1} are not identical, but within "
+                    "tolerances:\n\t")
+COMPARE_PASS_MSG = "Values {desc0} and {desc0} are identical:\n\t"
+
+
+def compareDocDecorator(f):
+    """Decorator that updates doc strings for comparison methods.
+
+    Similar to :func:`serpentTools.plot.magicPlotDocDecorator`
+    but for comparison functions
+    """
+
+    @wraps(f)
+    def decorated(*args, **kwargs):
+        return f(*args, **kwargs)
+    doc = dedent(f.__doc__)
+    for magic, replace in iteritems(COMPARE_DOC_MAPPING):
+        lookF = '{' + magic + '}'
+        if lookF in doc:
+            doc = doc.replace(lookF, dedent(replace))
+    decorated.__doc__ = doc
+    return decorated
+
+
+def _getDefDescs(desc0, desc1):
+    desc0 = desc0 if desc0 is not None else 'first'
+    desc1 = desc1 if desc1 is not None else 'second'
+    return desc0, desc1
+
+
+@compareDocDecorator
+def getCommonKeys(d0, d1, quantity, desc0='first', desc1='second',
+                  herald=error):
+    """
+    Return a set of common keys from two dictionaries
+
+    Also supports printing warning messages for keys not
+    found on one collection.
+
+    If ``d0`` and ``d1`` are :class:`dict`, then the
+    keys will be obtained with ``d1.keys()``. Otherwise,
+    assume we have an iterable of keys and convert to
+    :class:`set`.
+
+    Parameters
+    ----------
+    d0: dict or iterable
+    d1: dict or iterable
+        Dictionary of keys or iterable of keys to be compared
+    quantity: str
+        Indicator as to what is being compared, e.g. ``'metadta'``
+    {desc}
+    {herald}
+    Returns
+    -------
+    set:
+        Keys found in both ``d{{0, 1}}``
+    """
+    k0 = d0.keys() if isinstance(d0, dict) else d0
+    k1 = d1.keys() if isinstance(d1, dict) else d1
+    s0 = set(k0)
+    s1 = set(k1)
+
+    common = s0.intersection(s1)
+    missing = s0.symmetric_difference(s1)
+    if missing:
+        in0 = s0.difference(s1)
+        in1 = s1.difference(s0)
+        logMissingKeys(quantity, desc0, desc1, in0, in1, herald)
+    return common
+
+
+TPL_FLOAT_INT = float, int
+
+COMPARE_STATUS_CODES = {
+    0: (logIdentical, True),
+    1: (logAcceptableLow, True),
+    10: (logAcceptableHigh, True),
+    200: (logNotIdentical, False),
+    100: (logOutsideTols, False),
+    255: (logDifferentTypes, False),
+}
+"""Keys of status codes with ``(caller, return)`` values."""
+
+
+@compareDocDecorator
+def directCompare(obj0, obj1, lower, upper):
+    """
+    Return True if values are close enough to each other.
+
+    Wrapper around various comparision tests for strings, numeric, and
+    arrays.
+
+    Parameters
+    ----------
+    obj0: str or float or int or :class:`numpy.ndarray`
+    obj1: str or float or int or :class:`numpy.ndarray`
+        Objects to compare
+    {compLimits}
+    quantity: str
+        Description of the value being compared. Will be
+        used to notify the user about any differences
+
+    Returns
+    -------
+    int:
+        Status code of the comparison.
+
+        * 0 - Values are identical to floating point precision or,
+          for strings/booleans, are identical with the ``==`` operator
+        * 1 - Values are not identical, but the max difference
+          is less than ``lower``.
+        * 10 - Values differ, with the max difference greater
+          than ``lower`` but less than ``upper``
+        * 100 - Values differ by greater than or equal to ``upper``
+        * 200 - Values should be identical (strings, booleans),
+          but are not
+        * 255 - Values are of different types
+        * -1 - Type comparison is not supported. This means that
+          developers should either implement a test for this
+          data type, or use a different function
+
+    See Also
+    --------
+    * :func:`logDirectCompare` - Function that utilizes this and logs
+      the results using the :mod:`serpentTools.messages` module
+    """
+    type0 = type(obj0)
+    type1 = type(obj1)
+    if type0 != type1:
+        if type0 not in TPL_FLOAT_INT or type1 not in TPL_FLOAT_INT:
+            return 255
+    if type0 in (str, bool):
+        if obj0 != obj1:
+            return 200
+        return 0
+
+    if type0 in (float, int, ndarray) or hasattr(obj0, 'dtype'):
+        diff = fabs(obj0 - obj1) * 100
+        if type0 is ndarray:
+            nonZI = where(obj0 > LOWER_LIM_DIVISION)
+            diff[nonZI] /= obj0[nonZI]
+        elif obj0 > LOWER_LIM_DIVISION:
+            diff /= obj0
+        maxDiff = diff.max() if isinstance(diff, ndarray) else diff
+        if maxDiff < LOWER_LIM_DIVISION:
+            return 0
+        if maxDiff <= lower:
+            return 1
+        if maxDiff >= upper:
+            return 100
+        return 10
+    return -1
+
+
+@compareDocDecorator
+def logDirectCompare(obj0, obj1, lower, upper, quantity):
+    """
+    Compare objects using :func:`directCompare` and log the result
+
+    Parameters
+    ----------
+    obj0: str or float or int or :class:`numpy.ndarray`
+    obj1: str or float or int or :class:`numpy.ndarray`
+        Objects to compare
+    {compLimits}
+    quantity: str
+        Description of the value being compared. Will be
+        used to notify the user about any differences
+
+    Returns
+    -------
+    bool:
+        ``True`` if the objects agree according to tolerances, or numerics
+        differ less than ``upper``. ``False`` otherwise
+
+    Raises
+    ------
+    TypeError:
+        If the objects being compared are not supported by
+        :func:`directCompare`.  Developers should either extend the
+        function or utilize a different comparison function
+
+    See Also
+    --------
+    * :func:`directCompare` - function that does the comparison
+    * :func:`getOverlaps` - function for evaluating values with uncertainties
+    * :func:`getLogOverlaps` - function that logs the result of stastistical
+      comparisions
+    """
+    result = directCompare(obj0, obj1, lower, upper)
+    if result < 0:  # failures
+        if result == -1:
+            raise TypeError(
+                  "directCompare is not configured to make tests on objects "
+                  "of type {tp}\n\tQuantity: {k}\n\tUsers: Create a issue on "
+                  "GitHub to alert developers.\n\tDevelopers: Update this "
+                  "function or create a compare function "
+                  "for {tp} objects.".format(k=quantity, tp=type(obj0)))
+    noticeTuple = [obj0, obj1, quantity]
+    if result in COMPARE_STATUS_CODES:
+        func, returnV = COMPARE_STATUS_CODES[result]
+        func(*noticeTuple)
+        return returnV
+    raise ValueError("Received value of {} from directCompare. Not sure "
+                     "what this means.")
+
+
+def splitDictByKeys(map0, map1, keySet=None):
+    """
+    Return various sub-sets and dictionaries from two maps.
+
+    Used to test the internal workings on :func:`getKeyMatchingShapes`
+
+    Parameters
+    ----------
+    map0: dict
+    map1: dict
+        Dictionaries to compare
+    keySet: set or None
+        Iterable collection of keys found in ``map0`` and ``map1``.
+        Missing keys will be returned from this function under
+        the ``missing0`` and ``missing1`` sets. If ``None``, take
+        to be the set of keys that exist in both maps
+
+    Returns
+    -------
+    missing0: set
+        Keys that exist in ``keySet`` but not in ``map0``
+    missing1: set
+        Keys that exist in ``keySet`` but not in ``map1``
+    differentTypes: dict
+        Dictionary with tuples ``{key: (t0, t1)}`` indicating the values
+        ``map0[key]`` and ``map1[key]`` are of different types
+    badShapes: dict
+        Dictionary with tuples ``{key: (t0, t1)}`` indicating the values
+        ``map0[key]`` and ``map1[key]`` are arrays of different shapes
+    goodKeys: set
+        Keys found in both ``map0`` and ``map1`` that are of the same type
+        or point to arrays of the same shape
+    """
+    if keySet is None:
+        k1 = set(map1.keys())
+        keySet = k1.intersection(set(map0.keys()))
+    missing = {0: set(), 1: set()}
+    differentTypes = {}
+    badShapes = {}
+    goodKeys = set()
+    for key in keySet:
+        if key not in map0 or key not in map1:
+            for mapD, misK in zip((map0, map1), (0, 1)):
+                if key not in mapD:
+                    missing[misK].add(key)
+            continue
+        v0 = map0[key]
+        v1 = map1[key]
+        t0 = type(v0)
+        t1 = type(v1)
+        if t0 != t1:
+            differentTypes[key] = (t0, t1)
+            continue
+        if t0 is ndarray:
+            if v0.shape != v1.shape:
+                badShapes[key] = (v0.shape, v1.shape)
+                continue
+        goodKeys.add(key)
+
+    return missing[0], missing[1], differentTypes, badShapes, goodKeys
+
+
+def getKeyMatchingShapes(map0, map1, quantity, keySet=None, desc0='first',
+                         desc1='second'):
+    """
+    Return a set of keys in map0/1 that point to arrays with identical shapes.
+
+    Parameters
+    ----------
+    keySet: set or list or tuple or iterable or None
+        Iterable container with keys that exist in map0 and map1. The contents
+        of ``map0/1`` under these keys will be compared. If ``None``,
+        will be determined by :func:`splitDictByKeys`
+    map0: dict
+    map1: dict
+        Two dictionaries containing at least all the keys in ``keySet``.
+        Objects under keys in ``keySet`` will have their sizes compared if
+        they are :class:`numpy.ndarray`. Non-arrays will be included only
+        if their types are identical
+    quantity: str
+        Indicator as to what is being compared, e.g. ``'metadata'``
+    desc0: str
+    decs1: str
+        Descriptions of the two dictionaries being compared. Used to alert the
+        user to the shortcomings of the two dictionaries
+
+    Returns
+    -------
+    set:
+        Set of all keys that exist in both dictionaries and are either
+        identical types, or are arrays of identical shapes
+
+    See Also
+    --------
+    * :func:`splitDictByKeys`
+    """
+    missing0, missing1, differentTypes, badShapes, goodKeys = (
+            splitDictByKeys(map0, map1, keySet))
+
+    # raise some messages
+    if any(missing0) or any(missing1):
+        logMissingKeys(quantity, desc0, desc1, missing0, missing1)
+    if differentTypes:
+        logBadTypes(quantity, desc0, desc1, differentTypes)
+    if badShapes:
+        logBadShapes(quantity, desc0, desc1, badShapes)
+    return goodKeys
+
+
+@compareDocDecorator
+def getOverlaps(arr0, arr1, unc0, unc1, sigma, relative=True):
+    r"""
+    Return the indicies of overlapping confidence intervals
+
+    Parameters
+    ----------
+    arr0: :class:`numpy.ndarray`
+    arr1: :class:`numpy.ndarray`
+        Arrays containing the expected values to be compared
+    unc0: :class:`numpy.ndarray`
+    unc1: :class:`numpy.ndarray`
+        Associated absolute uncertainties, :math:`1\sigma`,
+        corresponding to the values in ``arr0`` and ``arr1``
+    {sigma}
+    relative: bool
+        True if uncertainties are relative and should be multiplied
+        by their respective values. Otherwise, assume values are
+        absolute
+
+    Returns
+    -------
+    :class:`numpy.ndarray`
+        Boolean array of equal shape to incoming arrays.
+        Every index with ``True`` as the value indicates that
+        the confidence intervals for the arrays overlap
+        at those indices.
+
+    Examples
+    --------
+    Using absolute uncertainties::
+
+        >>> from numpy import ones, zeros, array
+        >>> a0 = ones(4)
+        >>> a1 = ones(4) * 0.5
+        >>> u0 = array([0, 0.2, 0.1, 0.2])
+        >>> u1 = array([1, 0.55, 0.25, 0.4])
+
+    Here, the first point in the confidence interval for
+    ``a0`` is completely contained within that of ``a1``.
+    The upper limit of ``a1[1]`` is contained within the confidence
+    interval for ``a0``.
+    The confidence intervals for the third point do not overlap,
+    while the lower bound of ``a0[3]`` is within the confidence interval
+    of ``a1[3]``.
+    ::
+
+        >>> getOverlaps(a0, a1, u0, u1, 1, relative=False)
+        array([True, True, False, True])
+
+    This function also works for multi-dimensional arrays as well.
+    ::
+
+        >>> a2 = a0.reshape(2, 2)
+        >>> a3 = a1.reshape(2, 2)
+        >>> u2 = u0.reshape(2, 2)
+        >>> u3 = u1.reshape(2, 2)
+        >>> getOverlaps(a2, a3, u2, u3 1, relative=False)
+        array([[ True,  True],
+               [False, False])
+
+    Raises
+    ------
+    IndexError
+        If the shapes of incoming arrays do not agree
+
+    See Also
+    --------
+    * :func:`getLogOverlaps` - High-level function that
+      uses this to report if two values have overlapping
+      confidence intervals 
+    """
+    shapes = {arg.shape for arg in (arr0, arr1, unc1, unc0)}
+    if len(shapes) != 1:
+        shapes = [str(a.shape) for a in [arr0, arr1, unc1, unc0]]
+        raise IndexError("Array shapes do not agree:\n{}"
+                         .format(', '.join(shapes)))
+    err0 = fabs(unc0 * sigma)
+    err1 = fabs(unc1 * sigma)
+
+    if relative:
+        err0 *= arr0
+        err1 *= arr1
+
+    min0 = arr0 - err0
+    max0 = arr0 + err0
+    min1 = arr1 - err1
+    max1 = arr1 + err1
+
+    overlap = zeros_like(arr0, dtype=bool)
+
+    # Where values are identical to numerical precision
+    overlap[arr0 == arr1] = True
+
+    min0le1 = min0 <= min1
+    max0ge1 = max0 >= max1
+    min1le0 = min1 <= min0
+    max1ge0 = max1 >= max0
+
+    # locations where condidence intervals are completely contained
+    # in the other set
+    cont0In1 = min0le1 * (min0le1 == max0ge1)
+    overlap[cont0In1] = True
+    cont1In0 = min1le0 * (min1le0 == max1ge0)
+    overlap[cont1In0] = True
+
+    # locations where min of 0 is less than 1, but max 0 > min 1
+    # and the opposite
+    overlap[min0le1 * (max0 >= min1)] = True
+    overlap[min1le0 * (max1 >= min0)] = True
+
+    # locations where max 0 > max 1, but min 0 < max 1
+    # and the opposite
+    overlap[max0ge1 * (min0 <= max1)] = True
+    overlap[max1ge0 * (min1 <= max0)] = True
+
+    return overlap
+
+
+@compareDocDecorator
+def getLogOverlaps(quantity, arr0, arr1, unc0, unc1, sigma, relative=True):
+    """
+    Wrapper around :func:`getOverlaps` that logs the result
+
+    Parameters
+    ----------
+    quantity: str
+        Name of the value being compared
+    arr0: :class:`numpy.ndarray`
+    arr1: :class:`numpy.ndarray`
+    unc0: :class:`numpy.ndarray`
+    unc1: :class:`numpy.ndarray`
+        Arrays and their uncertainties to evaluate
+    {sigma}
+    relative: bool
+        If uncertainties are relative. Otherwise, assume absolute
+        uncertainties.
+
+    Returns
+    -------
+    bool:
+        ``True`` if all locations ``arr0`` and ``arr1`` are either
+        identical or within allowable statistical variations.
+
+    See Also
+    --------
+    * :func:`getOverlaps` - This function performs all the comparisons
+      while this function simply reports the output using
+      :mod:`serpentTools.messages`
+    """
+
+    if (arr0 == arr1).all():
+        logIdenticalWithUncs(arr0, unc0, unc1, quantity)
+        return True
+    overlaps = getOverlaps(arr0, arr1, unc0, unc1, sigma, relative)
+    if overlaps.all():
+        logInsideConfInt(arr0, unc0, arr1, unc1, quantity)
+        return True
+    logOutsideConfInt(arr0, unc0, arr1, unc1, quantity)
+    return False


Test patch:
diff --git a/serpentTools/tests/test_utils.py b/serpentTools/tests/test_utils.py
index 6c9fc9d..2cb76b5 100644
--- a/serpentTools/tests/test_utils.py
+++ b/serpentTools/tests/test_utils.py
@@ -2,16 +2,24 @@
 Test the various utilities in serpentTools/utils.py
 """
 
-import unittest
+from unittest import TestCase
 
-from numpy import arange, ndarray, array
+from numpy import arange, ndarray, array, ones, ones_like, zeros_like
 from numpy.testing import assert_array_equal
 from six import iteritems
 
-from serpentTools.utils import convertVariableName, splitValsUncs, str2vec
+from serpentTools.utils import (
+    convertVariableName,
+    splitValsUncs,
+    str2vec,
+    getCommonKeys,
+    directCompare,
+    getOverlaps,
+    splitDictByKeys,
+    )
 
 
-class VariableConverterTester(unittest.TestCase):
+class VariableConverterTester(TestCase):
     """Class for testing our variable name conversion function."""
 
     def test_variableConversion(self):
@@ -19,14 +27,14 @@ class VariableConverterTester(unittest.TestCase):
         testCases = {
             "VERSION": "version",
             "INF_KINF": "infKinf",
-            "ADJ_PERT_KEFF_SENS": "adjPertKeffSens"
-            ,}
+            "ADJ_PERT_KEFF_SENS": "adjPertKeffSens",
+            }
         for serpentStyle, expected in iteritems(testCases):
             actual = convertVariableName(serpentStyle)
             self.assertEqual(expected, actual, msg=serpentStyle)
 
 
-class VectorConverterTester(unittest.TestCase):
+class VectorConverterTester(TestCase):
     """Class for testing the str2vec function"""
 
     def setUp(self):
@@ -54,13 +62,13 @@ class VectorConverterTester(unittest.TestCase):
             actual = str2vec(case, of=valType, out=outType)
             self.assertIsInstance(actual, compareType, msg=case)
             ofRightType = [isinstance(xx, valType) for xx in actual]
-            self.assertTrue(all(ofRightType), 
+            self.assertTrue(all(ofRightType),
                             msg="{} -> {}, {}".format(case, actual,
                                                       type(actual)))
             self.assertEqual(expected, actual, msg=case)
 
 
-class SplitValsTester(unittest.TestCase):
+class SplitValsTester(TestCase):
     """Class that tests splitValsUncs."""
 
     def setUp(self):
@@ -80,9 +88,245 @@ class SplitValsTester(unittest.TestCase):
         copyV, copyU = splitValsUncs(self.input, copy=True)
         for view, copy, msg in zip(
                 (viewV, viewU), (copyV, copyU), ('value', 'uncertainty')):
-            assert_array_equal(view, copy,err_msg=msg)
+            assert_array_equal(view, copy, err_msg=msg)
             self.assertFalse(view is copy, msg=msg)
 
-if __name__ == '__main__':
-    unittest.main()
 
+class CommonKeysTester(TestCase):
+    """Class that tests getCommonKeys"""
+
+    def test_goodKeys_dict(self):
+        """Verify a complete set of keys is returned from getCommonKeys"""
+        d0 = {1: None, '2': None, (1, 2, 3): "tuple"}
+        expected = set(d0.keys())
+        actual = getCommonKeys(d0, d0, 'identical dictionary')
+        self.assertSetEqual(expected, actual,
+                            msg="Passed two dictionaries")
+        actual = getCommonKeys(d0, expected, 'dictionary and set')
+        self.assertSetEqual(expected, actual,
+                            msg="Passed dictionary and set")
+
+    def test_getKeys_missing(self):
+        """Verify that missing keys are properly notified."""
+        log = []
+        d0 = {1, 2, 3}
+        emptyS = set()
+        desc0 = "xObj0x"
+        desc1 = "xObj1x"
+        common = getCommonKeys(d0, emptyS, 'missing keys', desc0, desc1,
+                               herald=log.append)
+        self.assertSetEqual(emptyS, common)
+        self.assertEqual(len(log), 1, msg="Failed to append warning message")
+        warnMsg = log[0]
+        self.assertIsInstance(warnMsg, str, msg="Log messages is not string")
+        for desc in (desc0, desc1):
+            errMsg = "Description {} missing from warning message\n{}"
+            self.assertIn(desc, warnMsg, msg=errMsg.format(desc, warnMsg))
+
+
+class DirectCompareTester(TestCase):
+    """Class for testing utils.directCompare."""
+
+    NUMERIC_ITEMS = (
+        [0, 0.001],
+        [1E-8, 0],
+        [array([1, 1, ]), array([1, 1.0001])],
+    )
+
+    def checkStatus(self, expected, *args, **kwargs):
+        """Wrapper around directCompare with ``args``. Pass ``kwargs`` to
+        assertEqual."""
+        actual = directCompare(*args)
+        self.assertEqual(expected, actual, **kwargs)
+
+    def test_badTypes(self):
+        """Verify that two objects of different types return -1."""
+        status = 255
+        value = 1
+        for otherType in (bool, str):
+            self.checkStatus(status, value, otherType(value), 0, 1,
+                             msg=str(otherType))
+        asIterable = [value, ]
+        for otherType in (list, set, tuple, array):
+            self.checkStatus(status, value, otherType(asIterable), 0, 1,
+                             msg=str(otherType))
+
+    def test_identicalString(self):
+        """Verify that identical strings return 0."""
+        msg = obj = 'identicalStrings'
+        status = 0
+        self.checkStatus(status, obj, obj, 0, 1, msg=msg)
+
+    def test_dissimilarString(self):
+        """Verify returns the proper code for dissimilar strings."""
+        msg = "dissimilarStrings"
+        status = 200
+        self.checkStatus(status, 'item0', 'item1', 0, 1, msg=msg)
+
+    def _testNumericsForItems(self, status, lower, upper):
+        msg = "lower: {lower}, upper: {upper}\n{}\n{}"
+        for (obj0, obj1) in self.NUMERIC_ITEMS:
+            self.checkStatus(status, obj0, obj1, lower, upper,
+                             msg=msg.format(obj0, obj1, lower=lower,
+                                            upper=upper))
+
+    def test_acceptableLow(self):
+        """Verify returns the proper code for close numerics."""
+        lower = 5
+        upper = 1E4
+        self._testNumericsForItems(1, lower, upper)
+
+    def test_acceptableHigh(self):
+        """Verify returns the proper code for close but not quite values."""
+        lower = 0
+        upper = 1E4
+        self._testNumericsForItems(10, lower, upper)
+
+    def test_outsideTols(self):
+        """Verify returns the proper code for values outside tolerances."""
+        lower = 1E-8
+        upper = 1E-8
+        self._testNumericsForItems(100, lower, upper)
+
+
+class OverlapTester(TestCase):
+    """Class for testing the Overlapping uncertainties function."""
+
+    a0 = ones(4)
+    a1 = ones(4) * 0.5
+    u0 = array([0, 0.2, 0.1, 0.2])
+    u1 = array([1, 0.55, 0.25, 0.4])
+    expected = array([True, True, False, True])
+    sigma = 1
+    relative = False
+
+    _errMsg = "Sigma:{}\na0:\n{}\nu0:\n{}\na1:\n{}\nu1:\n{}"
+
+    def _test(self, expected, a0, a1, u0, u1, sigma, relative):
+        """Symmetric test on the data by switching the order of arguments."""
+        assert_array_equal(expected, getOverlaps(a0, a1, u0, u1, sigma,
+                                                 relative),
+                           err_msg=self._errMsg.format(a0, u0, a1, u1, sigma))
+        assert_array_equal(expected, getOverlaps(a1, a0, u1, u0, sigma,
+                                                 relative),
+                           err_msg=self._errMsg.format(sigma, a1, u1, a0, u0))
+
+    def _testWithReshapes(self, expected, a0, a1, u0, u1, sigma, shape,
+                          relative):
+        """Call symmetric test twice, using reshaped arrays the second time."""
+        self._test(expected, a0, a1, u0, u1, sigma, relative)
+        ra0, ra1, ru0, ru1  = [arg.reshape(*shape) for arg in [a0, a1, u0, u1]]
+        self._test(expected.reshape(*shape), ra0, ra1, ru0, ru1, sigma, relative)
+
+    def test_overlap_absolute(self):
+        """Verify the getOverlaps works using absolute uncertainties."""
+        self._testWithReshapes(self.expected, self.a0, self.a1, self.u0,
+                               self.u1, self.sigma, (2, 2), False)
+
+    def test_overlap_relative(self):
+        """Verify the getOverlaps works using relative uncertainties."""
+        u0 = self.u0 / self.a0
+        u1 = self.u1 / self.a1
+        self._testWithReshapes(self.expected, self.a0, self.a1, u0,
+                               u1, self.sigma, (2, 2), True)
+
+    @staticmethod
+    def _setupIdentical(nItems, shape=None):
+        arr = arange(nItems)
+        if shape is not None:
+            arr = arr.reshape(*shape)
+        unc = zeros_like(arr)
+        expected = ones_like(arr, dtype=bool)
+        return arr, unc, expected
+
+    def test_overlap_identical_1D(self):
+        """
+        Verify that all positions are found to overlap for identical arrays.
+        """
+        vec, unc, expected = self._setupIdentical(8)
+        self._test(expected, vec, vec, unc, unc, 1, False)
+
+    def test_overlap_identical_2D(self):
+        """
+        Verify that all positions are found to overlap for identical arrays.
+        """
+        vec, unc, expected = self._setupIdentical(8, (2, 4))
+        self._test(expected, vec, vec, unc, unc, 1, False)
+
+    def test_overlap_identical_3D(self):
+        """
+        Verify that all positions are found to overlap for identical arrays.
+        """
+        vec, unc, expected = self._setupIdentical(8, (2, 2, 2))
+        self._test(expected, vec, vec, unc, unc, 1, False)
+
+    def test_overlap_badshapes(self):
+        """Verify IndexError is raised for bad shapes."""
+        vec = arange(4)
+        mat = vec.reshape(2, 2)
+        with self.assertRaises(IndexError):
+            getOverlaps(vec, mat, vec, mat, 1)
+
+
+class SplitDictionaryTester(TestCase):
+    """Class for testing utils.splitDictByKeys."""
+
+    def setUp(self):
+        self.map0 = {
+            'hello': 'world',
+            'missingFrom1': True,
+            'infKeff': arange(2),
+            'float': 0.24,
+            'absKeff': arange(2),
+            'anaKeff': arange(6),
+            'notBool': 1,
+        }
+        self.map1 = {
+            'hello': 'world',
+            'infKeff': arange(2),
+            'float': 0.24,
+            'missingFrom0': True,
+            'notBool': False,
+            'anaKeff': arange(2),
+            'absKeff': arange(2),
+        }
+        self.badTypes = {'notBool': (int, bool)}
+        self.badShapes = {'anaKeff': ((6, ), (2, )), }
+        self.goodKeys = {'hello', 'absKeff', 'float', 'infKeff', }
+
+    def callTest(self, keySet=None):
+        return splitDictByKeys(self.map0, self.map1, keySet)
+
+    def test_noKeys(self):
+        """Verify that splitDictByKeys works when keySet is None."""
+        m0, m1, badTypes, badShapes, goodKeys = self.callTest(None)
+        self.assertSetEqual(set(), m0)
+        self.assertSetEqual(set(), m1)
+        self.assertDictEqual(self.badTypes, badTypes)
+        self.assertDictEqual(self.badShapes, badShapes)
+        self.assertSetEqual(self.goodKeys, goodKeys)
+
+    def test_keySet_all(self):
+        """Verify that splitDictByKeys works when keySet is all keys."""
+        keys = set(self.map0.keys())
+        keys.update(set(self.map1.keys()))
+        keys.add('NOT IN ANY MAP')
+        missing0 = set()
+        missing1 = set()
+        for key in keys:
+            if key not in self.map0:
+                missing0.add(key)
+            if key not in self.map1:
+                missing1.add(key)
+
+        m0, m1, badTypes, badShapes, goodKeys = self.callTest(keys)
+        self.assertSetEqual(missing0, m0)
+        self.assertSetEqual(missing1, m1)
+        self.assertDictEqual(self.badTypes, badTypes)
+        self.assertDictEqual(self.badShapes, badShapes)
+        self.assertSetEqual(self.goodKeys, goodKeys)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()