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

Issue:
Detectors with time binning 
## Summary of issue

Detectors with [time binning](http://serpent.vtt.fi/mediawiki/index.php/Input_syntax_manual#det_di) have 13 columns in the detector output file (an additional column indicating the time bin between the normal 1st and 2nd column) and thus get mixed up with the Serpent 1 style detector output with the number of scores as the 13th column.

The Detector Reader reads the tally values from column 11 and error values from column 12 as usual. The problem is that in detectors with time binning, the tally values are actually at column 12 and error values at column 13.  

I'm not sure what's the best approach for this at serpentTools side, but the best way to figure out whether the detector that has 13 columns has time binning is to check for the

`DET<name>T`

array that is printed in the detector file and gives the time binning associated with the detector.

## Code for reproducing the issue

Serpent input:

    set title "Homogeneous case"

    % --- Disable group constant calculation

    set gcu -1

    % --- Geometry is just a cube

    surf 2 cuboid -100.0 100.0 -100.0 100.0 -100.0 100.0

    % --- Cell definitions:

    cell  3  0  fuel     -2
    cell 99  0  outside   2     % Outside world

    % --- Fuel material:

    mat fuel   sum vol 8.0000E+06
    8016.03c  0.316667
    1001.03c  0.633333
    92235.03c 0.0015
    92238.03c 0.0485
    5010.03c  0.000115797

    % --- Reflective boundary conditions:

    set bc 2 2 2

    % --- Neutron population:

    set pop 1000 10 10

    % --- Normalization

    set power 1.0

    % --- Specify a time binnig (10 bins between 0 and 50 microseconds)

    tme dettime 2 10 0 50e-6

    % --- Detector with time binning

    det FP dr -8 void di dettime

Produced input_det0.m:


    DETFP = [
        1    1    1    1    1    1    1    1    1    1    1  8.68766E-01 0.00612 
        2    2    1    1    1    1    1    1    1    1    1  1.14554E-01 0.03156 
        3    3    1    1    1    1    1    1    1    1    1  1.49875E-02 0.10890 
        4    4    1    1    1    1    1    1    1    1    1  1.51392E-03 0.28029 
        5    5    1    1    1    1    1    1    1    1    1  1.60274E-04 1.00000 
        6    6    1    1    1    1    1    1    1    1    1  1.89739E-05 1.00000 
        7    7    1    1    1    1    1    1    1    1    1  0.00000E+00 0.00000 
        8    8    1    1    1    1    1    1    1    1    1  0.00000E+00 0.00000 
        9    9    1    1    1    1    1    1    1    1    1  0.00000E+00 0.00000 
       10   10    1    1    1    1    1    1    1    1    1  0.00000E+00 0.00000 
    ];


    DETFPT = [
     0.00000E+00  5.00000E-06  2.50000E-06
     5.00000E-06  1.00000E-05  7.50000E-06
     1.00000E-05  1.50000E-05  1.25000E-05
     1.50000E-05  2.00000E-05  1.75000E-05
     2.00000E-05  2.50000E-05  2.25000E-05
     2.50000E-05  3.00000E-05  2.75000E-05
     3.00000E-05  3.50000E-05  3.25000E-05
     3.50000E-05  4.00000E-05  3.75000E-05
     4.00000E-05  4.50000E-05  4.25000E-05
     4.50000E-05  5.00000E-05  4.75000E-05
    ];

Postprocessing with serpentTools:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    import serpentTools as sT

    dr = sT.DetectorReader("./input_det0.m")
    dr.read()

    det = dr.detectors["FP"]

    print(det.tallies)
    print(det.errors)


## Actual outcome including console output and error traceback if applicable

    [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
    [8.68766e-01 1.14554e-01 1.49875e-02 1.51392e-03 1.60274e-04 1.89739e-05
     0.00000e+00 0.00000e+00 0.00000e+00 0.00000e+00]

## Expected outcome
    [8.68766e-01 1.14554e-01 1.49875e-02 1.51392e-03 1.60274e-04 1.89739e-05
     0.00000e+00 0.00000e+00 0.00000e+00 0.00000e+00]
    [0.00612 0.03156 0.1089  0.28029 1.      1.      0.      0.      0.
     0.     ]

## Versions

- Serpent 2.1.31
- serpentTools 0.7.0+17.g7ae29a3.dirty
- Python 3.6.7


Pull request:
API/BUG Support for detectors with time bins
Closes #312 by properly handing detectors with the additional time-column. There is some additional logic that had to be incorporated to handle possible scores columns and the presence of the time column. This is partially why I put forward #314, the removal of our support of SERPENT 1 detectors. This would clean up the selection of the tallies and errors column with the assumption that tallies are *always* in the second to last column and errors in the last column.

Include a new data file based of that in the issue, but a little more minimal. A simple test is included to verify that the detector is properly handled.

Lastly, a simple inclusion in the detector plot labels allows the labeling of detector data plotted against time. Using the output file in the linked issue,
```
>>> t = serpentTools.read('time_det0.m')['FP']
>>> t.plot('time')
```
![Plot of detector data over time](https://user-images.githubusercontent.com/19477741/58578427-c1926900-8216-11e9-809c-8c88e9df07ad.png)


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index baa3628..2d7fdb2 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -16,6 +16,11 @@ Next
   :meth:`~serpentTools.objects.CartesianDetector.meshPlot`, where
   only data greater than ``thresh`` is plotted.
 
+Bug fixes
+---------
+
+* Tally data for detectors with time-bins are properly handled - :issue:`312`
+
 .. _v0.7.0:
 
 :release-tag:`0.7.0`
diff --git a/serpentTools/data/time_det0.m b/serpentTools/data/time_det0.m
new file mode 100644
index 0000000..7692115
--- /dev/null
+++ b/serpentTools/data/time_det0.m
@@ -0,0 +1,11 @@
+
+DETFP = [
+    1    1    1    1    1    1    1    1    1    1    1  9.99978E-01 0.00002 
+    2    2    1    1    1    1    1    1    1    1    1  2.24379E-05 1.00000 
+];
+
+
+DETFPT = [
+ 0.00000E+00  2.50000E-05  1.25000E-05
+ 2.50000E-05  5.00000E-05  3.75000E-05
+];
diff --git a/serpentTools/objects/detectors.py b/serpentTools/objects/detectors.py
index 98b8fc9..3f3eccc 100644
--- a/serpentTools/objects/detectors.py
+++ b/serpentTools/objects/detectors.py
@@ -43,8 +43,12 @@ __all__ = ['Detector', 'CartesianDetector', 'HexagonalDetector',
            'CylindricalDetector', 'SphericalDetector']
 
 
-DET_COLS = ('value', 'energy', 'universe', 'cell', 'material', 'lattice',
-            'reaction', 'zmesh', 'ymesh', 'xmesh', 'tally', 'error', 'scores')
+DET_COLS = (
+    'value', 'time', 'energy', 'universe',
+    'cell', 'material', 'lattice',
+    'reaction', 'zmesh', 'ymesh', 'xmesh',
+    'tally', 'error', 'scores',
+)
 """Name of the columns of the data"""
 
 
@@ -74,13 +78,13 @@ class Detector(DetectorBase):
     def _isReshaped(self):
         return self.__reshaped
 
-    def addTallyData(self, bins):
+    def addTallyData(self, bins, hasTime=False):
         """Add tally data to this detector"""
         self.__reshaped = False
         self.bins = bins
-        self.reshape()
+        self.reshape(hasTime)
 
-    def reshape(self):
+    def reshape(self, hasTime=False):
         """
         Reshape the tally data into a multidimensional array
 
@@ -110,24 +114,35 @@ class Detector(DetectorBase):
             warning('Data has already been reshaped')
             return
         shape = []
+        tallyCol = 10
+        errorCol = 11
+        scoreCol = 12
+        if hasTime:
+            tallyCol += 1
+            errorCol += 1
+            scoreCol += 1
         self.indexes = OrderedDict()
-        hasScores = self.bins.shape[1] == 13
+        hasScores = self.bins.shape[1] == (scoreCol + 1)
+
         if self.bins.shape[0] == 1:
-            self.tallies = self.bins[0, 10]
-            self.errors = self.bins[0, 11]
+            # single tally value
+            self.tallies = self.bins[0, tallyCol]
+            self.errors = self.bins[0, errorCol]
             if hasScores:
-                self.scores = self.bins[0, 12]
+                self.scores = self.bins[0, scoreCol]
         else:
-            for index in range(1, 10):
-                uniqueVals = unique(self.bins[:, index])
+            for tallyIx, colIx in enumerate(
+                    range(1, tallyCol),
+                    start=1 if hasTime else 2):
+                uniqueVals = unique(self.bins[:, colIx])
                 if len(uniqueVals) > 1:
-                    indexName = self._indexName(index)
+                    indexName = self._indexName(tallyIx)
                     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)
+            self.tallies = self.bins[:, tallyCol].reshape(shape)
+            self.errors = self.bins[:, errorCol].reshape(shape)
             if hasScores:
-                self.scores = self.bins[:, 12].reshape(shape)
+                self.scores = self.bins[:, scoreCol].reshape(shape)
         self._map = {'tallies': self.tallies, 'errors': self.errors,
                      'scores': self.scores}
         self.__reshaped = True
@@ -214,9 +229,10 @@ class HexagonalDetector(Detector):
                           detAttrs=Detector.docAttrs,
                           baseAttrs=DetectorBase.baseAttrs)
 
+    # column indicies in full (time-bin included) bins matrix
     _INDEX_MAP = {
-        8: 'ycoord',
-        9: 'xcoord',
+        9: 'ycoord',
+        10: 'xcoord',
     }
 
     _NON_CART = _INDEX_MAP.values()
@@ -405,10 +421,10 @@ class CylindricalDetector(Detector):
     {seeAlso:s}""".format(
         params=DetectorBase.baseParams, detAttrs=Detector.docAttrs,
         baseAttrs=DetectorBase.baseAttrs, seeAlso=docSeeAlso)
-
+    # column indices in full (time-bin included) bins matrix
     _INDEX_MAP = {
-        8: 'phi',
-        9: 'rmesh'
+        9: 'phi',
+        10: 'rmesh'
     }
 
     _NON_CART = _INDEX_MAP.values()
@@ -456,10 +472,12 @@ class SphericalDetector(Detector):
         params=DetectorBase.baseParams, detAttrs=Detector.docAttrs,
         baseAttrs=DetectorBase.baseAttrs,
         seeAlso=CylindricalDetector.docSeeAlso)
+
+    # column indices in full (time-bin included) bins matrix
     _INDEX_MAP = {
-        7: 'theta',
-        8: 'phi',
-        9: 'rmesh',
+        8: 'theta',
+        9: 'phi',
+        10: 'rmesh',
     }
 
     _NON_CART = _INDEX_MAP.values()
@@ -528,7 +546,7 @@ def detectorFactory(name, dataDict):
     tallyD = dataDict.pop('tally')
     detCls = _getDetectorType(dataDict)
     det = detCls(name)
-    det.addTallyData(tallyD)
+    det.addTallyData(tallyD, 'T' in dataDict)
     for gridK, value in iteritems(dataDict):
         det.grids[gridK] = value
     return det
diff --git a/serpentTools/utils/plot.py b/serpentTools/utils/plot.py
index e4cfb54..dc574e9 100644
--- a/serpentTools/utils/plot.py
+++ b/serpentTools/utils/plot.py
@@ -48,6 +48,7 @@ DEPLETION_PLOT_LABELS = {
 
 DETECTOR_PLOT_LABELS = {
     'energy': 'Energy [MeV]',
+    'time': 'Time [s]',
 }
 for dim in ['x', 'y', 'z']:
     DETECTOR_PLOT_LABELS[dim] = "{} Position [cm]".format(dim.capitalize())


Test patch:
diff --git a/serpentTools/tests/test_detector.py b/serpentTools/tests/test_detector.py
index 6a79aee..a369455 100644
--- a/serpentTools/tests/test_detector.py
+++ b/serpentTools/tests/test_detector.py
@@ -334,6 +334,29 @@ class SingleTallyTester(TestCase):
         self.assertEqual(self.detector.errors, 0.05187)
 
 
+class TimeBinnedDetectorTester(TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.reader = read(getFile('time_det0.m'), 'det')
+        cls.timeDet = cls.reader['FP']
+
+    def test_timeDetector(self):
+        """Verify a simple time-binned detector is processed properly"""
+        self.assertEqual(1, len(self.timeDet.grids),
+                         msg=', '.join(self.timeDet.grids.keys()))
+        self.assertTrue("T" in self.timeDet.grids)
+        expTallies = array([9.99978E-01, 2.24379E-05])
+        expErrors = array([0.00002, 1.0])
+        expTimeGrid = array([
+            [0.00000E+00, 2.50000E-05, 1.25000E-05],
+            [2.50000E-05, 5.00000E-05, 3.75000E-05],
+        ])
+        assert_equal(self.timeDet.tallies, expTallies)
+        assert_equal(self.timeDet.errors, expErrors)
+        assert_equal(self.timeDet.grids['T'], expTimeGrid)
+
+
 if __name__ == '__main__':
     from unittest import main
     main()