Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #329 (2019-08-05 12:11:13)

API Better handling of discontinuity factors
Resolves an issue where all data was fed to the `HomogUniv` objects under the assumption that everything was a vector with uncertainties. Some quantities, like `infKeff` are now stored as single scalars, rather than vectors of a single item. Other quantities, like `dfSurfArea`, are stored as vectors but do not have any uncertainties. This may cause some issues if people expect a single-valued vector when pulling `infKeff`, so this is delayed to the next major release, `0.8.0`, and marked as containing [potentially] incompatible API changes


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index f5166a5..ea31701 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -6,6 +6,18 @@
 Changelog
 =========
 
+Next
+====
+
+* Better handling of discontinuity factors
+* |HomogUniv| objects no longer automatically convert data to arrays
+
+Incompatible API Changes
+------------------------
+
+* Values are stored in array form on |HomogUniv| when it makes sense.
+  For example, values like ``infKinf`` are stored as scalars.
+
 .. _v0.7.1
 
 :release-tag:`0.7.1`
diff --git a/serpentTools/objects/containers.py b/serpentTools/objects/containers.py
index 8a8984c..b45f873 100644
--- a/serpentTools/objects/containers.py
+++ b/serpentTools/objects/containers.py
@@ -7,14 +7,13 @@ Contents
 * :py:class:`~serpentTools.objects.containers.BranchContainer`
 
 """
-from re import compile
 from itertools import product
 from warnings import warn
 from numbers import Real
 
 from six import iteritems, PY2
 from matplotlib import pyplot
-from numpy import array, arange, hstack, ndarray, zeros_like
+from numpy import arange, hstack, ndarray, zeros_like
 
 from serpentTools.settings import rc
 from serpentTools.objects.base import NamedObject, BaseObject
@@ -63,9 +62,6 @@ __all__ = (
 )
 
 
-CRIT_RE = compile('K[EI][NF]F')
-
-
 class HomogUniv(NamedObject):
     """
     Class for storing homogenized universe specifications and retrieving them
@@ -241,14 +237,9 @@ class HomogUniv(NamedObject):
         """
         Return the new value to be stored after some cleaning.
 
-        Makes sure all vectors, everything but keff/kinf data, are
-        converted to numpy arrays. Reshapes scattering matrices
-        if number of groups is known and ``xs.reshapeScatter``
+        Reshapes scattering matrices if number of groups
+        is known and ``xs.reshapeScatter``
         """
-        if not isinstance(value, ndarray):
-            value = array(value)
-        if CRIT_RE.search(name):
-            return value[0]
         ng = self.numGroups
         if self.__reshaped and name in SCATTER_MATS:
             if ng is None:
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index 2e2d2e4..c7edc3b 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -1,5 +1,7 @@
 """Parser responsible for reading the ``*res.m`` files"""
 from collections import OrderedDict
+from numbers import Real
+import re
 from six import iteritems
 
 from numpy import array, vstack, empty
