Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #233 (2018-08-24 20:51:35)

[DEV] Direct compare all arrays
This PR expands the powers of `directCompare` by allowing the function to work with vectors of strings. `directCompare` also is more flexible and maintainable, with the status codes, used by logging and testing, being variables. These variables now will propagate changes through testing and documentation without having to track down instances one by one.

This change will be helpful for assisting the comparison of the depleted material objects coming soon.

- [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/serpentTools/utils/compare.py b/serpentTools/utils/compare.py
index 4c69b08..eac6657 100644
--- a/serpentTools/utils/compare.py
+++ b/serpentTools/utils/compare.py
@@ -1,7 +1,14 @@
 """
 Comparison utilities
 """
-from numpy import fabs, where, zeros_like, ndarray
+
+from collections import Iterable
+
+from numpy.core.defchararray import equal as charEqual
+from numpy import (
+    fabs, zeros_like, ndarray, array, greater, multiply, subtract,
+    equal,
+)
 
 from serpentTools.messages import (
     error,
@@ -76,13 +83,29 @@ def getCommonKeys(d0, d1, quantity, desc0='first', desc1='second',
 
 TPL_FLOAT_INT = float, int
 
+# Error codes for direct compare
+DC_STAT_GOOD = 0
+"""Values are identical to FP precision, or by ``==`` operator."""
+DC_STAT_LE_LOWER = 1
+"""Values are not identical, but max diff <= lower tolerance."""
+DC_STAT_MID = 10
+"""Values differ with max difference between lower and upper tolerance."""
+DC_STAT_GE_UPPER = 100
+"""Values differ with max difference greater than or equal to upper tolerance"""  # noqa
+DC_STAT_NOT_IDENTICAL = 200
+"""Values should be identical but are not, e.g. strings or bools."""
+DC_STAT_DIFF_TYPES = 255
+"""Values are of different types"""
+DC_STAT_NOT_IMPLEMENTED = -1
+"""Direct compare is not implemented for these types"""
+
 COMPARE_STATUS_CODES = {
-    0: (logIdentical, True),
-    1: (logAcceptableLow, True),
-    10: (logAcceptableHigh, True),
-    200: (logNotIdentical, False),
-    100: (logOutsideTols, False),
-    255: (logDifferentTypes, False),
+    DC_STAT_GOOD: (logIdentical, True),
+    DC_STAT_LE_LOWER: (logAcceptableLow, True),
+    DC_STAT_MID: (logAcceptableHigh, True),
+    DC_STAT_NOT_IDENTICAL: (logNotIdentical, False),
+    DC_STAT_GE_UPPER: (logOutsideTols, False),
+    DC_STAT_DIFF_TYPES: (logDifferentTypes, False),
 }
 """Keys of status codes with ``(caller, return)`` values."""
 
@@ -110,17 +133,17 @@ def directCompare(obj0, obj1, lower, upper):
     int:
         Status code of the comparison.
 
-        * 0 - Values are identical to floating point precision or,
+        * {good} - 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
+        * {leLower} - Values are not identical, but the max difference
           is less than ``lower``.
-        * 10 - Values differ, with the max difference greater
+        * {mid} - 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),
+        * {geUpper} - Values differ by greater than or equal to ``upper``
+        * {notIdentical} - Values should be identical (strings, booleans),
           but are not
-        * 255 - Values are of different types
-        * -1 - Type comparison is not supported. This means that
+        * {diffTypes} - Values are of different types
+        * {notImplemented} - Type comparison is not supported. This means that
           developers should either implement a test for this
           data type, or use a different function
 
@@ -131,30 +154,71 @@ def directCompare(obj0, obj1, lower, upper):
     """
     type0 = type(obj0)
     type1 = type(obj1)
+
     if type0 != type1:
+        # can still compare floats and ints easily
         if type0 not in TPL_FLOAT_INT or type1 not in TPL_FLOAT_INT:
-            return 255
+            return DC_STAT_DIFF_TYPES
     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
+            return DC_STAT_NOT_IDENTICAL
+        return DC_STAT_GOOD
+
+    # Convert all to numpy arrays
+    if not isinstance(obj0, Iterable):
+        obj0 = array([obj0])
+        obj1 = array([obj1])
+    else:
+        # convert to array, but return if data-type is object
+        # need some indexable structure so dicts and sets won't work
+        obj0 = array(obj0)
+        obj1 = array(obj1)
+        if obj0.dtype.name == 'object':
+            return DC_STAT_NOT_IMPLEMENTED
+
+    if not upper:
+        return _directCompareIdentical(obj0, obj1)
+    return _directCompareWithTols(obj0, obj1, lower, upper)
+
+
+def _directCompareIdentical(obj0, obj1):
+    """Compare arrays that should be identical"""
+    # special case for strings
+    if obj0.dtype.name[:3] == 'str':
+        compArray = charEqual(obj0, obj1)
+    else:
+        compArray = equal(obj0, obj1)
+    if compArray.all():
+        return DC_STAT_GOOD
+    return DC_STAT_NOT_IDENTICAL
+
+
+def _directCompareWithTols(obj0, obj1, lower, upper):
+    """Compare arrays that have some allowable tolerances"""
+    diff = multiply(
+        fabs(subtract(obj0, obj1)), 100
+    )
+    nonZI = greater(fabs(obj0), LOWER_LIM_DIVISION)
+    diff[nonZI] /= obj0[nonZI]
+    maxDiff = diff.max()
+    if maxDiff < LOWER_LIM_DIVISION:
+        return DC_STAT_GOOD
+    if maxDiff <= lower:
+        return DC_STAT_LE_LOWER
+    if maxDiff >= upper:
+        return DC_STAT_GE_UPPER
+    return DC_STAT_MID
+
+
+directCompare.__doc__ = directCompare.__doc__.format(
+    good=DC_STAT_GOOD,
+    leLower=DC_STAT_LE_LOWER,
+    mid=DC_STAT_MID,
+    geUpper=DC_STAT_GE_UPPER,
+    notIdentical=DC_STAT_NOT_IDENTICAL,
+    diffTypes=DC_STAT_DIFF_TYPES,
+    notImplemented=DC_STAT_NOT_IMPLEMENTED,
+)
 
 
 @compareDocDecorator
@@ -194,7 +258,7 @@ def logDirectCompare(obj0, obj1, lower, upper, quantity):
     """
     result = directCompare(obj0, obj1, lower, upper)
     if result < 0:  # failures
-        if result == -1:
+        if result == DC_STAT_NOT_IMPLEMENTED:
             raise TypeError(
                 "directCompare is not configured to make tests on objects "
                 "of type {tp}\n\tQuantity: {k}\n\tUsers: Create a issue on "


Test patch:
diff --git a/serpentTools/tests/test_utils.py b/serpentTools/tests/test_utils.py
index cbe1a7d..67a7862 100644
--- a/serpentTools/tests/test_utils.py
+++ b/serpentTools/tests/test_utils.py
@@ -16,6 +16,13 @@ from serpentTools.utils import (
     directCompare,
     getOverlaps,
     splitDictByKeys,
+    DC_STAT_GOOD,
+    DC_STAT_LE_LOWER,
+    DC_STAT_MID,
+    DC_STAT_GE_UPPER,
+    DC_STAT_NOT_IDENTICAL,
+    DC_STAT_NOT_IMPLEMENTED,
+    DC_STAT_DIFF_TYPES,
 )
 
 
@@ -142,15 +149,15 @@ class DirectCompareTester(TestCase):
         [array([1, 1, ]), array([1, 1.0001])],
     )
 
-    def checkStatus(self, expected, *args, **kwargs):
+    def checkStatus(self, expected, obj0, obj1, lower, upper, msg=None):
         """Wrapper around directCompare with ``args``. Pass ``kwargs`` to
         assertEqual."""
-        actual = directCompare(*args)
-        self.assertEqual(expected, actual, **kwargs)
+        actual = directCompare(obj0, obj1, lower, upper)
+        self.assertEqual(expected, actual, msg=msg)
 
     def test_badTypes(self):
         """Verify that two objects of different types return -1."""
-        status = 255
+        status = DC_STAT_DIFF_TYPES
         value = 1
         for otherType in (bool, str):
             self.checkStatus(status, value, otherType(value), 0, 1,
@@ -163,13 +170,13 @@ class DirectCompareTester(TestCase):
     def test_identicalString(self):
         """Verify that identical strings return 0."""
         msg = obj = 'identicalStrings'
-        status = 0
+        status = DC_STAT_GOOD
         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
+        status = DC_STAT_NOT_IDENTICAL
         self.checkStatus(status, 'item0', 'item1', 0, 1, msg=msg)
 
     def _testNumericsForItems(self, status, lower, upper):
@@ -183,19 +190,36 @@ class DirectCompareTester(TestCase):
         """Verify returns the proper code for close numerics."""
         lower = 5
         upper = 1E4
-        self._testNumericsForItems(1, lower, upper)
+        self._testNumericsForItems(DC_STAT_LE_LOWER, 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)
+        self._testNumericsForItems(DC_STAT_MID, 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)
+        self._testNumericsForItems(DC_STAT_GE_UPPER, lower, upper)
+
+    def test_notImplemented(self):
+        """Check the not-implemented cases."""
+        self.checkStatus(DC_STAT_NOT_IMPLEMENTED, {'hello': 'world'},
+                         {'hello': 'world'}, 0, 0,
+                         msg="testing on dictionaries")
+
+    def test_stringArrays(self):
+        """Verify the function handles string arrays properly."""
+        stringList = ['hello', 'world', '1', '-1']
+        vec0 = array(stringList)
+        self.checkStatus(DC_STAT_GOOD, vec0, vec0, 0, 0,
+                         msg='Identical string arrays')
+        vec1 = array(stringList)
+        vec1[-1] = 'foobar'
+        self.checkStatus(DC_STAT_NOT_IDENTICAL, vec0, vec1, 0, 0,
+                         msg='Dissimilar string arrays')
 
 
 class OverlapTester(TestCase):