Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #226 (2018-08-09 14:01:07)

[DEV] Add utils.compare module
Places all of the utilities for comparisons in a new module `serpentTools/utils/compare.py`.  
Cleans up the code, and assists in closing #150 

Make sure you have read over the developer guide to ease
the review process. These include:

- [x] PR fits the [project scope](http://serpent-tools.readthedocs.io/en/latest/develop/contributing.html#project-scope)
- [x] Code to be committed is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and matches the [code style](http://serpent-tools.readthedocs.io/en/latest/develop/codestyle.html#code-style) of the project
- [x] New and/or changed features are [well documented](http://serpent-tools.readthedocs.io/en/latest/develop/documentation.html#documentation) and have adequate testing


Patch:
diff --git a/docs/develop/comparisons.rst b/docs/develop/comparisons.rst
index 6170586..8ac3e90 100644
--- a/docs/develop/comparisons.rst
+++ b/docs/develop/comparisons.rst
@@ -79,21 +79,23 @@ 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.compare.compareDictOfArrays
 
-.. autofunction:: serpentTools.utils.getCommonKeys
+.. autofunction:: serpentTools.utils.compare.getCommonKeys
 
-.. autofunction:: serpentTools.utils.directCompare
+.. autofunction:: serpentTools.utils.compare.directCompare
 
-.. autofunction:: serpentTools.utils.logDirectCompare
+.. autofunction:: serpentTools.utils.compare.logDirectCompare
 
-.. autofunction:: serpentTools.utils.splitdictByKeys
+.. autofunction:: serpentTools.utils.compare.splitdictByKeys
 
-.. autofunction:: serpentTools.utils.getKeyMatchingShapes
+.. autofunction:: serpentTools.utils.compare.getKeyMatchingShapes
 
-.. autofunction:: serpentTools.utils.getOverlaps
+.. autofunction:: serpentTools.utils.compare.getOverlaps
 
-.. autofunction:: serpentTools.utils.getLogOverlaps
+.. autofunction:: serpentTools.utils.compare.getLogOverlaps
+
+.. autofunction:: serpentTools.utils.docstrings.compareDocDecorator
 
 .. _dev-comp-log:
 
diff --git a/serpentTools/objects/base.py b/serpentTools/objects/base.py
index 4c283d0..7244751 100644
--- a/serpentTools/objects/base.py
+++ b/serpentTools/objects/base.py
@@ -12,15 +12,9 @@ from serpentTools.messages import debug, warning, SerpentToolsException, info
 from serpentTools.plot import plot, cartMeshPlot
 from serpentTools.utils import (
     magicPlotDocDecorator, formatPlot, DETECTOR_PLOT_LABELS,
-    compareDocDecorator,
+    compareDocDecorator, DEF_COMP_LOWER, DEF_COMP_SIGMA, DEF_COMP_UPPER,
 )
-
-#
-# Defaults for comparison
-#
-DEF_COMP_LOWER = 0
-DEF_COMP_UPPER = 10
-DEF_COMP_SIGMA = 2
+from serpentTools.settings import rc
 
 
 class BaseObject(object):
diff --git a/serpentTools/objects/containers.py b/serpentTools/objects/containers.py
index 1084f09..e1c3fd2 100644
--- a/serpentTools/objects/containers.py
+++ b/serpentTools/objects/containers.py
@@ -17,16 +17,16 @@ from numpy import array, arange, hstack, ndarray, zeros_like
 from serpentTools.settings import rc
 from serpentTools.objects.base import NamedObject, BaseObject
 from serpentTools.utils import (
-    COMPARE_DOC_SIGMA,
-    COMPARE_DOC_TYPE_ERR,
     convertVariableName,
     getKeyMatchingShapes,
     logDirectCompare,
     getLogOverlaps,
+    compareDocReplacer,
     compareDocDecorator,
     magicPlotDocDecorator,
     formatPlot,
 )
+
 from serpentTools.objects.base import (DEF_COMP_LOWER,
                                        DEF_COMP_UPPER, DEF_COMP_SIGMA)
 from serpentTools.messages import (
@@ -509,14 +509,14 @@ class HomogUniv(NamedObject):
 
         return similar
 
-    __docCompare = """
+    __docCompare = compareDocReplacer("""
     Return ``True`` if contents of ``{qty}Exp`` and ``{qty}Unc`` agree
 
     Parameters
     ----------
     other: :class:`HomogUniv`
         Object from which to grab group constant dictionaries
-    

+    {sigma}
 
     Returns
     bool
@@ -524,8 +524,8 @@ class HomogUniv(NamedObject):
         and if those values have overlapping confidence intervals
     Raises
     ------
-    <e>
-    """.replace('
', COMPARE_DOC_SIGMA).replace('<e>', COMPARE_DOC_TYPE_ERR)
+    {compTypeErr}
+    """)
 
     def _helpCompareGCDict(self, other, attrBase, sigma):
         """
diff --git a/serpentTools/parsers/_collections.py b/serpentTools/parsers/_collections.py
index e2424a6..0b93156 100644
--- a/serpentTools/parsers/_collections.py
+++ b/serpentTools/parsers/_collections.py
@@ -3,6 +3,11 @@ Collections of objects that are helpful for the parsers
 """
 
 RES_DATA_NO_UNCS = {
+    "burnMaterials",
+    "burnMode",
+    "burnStep",
+    "iniBurnFmass",
+    "totBurnFmass",
     "resMemsize",
     "totNuclides",
     "fissionProductInhTox",
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index 91308ea..bde4c23 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -491,6 +491,7 @@ class ResultsReader(XSReader):
             myUniv = myUniverses[univKey]
             otherUniv = otherUniverses[univKey]
             similar &= myUniv.compare(otherUniv, lower, upper, sigma)
+        return similar
 
     def _cleanMetadata(self):
         """Replace some items in metadata dictionary with easier data types."""
diff --git a/serpentTools/utils/__init__.py b/serpentTools/utils/__init__.py
index e7679f1..b9dec17 100644
--- a/serpentTools/utils/__init__.py
+++ b/serpentTools/utils/__init__.py
@@ -1,731 +1,7 @@
 """
 Commonly used functions and utilities
 """
-from re import compile
-
-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.utils.core import *  # noqa
 from serpentTools.utils.docstrings import *  # noqa
+from serpentTools.utils.compare import *  # noqa
 from serpentTools.utils.plot import *  # noqa
-
-# Regular expressions
-
-STR_REGEX = compile(r'\'.+\'')  # string
-VEC_REGEX = compile(r'(?<==.)\[.+?\]')  # vector
-SCALAR_REGEX = compile(r'=.+;')  # scalar
-FIRST_WORD_REGEX = compile(r'^\w+')  # first word in the line
-
-
-def str2vec(iterable, of=float, out=array):
-    """
-    Convert a string or other iterable to vector.
-
-    Parameters
-    ----------
-    iterable: str or iterable
-        If string, will be split with ``split(splitAt)``
-        to create a list. Every item in this list, or original
-        iterable, will be iterated over and converted accoring
-        to the other arguments.
-    of: type
-        Convert each value in ``iterable`` to this data type.
-    out: type
-        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,
-        converted to type ``of``.
-
-    Examples
-    --------
-    ::
-
-        >>> v = "1 2 3 4"
-        >>> str2vec(v)
-        array([1., 2., 3., 4.,])
-
-        >>> str2vec(v, int, list)
-        [1, 2, 3, 4]
-
-        >>> x = [1, 2, 3, 4]
-        >>> str2vec(x)
-        array([1., 2., 3., 4.,])
-
-    """
-    vec = (iterable.split() if isinstance(iterable, str)
-           else iterable)
-    return out([of(xx) for xx in vec])
-
-
-def splitValsUncs(iterable, copy=False):
-    """
-    Return even and odd indexed values from iterable
-
-    Designed to extract expected values and uncertainties from
-    SERPENT vectors/matrices of the form
-    ``[x1, u1, x2, u2, ...]``
-
-    Slices along the last axis present on ``iterable``, e.g.
-    columns in 2D matrix.
-
-    Parameters
-    ----------
-    iterable: :class:`numpy.ndarray`or iterable
-        Initial arguments to be processed. If not
-        :class:`numpy.ndarray`, then strings will be converted
-        by calling :func:`str2vec`. Lists and tuples
-        will be sent directly to arrays with
-        :func:`numpy.array`.
-    copy: bool
-        If true, return a unique instance of the values
-        and uncertainties. Otherwise, returns a view
-        per numpy slicing methods
-
-    Returns
-    -------
-    :class:`numpy.ndarray`
-        Even indexed values from ``iterable``
-    :class:`numpy.ndarray`
-        Odd indexed values from ``iterable``
-
-    Examples
-    --------
-    ::
-
-        >>> v = [1, 2, 3, 4]
-        >>> splitValsUncs(v)
-        array([1, 3]), array([2, 4])
-
-        >>> line = "1 2 3 4"
-        >>> splitValsUnc(line)
-        array([1, 3]), array([2, 4])
-
-        >>> v = [[1, 2], [3, 4]]
-        >>> splitValsUncs(v)
-        array([[1], [3]]), array([[2], [4]])
-
-    """
-
-    if not isinstance(iterable, ndarray):
-        iterable = (str2vec(iterable) if isinstance(iterable, str)
-                    else array(iterable))
-    vals = iterable[..., 0::2]
-    uncs = iterable[..., 1::2]
-    if copy:
-        return vals.copy(), uncs.copy()
-    return vals, uncs
-
-
-def convertVariableName(variable):
-    """
-    Return the mixedCase version of a SERPENT variable.
-
-    Parameters
-    ----------
-    variable: str
-        ``SERPENT_STYLE`` variable name to be converted
-
-    Returns
-    -------
-    str:
-        Variable name that has been split at underscores and
-        converted to ``mixedCase``
-
-    Examples
-    --------
-    ::
-
-        >>> v = "INF_KINF"
-        >>> convertVariableName(v)
-        infKinf
-
-        >>> v = "VERSION"
-        >>> convertVariableName(v)
-        version
-
-    """
-    lowerSplits = [item.lower() for item in variable.split('_')]
-    if len(lowerSplits) == 1:
-        return lowerSplits[0]
-    return lowerSplits[0] + ''.join([item.capitalize()
-                                     for item in lowerSplits[1:]])
-
-
-LEADER_TO_WIKI = "http://serpent.vtt.fi/mediawiki/index.php/"
-
-
-def linkToWiki(subLink, text=None):
-    """
-    Return a string that will render as a hyperlink to the SERPENT wiki.
-
-    Parameters
-    ----------
-    subLink: str
-        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.
-
-    Returns
-    -------
-    str:
-        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',
-    ...            "Depletion Output")
-    `Depletion Output <http://serpent.vtt.fi/mediawiki/index.php/
-    Description_of_output_files#Burnup_calculation_output>`_
-    """
-    fullLink = LEADER_TO_WIKI + subLink
-    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
diff --git a/serpentTools/utils/compare.py b/serpentTools/utils/compare.py
new file mode 100644
index 0000000..9b958b2
--- /dev/null
+++ b/serpentTools/utils/compare.py
@@ -0,0 +1,537 @@
+"""
+Comparison utilities
+"""
+from numpy import fabs, where, zeros_like, ndarray
+
+from serpentTools.messages import (
+    error,
+    logIdentical,
+    logNotIdentical,
+    logAcceptableLow,
+    logAcceptableHigh,
+    logOutsideTols,
+    logDifferentTypes,
+    logMissingKeys,
+    logBadTypes,
+    logBadShapes,
+    logIdenticalWithUncs,
+    logInsideConfInt,
+    logOutsideConfInt,
+)
+
+from serpentTools.utils.docstrings import compareDocDecorator
+
+LOWER_LIM_DIVISION = 1E-8
+"""Lower limit for denominator for division"""
+
+#
+# Defaults for comparison
+#
+DEF_COMP_LOWER = 0
+DEF_COMP_UPPER = 10
+DEF_COMP_SIGMA = 2
+
+
+@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. ``'metadata'``
+    {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:
+        keySet = set(map1.keys())
+        keySet.update(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
+
+
+@compareDocDecorator
+def compareDictOfArrays(d0, d1, desc, lower=DEF_COMP_LOWER,
+                        upper=DEF_COMP_UPPER, sigma=DEF_COMP_SIGMA,
+                        u0={}, u1={}, relative=True):
+    """
+    High-level routine for evaluating the similarities of two dictionaries
+
+    The following tests are performed
+
+        1. Find a set of keys that both exist in ``d0`` and ``d1``
+          and point to arrays with identical shapes using
+          :meth:`getKeyMatchingShapes`
+        2. For each key in this common set, compare the values
+          with :meth:`logDirectCompare` or :meth:`getLogOverlaps`.
+          The latter is used if the key exists in  ``u0`` and
+          ``u1``, provided uncertainty arrays are of identical shapes.
+
+    Parameters
+    ----------
+    d0: dict
+    d1: dict
+        Dictionaries to be compared
+    desc: str
+        Descption of the two dictionaries. What data do they represent?
+    {compLimits}
+    {sigma}
+    u0: dict
+    u1: dict
+        If uncKeys is not ``None``, then find the uncertainties for data in
+        ``d0`` and ``d1`` under the same keys.
+    relative: bool
+        If this evaluates to ``true``, then uncertainties in ``u0`` and ``u1``
+        are relative.
+
+    Returns
+    -------
+    bool
+        ``True`` If all comparisons pass
+    """
+    similar = len(d0) == len(d1)
+    keysMatchingTypes = getKeyMatchingShapes(d0, d1, desc)
+    similar &= len(d0) == len(keysMatchingTypes)
+
+    for key in sorted(keysMatchingTypes):
+        val0 = d0[key]
+        val1 = d1[key]
+        if key in u0 and key in u1:
+            unc0 = u0[key]
+            unc1 = u1[key]
+            similar &= getLogOverlaps(key, val0, val1, unc0, unc1,
+                                      sigma, relative)
+            continue
+        similar &= logDirectCompare(val0, val1, lower, upper, key)
+    return similar
diff --git a/serpentTools/utils/core.py b/serpentTools/utils/core.py
new file mode 100644
index 0000000..3a19ea7
--- /dev/null
+++ b/serpentTools/utils/core.py
@@ -0,0 +1,189 @@
+"""
+Core utilities
+"""
+
+from re import compile
+
+from numpy import array, ndarray
+
+
+# Regular expressions
+
+STR_REGEX = compile(r'\'.+\'')  # string
+VEC_REGEX = compile(r'(?<==.)\[.+?\]')  # vector
+SCALAR_REGEX = compile(r'=.+;')  # scalar
+FIRST_WORD_REGEX = compile(r'^\w+')  # first word in the line
+
+
+def str2vec(iterable, of=float, out=array):
+    """
+    Convert a string or other iterable to vector.
+
+    Parameters
+    ----------
+    iterable: str or iterable
+        If string, will be split with ``split(splitAt)``
+        to create a list. Every item in this list, or original
+        iterable, will be iterated over and converted accoring
+        to the other arguments.
+    of: type
+        Convert each value in ``iterable`` to this data type.
+    out: type
+        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,
+        converted to type ``of``.
+
+    Examples
+    --------
+    ::
+
+        >>> v = "1 2 3 4"
+        >>> str2vec(v)
+        array([1., 2., 3., 4.,])
+
+        >>> str2vec(v, int, list)
+        [1, 2, 3, 4]
+
+        >>> x = [1, 2, 3, 4]
+        >>> str2vec(x)
+        array([1., 2., 3., 4.,])
+
+    """
+    vec = (iterable.split() if isinstance(iterable, str)
+           else iterable)
+    return out([of(xx) for xx in vec])
+
+
+def splitValsUncs(iterable, copy=False):
+    """
+    Return even and odd indexed values from iterable
+
+    Designed to extract expected values and uncertainties from
+    SERPENT vectors/matrices of the form
+    ``[x1, u1, x2, u2, ...]``
+
+    Slices along the last axis present on ``iterable``, e.g.
+    columns in 2D matrix.
+
+    Parameters
+    ----------
+    iterable: :class:`numpy.ndarray`or iterable
+        Initial arguments to be processed. If not
+        :class:`numpy.ndarray`, then strings will be converted
+        by calling :func:`str2vec`. Lists and tuples
+        will be sent directly to arrays with
+        :func:`numpy.array`.
+    copy: bool
+        If true, return a unique instance of the values
+        and uncertainties. Otherwise, returns a view
+        per numpy slicing methods
+
+    Returns
+    -------
+    :class:`numpy.ndarray`
+        Even indexed values from ``iterable``
+    :class:`numpy.ndarray`
+        Odd indexed values from ``iterable``
+
+    Examples
+    --------
+    ::
+
+        >>> v = [1, 2, 3, 4]
+        >>> splitValsUncs(v)
+        array([1, 3]), array([2, 4])
+
+        >>> line = "1 2 3 4"
+        >>> splitValsUnc(line)
+        array([1, 3]), array([2, 4])
+
+        >>> v = [[1, 2], [3, 4]]
+        >>> splitValsUncs(v)
+        array([[1], [3]]), array([[2], [4]])
+
+    """
+
+    if not isinstance(iterable, ndarray):
+        iterable = (str2vec(iterable) if isinstance(iterable, str)
+                    else array(iterable))
+    vals = iterable[..., 0::2]
+    uncs = iterable[..., 1::2]
+    if copy:
+        return vals.copy(), uncs.copy()
+    return vals, uncs
+
+
+def convertVariableName(variable):
+    """
+    Return the mixedCase version of a SERPENT variable.
+
+    Parameters
+    ----------
+    variable: str
+        ``SERPENT_STYLE`` variable name to be converted
+
+    Returns
+    -------
+    str:
+        Variable name that has been split at underscores and
+        converted to ``mixedCase``
+
+    Examples
+    --------
+    ::
+
+        >>> v = "INF_KINF"
+        >>> convertVariableName(v)
+        infKinf
+
+        >>> v = "VERSION"
+        >>> convertVariableName(v)
+        version
+
+    """
+    lowerSplits = [item.lower() for item in variable.split('_')]
+    if len(lowerSplits) == 1:
+        return lowerSplits[0]
+    return lowerSplits[0] + ''.join([item.capitalize()
+                                     for item in lowerSplits[1:]])
+
+
+LEADER_TO_WIKI = "http://serpent.vtt.fi/mediawiki/index.php/"
+
+
+def linkToWiki(subLink, text=None):
+    """
+    Return a string that will render as a hyperlink to the SERPENT wiki.
+
+    Parameters
+    ----------
+    subLink: str
+        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.
+
+    Returns
+    -------
+    str:
+        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',
+    ...            "Depletion Output")
+    `Depletion Output <http://serpent.vtt.fi/mediawiki/index.php/
+    Description_of_output_files#Burnup_calculation_output>`_
+    """
+    fullLink = LEADER_TO_WIKI + subLink
+    if not text:
+        return fullLink
+    return "`{} <{}>`_".format(text, fullLink)
diff --git a/serpentTools/utils/docstrings.py b/serpentTools/utils/docstrings.py
index cb32b74..499eb16 100644
--- a/serpentTools/utils/docstrings.py
+++ b/serpentTools/utils/docstrings.py
@@ -4,8 +4,12 @@ Utilities for modifying docstrings
 from functools import wraps
 from textwrap import dedent
 
+from six import iteritems
+
 __all__ = [
     'magicPlotDocDecorator',
+    'compareDocDecorator',
+    'compareDocReplacer',
 ]
 
 _MPL_AX = ':py:class:`matplotlib.axes.Axes`'
@@ -102,3 +106,73 @@ def magicPlotDocDecorator(f):
             doc = doc.replace(lookF, replace)
     decorated.__doc__ = doc
     return decorated
+
+
+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 compareDocReplacer(doc):
+    """Make replacements for comparison docstrings."""
+    if not doc:
+        return ""
+    doc = dedent(doc)
+    for magic, replace in iteritems(COMPARE_DOC_MAPPING):
+        lookF = '{' + magic + '}'
+        if lookF in doc:
+            doc = doc.replace(lookF, dedent(replace))
+    return doc
+
+
+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)
+    decorated.__doc__ = compareDocReplacer(f.__doc__)
+    return decorated


Test patch:
diff --git a/serpentTools/tests/test_compare.py b/serpentTools/tests/test_compare.py
new file mode 100644
index 0000000..32a1b6f
--- /dev/null
+++ b/serpentTools/tests/test_compare.py
@@ -0,0 +1,80 @@
+"""
+Test object-level compare methods
+"""
+
+from serpentTools.tests.utils import TestCaseWithLogCapture
+from serpentTools.data import readDataFile
+
+
+RES_DATA_FILE = 'pwr_res.m'
+# strings that should appear in specific logging messages after formatted
+# with a single value key
+IDENTICAL_KEY_FMT = "for {} are identical"
+OVERLAPPING_KEY_FMT = "{} overlap"
+WITHIN_TOLS_KEY_FMT = "{} are different, but within tolerance"
+OUTSIDE_TOLS_KEY_FMT = "{} are outside acceptable tolerances"
+
+
+class ResultsCompareTester(TestCaseWithLogCapture):
+    """
+    Test the ResultsReader comparison methods
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        cls.r0 = readDataFile(RES_DATA_FILE)
+        cls.resultsKeys = set(cls.r0.resdata.keys())
+        cls.univKeys = set()
+        for univ in cls.r0.universes.values():
+            cls.univKeys.update(set(univ.infExp.keys()))
+            cls.univKeys.update(set(univ.b1Exp.keys()))
+            cls.univKeys.update(set(univ.gc.keys()))
+            break
+
+    def setUp(self):
+        self.r1 = readDataFile(RES_DATA_FILE)
+        TestCaseWithLogCapture.setUp(self)
+
+    def _runCompare(self, verbosity):
+        return self.r0.compare(self.r1, verbosity=verbosity)
+
+    def test_fullCompare(self):
+        """Test the primary comparison method with no modifications."""
+        out = self._runCompare('debug')
+        self.assertTrue(out, msg="Result from comparison")
+        self.assertMsgInLogs('DEBUG', 'Updated setting verbosity to debug')
+        for resKey in self.resultsKeys:
+            self.assertMsgInLogs('DEBUG', IDENTICAL_KEY_FMT.format(resKey),
+                                 partial=True)
+
+    def test_moddedResults(self):
+        """
+        Verify that the results comparison logs errors in modified data.
+        """
+        resd = self.r1.resdata
+        # drastically increase one value with uncertainties
+        # to ensure disagreement
+        resd['anaKeff'][:, ::2] *= 2
+        # slightly modify one value with uncertainties to force overlapping
+        # confidence intervals, but not identical quantities
+        resd['colKeff'][:, ::2] *= 1.01
+        resd['colKeff'][:, 1::2] *= 1.05
+        # modify a value w/o uncertainties slightly
+        resd['allocMemsize'] *= 1.01
+        # drastically modify a value w/o uncertainties
+        resd['uresAvail'] *= -2
+        out = self._runCompare('debug')
+        self.assertFalse(out)
+        self.assertMsgInLogs('ERROR', 'anaKeff', partial=True)
+        self.assertMsgInLogs(
+            'DEBUG', OVERLAPPING_KEY_FMT.format('colKeff'), partial=True)
+        self.assertMsgInLogs(
+            'WARNING', WITHIN_TOLS_KEY_FMT.format('allocMemsize'),
+            partial=True)
+        self.assertMsgInLogs(
+            'ERROR', OUTSIDE_TOLS_KEY_FMT.format('uresAvail'), partial=True)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
diff --git a/serpentTools/tests/test_utils.py b/serpentTools/tests/test_utils.py
index c15f2ed..cbe1a7d 100644
--- a/serpentTools/tests/test_utils.py
+++ b/serpentTools/tests/test_utils.py
@@ -310,8 +310,8 @@ class SplitDictionaryTester(TestCase):
     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.assertSetEqual({'missingFrom0', }, m0)
+        self.assertSetEqual({'missingFrom1', }, m1)
         self.assertDictEqual(self.badTypes, badTypes)
         self.assertDictEqual(self.badShapes, badShapes)
         self.assertSetEqual(self.goodKeys, goodKeys)