@@ -89,6 +91,51 @@ METADATA_CONV = {
 Convert items in metadata dictionary from arrays to these data types
 """
 
+VAR_NO_CONV = {
+    "DF_SURFACE",
+}
+
+DF_CONV = {
+    # No uncertainty, convert to int
+    int: {
+        "DF_SYM",
+        "DF_N_SURF",
+        "DF_N_CORN",
+    },
+    # No uncertainty, convert to float
+    float: {
+        "DF_VOLUME",
+    },
+    # No uncertainty, convert to array of float
+    str2vec: {
+        "DF_SURF_AREA",
+        "DF_MID_AREA",
+        "DF_CORN_AREA,",
+        'MICRO_NG',
+        'MICRO_E',
+        'MACRO_NG',
+        'MACRO_E',
+    },
+}
+"""
+Functions for converting variable data to various types
+"""
+
+UNIV_K_RE = re.compile('_K[EI][NF]F')
+
+
+def varTypeFactory(key):
+    if key in VAR_NO_CONV:
+        return lambda x: (x, None)
+    for typeFunc in DF_CONV:
+        # no conversion for strings
+        if key in DF_CONV[typeFunc]:
+            return lambda x: (typeFunc(x), None)
+    # Perform array conversion, return expected values and uncertainties
+    if UNIV_K_RE.search(key) is not None:
+        return lambda x: str2vec(x, out=tuple)
+    return lambda x: splitValsUncs(x)
+
 
 __all__ = ['ResultsReader', ]
 
@@ -151,6 +198,7 @@ class ResultsReader(XSReader):
         self._numUniverses = 0
         self._univlist = []
         self.metadata, self.resdata, self.universes = {}, {}, {}
+        self._varTypeLookup = {}
 
     def _read(self):
         """Read through the results file and store requested data."""
@@ -196,18 +244,22 @@ class ResultsReader(XSReader):
     def _storeUnivData(self, varNameSer, varVals):
         """Process universes' data"""
         brState = self._getBUstate()  # obtain the branching tuple
-        values = str2vec(varVals)  # convert the string to float numbers
         if brState not in self.universes:
             self.universes[brState] = \
                 HomogUniv(brState[0], brState[1], brState[2], brState[3])
         if varNameSer == self._keysVersion['univ']:
             return
-        if varNameSer not in self._keysVersion['varsUnc']:
-            vals, uncs = splitValsUncs(values)
-            self.universes[brState].addData(varNameSer, uncs, True)
-            self.universes[brState].addData(varNameSer, vals, False)
-        else:
-            self.universes[brState].addData(varNameSer, array(values), False)
+
+        # Get variable specificy type converter
+        if varNameSer not in self._varTypeLookup:
+            self._varTypeLookup[varNameSer] = varTypeFactory(varNameSer)
+        converter = self._varTypeLookup[varNameSer]
+        values, uncertainties = converter(varVals)
+
+        # Values without uncertainties
+        self.universes[brState].addData(varNameSer, values, False)
+        if uncertainties is not None:
+            self.universes[brState].addData(varNameSer, uncertainties, True)
 
     def _storeResData(self, varNamePy, varVals):
         """Process time-dependent results data"""
@@ -385,6 +437,8 @@ class ResultsReader(XSReader):
     def _postcheck(self):
         self._inspectData()
         self._cleanMetadata()
+        del self._varTypeLookup
+        self._univList = []
 
     def _compare(self, other, lower, upper, sigma):
         similar = self.compareMetadata(other)
@@ -828,14 +882,18 @@ def gatherPairedUnivData(universe, uIndex, bIndex, shapeStart, convFunc,
     """Helper function to update universe dictionary for exporting"""
     for xsKey, xsVal in iteritems(expD):
         outKey = convFunc(xsKey)
-        xsUnc = uncD[xsKey]
         if outKey not in univData:
-            shape = xsVal.shape
-            if not shape:  # single value like infKinf
+            if isinstance(xsVal, Real):
                 shape = 1,
+            else:  # assume array or has shape
+                shape = xsVal.shape
             data = empty((shapeStart + shape + (2, )))
             univData[outKey] = data
         else:
             data = univData[outKey]
         data[bIndex, uIndex, ..., 0] = xsVal
-        data[bIndex, uIndex, ..., 1] = xsUnc
+        xsUnc = uncD.get(xsKey)
+        if xsUnc is None:
+            data[bIndex, uIndex, ..., 1] = 0.0
+        else:
+            data[bIndex, uIndex, ..., 1] = xsUnc
diff --git a/serpentTools/utils/compare.py b/serpentTools/utils/compare.py
index 15dda38..6296423 100644
--- a/serpentTools/utils/compare.py
+++ b/serpentTools/utils/compare.py
@@ -4,7 +4,7 @@ Comparison utilities
 from numpy.core.defchararray import equal as charEqual
 from numpy import (
     fabs, zeros_like, ndarray, array, greater, multiply, subtract,
-    equal,
+    equal, asarray,
 )
 
 from serpentTools._compat import Iterable
@@ -540,6 +540,10 @@ def getLogOverlaps(quantity, arr0, arr1, unc0, unc1, sigma, relative=True):
       :mod:`serpentTools.messages`
     """
 
+    arr0 = asarray(arr0)
+    unc0 = asarray(unc0)
+    arr1 = asarray(arr1)
+    unc1 = asarray(unc1)
     if (arr0 == arr1).all():
         logIdenticalWithUncs(arr0, unc0, unc1, quantity)
         return True


Test patch:
diff --git a/serpentTools/tests/test_ResultsReader.py b/serpentTools/tests/test_ResultsReader.py
index 2a29d02..f4d3c4b 100644
--- a/serpentTools/tests/test_ResultsReader.py
+++ b/serpentTools/tests/test_ResultsReader.py
@@ -1,5 +1,5 @@
 """Test the results reader."""
-
+# pylint: disable=line-too-long
 from os import remove
 from shutil import copy
 from unittest import TestCase
@@ -41,7 +41,12 @@ DF_SURFACE                (idx, [1:  3])  = 'ADF' ;
 DF_SYM                    (idx, 1)        = 1 ;
 DF_N_SURF                 (idx, 1)        = 4 ;
 DF_N_CORN                 (idx, 1)        = 4 ;
-""")
+DF_VOLUME                 (idx, 1)        =  4.62250E+02 ;
+DF_SURF_AREA              (idx, [1:  4])  = [ 2.15000E+01  2.15000E+01  2.15000E+01  2.15000E+01 ];
+DF_MID_AREA               (idx, [1:  4])  = [ 2.15000E+00  2.15000E+00  2.15000E+00  2.15000E+00 ];
+DF_CORN_AREA              (idx, [1:  4])  = [ 2.15000E+00  2.15000E+00  2.15000E+00  2.15000E+00 ];
+DF_SURF_IN_CURR           (idx, [1:  16]) = [  1.19014E+15 0.00046  1.99543E+14 0.00114  1.19014E+15 0.00046  1.99543E+14 0.00114  1.19014E+15 0.00046  1.99543E+14 0.00114  1.19014E+15 0.00046  1.99543E+14 0.00114 ];
+""") # noqa
 
 
 def tearDownModule():
@@ -727,9 +732,25 @@ class ResADFTester(TestCase):
         cls.reader.read()
 
     def test_adf(self):
+        """Verify the storage of ADF data"""
         univ = self.reader.getUniv('0', index=0)
-        expected = array('ADF')
-        assert_array_equal(expected, univ.gc['dfSurface'])
+        self.assertTrue(type(univ.infExp['infKinf']) is float)
+        self.assertEqual(1.15306, univ.infExp['infKinf'])
+        self.assertEqual(0.00127, univ.infUnc['infKinf'])
+        self.assertTrue(type(univ.gc['dfSurface']) is str)
+        self.assertEqual('ADF', univ.gc['dfSurface'])
+        self.assertTrue(type(univ.gc['dfSym']) is int)
+        self.assertEqual(1, univ.gc['dfSym'])
+        self.assertTrue(type(univ.gc['dfVolume']) is float)
+        self.assertEqual(4.62250E+02, univ.gc['dfVolume'])
+        assert_array_equal(
+            array([2.15000E+01, 2.15000E+01, 2.15000E+01, 2.15000E+01]),
+            univ.gc['dfSurfArea'])
+        assert_array_equal(array([
+            1.19014E+15, 1.99543E+14, 1.19014E+15, 1.99543E+14,
+            1.19014E+15, 1.99543E+14, 1.19014E+15, 1.99543E+14]),
+            univ.gc['dfSurfInCurr'])
+        self.assertTrue('dfSurfArea' not in univ.gcUnc)
 
 
 if __name__ == '__main__':
diff --git a/serpentTools/tests/test_container.py b/serpentTools/tests/test_container.py
index 35de971..b607372 100644
--- a/serpentTools/tests/test_container.py
+++ b/serpentTools/tests/test_container.py
@@ -29,12 +29,12 @@ class _HomogUnivTestHelper(TestCase):
         # Data definition
         rawData = {'B1_1': vec, 'B1_AS_LIST': list(vec),
                    'INF_1': vec, 'INF_S0': mat, 'CMM_TRANSP_X': vec,
-                   'INF_KEFF': vec, 'B1_KINF': vec, 'IMP_KEFF': vec,
-                   'INF_KINF': vec}
+                   'INF_KEFF': testK, 'B1_KINF': testK, 'IMP_KEFF': testK,
+                   'INF_KINF': testK}
         attrs = {'MACRO_E': groupStructure}
         # Partial dictionaries
         self.b1Unc = self.b1Exp = {
-            'b11': vec, 'b1AsList': vec, 'b1Kinf': testK,
+            'b11': vec, 'b1AsList': list(vec), 'b1Kinf': testK,
         }
         self.infUnc = self.infExp = {
             'inf1': vec, 'infS0': mat, 'infKeff': testK, 'infKinf': testK,