Repository: CORE-GATECH-GROUP/serpent-tools (python)

Issue:
[API] Detector tallies of a single bin are difficult to work with
## Summary of issue
When using a detector w/ a single bin, e.g. one group flux, the bin data in the output file is a single bin. Rather than a proper array, these are floats cast into array form. Indexing doesn't work, but you can get the "values" by converting the arrays to `float`.

## Code for reproducing the issue

[tally_det0.txt](https://github.com/CORE-GATECH-GROUP/serpent-tools/files/2340714/tally_det0.txt)
```
import serpentTools
r = serpentTools.read('tally_det0.txt', 'det')
one = r['one']
one.tallies
# array(8.19E17)
r['two'].tallies
# array([8.19E17, 7.18E17])
r['two'].tallies[0]
# 8.19E17
one.tallies[0]
```
## Actual outcome including console output and error traceback if applicable
This raises an `IndexError`

## Expected outcome
Better way to index tally data of a single point.  Plotting also doesn't work for this as well, as the tally data has no shape, but is of size 1.

Converting these to floats, or an array of a single float, e.g. array([tally]), would help with plotting and slicing down the road.

Labeling as `API-Incompatible` because, depending on the implementation, this could require changes from the user. If this is the case, then this should be included with a release, like 0.6.0

## Versions

* Version  from ``serpentTools.__version__`` `0.5.3`

Pull request description:
API INCOMP Store single tally detectors with floats
Closes #235 

When reading detectors with a single tally, the value of ``tallies``, ``errors``, and ``scores`` are stored as floats, rather than numpy arrays.

The numpy arrays were previously of a single value, often leading to mistaken accessing methods. Slicing doesn't work as one would first expect. Now, these are floats and can be used with, *hopefully*, less issues

Documentation has been updated in the changelog and the detector objects.

Labeled as incompatible because this will change how detectors with a single value are used, but not multidimensional detectors

Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 90d13c3..f7dec84 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -47,6 +47,9 @@ Incompatible API Changes
   burnup step was one. All burnup indices are now decreased by
   one. Similarly, if no burnup was present in the file, the
   values of burnup and days for all universes is zero.
+* When reading detectors with a single tally, the value of ``tallies``,
+  ``errors``, and ``scores`` are stored as floats, rather than 
+  :term:`numpy` arrays.
 
 Pending Deprecations
 --------------------
diff --git a/serpentTools/objects/base.py b/serpentTools/objects/base.py
index ac48432..a478d5e 100644
--- a/serpentTools/objects/base.py
+++ b/serpentTools/objects/base.py
@@ -173,11 +173,11 @@ class DetectorBase(NamedObject):
         Name of this detector"""
     baseAttrs = """grids: dict
         Dictionary with additional data describing energy grids or mesh points
-    tallies: None or {np}
+    tallies: None, float, or {np}
         Reshaped tally data to correspond to the bins used
-    errors: None or {np}
+    errors: None, float, or {np}
         Reshaped relative error data corresponding to bins used
-    scores: None or {np}
+    scores: None, float, or {np}
         Reshaped array of tally scores. SERPENT 1 only
     indexes: None or :class:`collections.OrderedDict`
         Collection of unique indexes for each requested bin
diff --git a/serpentTools/objects/detectors.py b/serpentTools/objects/detectors.py
index acb07c0..378c117 100644
--- a/serpentTools/objects/detectors.py
+++ b/serpentTools/objects/detectors.py
@@ -31,7 +31,7 @@ from matplotlib.patches import RegularPolygon
 from matplotlib.collections import PatchCollection
 from matplotlib.pyplot import gca
 
-from serpentTools.messages import warning, debug, SerpentToolsException
+from serpentTools.messages import warning, SerpentToolsException
 from serpentTools.objects.base import DetectorBase
 from serpentTools.utils import (
     magicPlotDocDecorator, formatPlot, setAx_xlims, setAx_ylims,
@@ -108,22 +108,27 @@ class Detector(DetectorBase):
         if self.__reshaped:
             warning('Data has already been reshaped')
             return
-        debug('Starting to sort tally data...')
         shape = []
         self.indexes = OrderedDict()
-        for index in range(1, 10):
-            uniqueVals = unique(self.bins[:, index])
-            if len(uniqueVals) > 1:
-                indexName = self._indexName(index)
-                self.indexes[indexName] = array(uniqueVals, dtype=int) - 1
-                shape.append(len(uniqueVals))
-        self.tallies = self.bins[:, 10].reshape(shape)
-        self.errors = self.bins[:, 11].reshape(shape)
-        if self.bins.shape[1] == 13:
-            self.scores = self.bins[:, 12].reshape(shape)
+        hasScores = self.bins.shape[1] == 13
+        if self.bins.shape[0] == 1:
+            self.tallies = self.bins[0, 10]
+            self.errors = self.bins[0, 11]
+            if hasScores:
+                self.scores = self.bins[0, 12]
+        else:
+            for index in range(1, 10):
+                uniqueVals = unique(self.bins[:, index])
+                if len(uniqueVals) > 1:
+                    indexName = self._indexName(index)
+                    self.indexes[indexName] = array(uniqueVals, dtype=int) - 1
+                    shape.append(len(uniqueVals))
+            self.tallies = self.bins[:, 10].reshape(shape)
+            self.errors = self.bins[:, 11].reshape(shape)
+            if hasScores:
+                self.scores = self.bins[:, 12].reshape(shape)
         self._map = {'tallies': self.tallies, 'errors': self.errors,
                      'scores': self.scores}
-        debug('Done')
         self.__reshaped = True
         return shape
 


Test patch:
diff --git a/serpentTools/tests/test_detector.py b/serpentTools/tests/test_detector.py
index 4b34ad3..6a79aee 100644
--- a/serpentTools/tests/test_detector.py
+++ b/serpentTools/tests/test_detector.py
@@ -9,25 +9,19 @@ from six import iteritems
 from numpy import arange, array
 from numpy.testing import assert_equal
 
-from serpentTools.parsers import DetectorReader
+from serpentTools.parsers import read
 from serpentTools.data import getFile
 from serpentTools.objects.detectors import (
     CartesianDetector, HexagonalDetector, CylindricalDetector)
 from serpentTools.tests import compareDictOfArrays
 
 
-def read(fileP):
-    reader = DetectorReader(fileP)
-    reader.read()
-    return reader
-
-
 class DetectorHelper(TestCase):
     """ Class that assists setting up and testing readers"""
 
     @classmethod
     def setUpClass(cls):
-        cls.reader = read(cls.FILE_PATH)
+        cls.reader = read(cls.FILE_PATH, 'det')
         cls.detectors = cls.reader.detectors
 
     def test_loadedDetectors(self):
@@ -278,6 +272,7 @@ TEST_SUB_CLASSES = {CartesianDetectorTester, HexagonalDetectorTester,
 
 
 COMBINED_OUTPUT_FILE = 'combinedDets_det0.m'
+SINGLE_TALLY_FILE = "single_det0.m"
 
 
 def setUpModule():
@@ -292,11 +287,17 @@ def setUpModule():
         for subCls in TEST_SUB_CLASSES:
             with open(subCls.FILE_PATH) as subFile:
                 out.write(subFile.read())
+    with open(SINGLE_TALLY_FILE, 'w') as out:
+        out.write("""
+DETone = [
+    1    1    1    1    1    1    1    1    1    1  8.19312E+17 0.05187
+];""")
 
 
 def tearDownModule():
-    """Remove any test fixtures created for this module."""
+    """Remove any test files created for this module."""
     remove(COMBINED_OUTPUT_FILE)
+    remove(SINGLE_TALLY_FILE)
 
 
 class CombinedDetTester(DetectorHelper):
@@ -318,6 +319,21 @@ class CombinedDetTester(DetectorHelper):
 
 del DetectorHelper
 
+
+class SingleTallyTester(TestCase):
+    """Test storing detector data with a single tally"""
+
+    def setUp(self):
+        self.detector = read(SINGLE_TALLY_FILE, 'det').detectors['one']
+
+    def test_singleTally(self):
+        """Test the conversion of a single tally to floats, not arrays"""
+        self.assertTrue(isinstance(self.detector.tallies, float))
+        self.assertTrue(isinstance(self.detector.errors, float))
+        self.assertEqual(self.detector.tallies, 8.19312E17)
+        self.assertEqual(self.detector.errors, 0.05187)
+
+
 if __name__ == '__main__':
     from unittest import main
     main()