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

Issue:
Universes in results reader are not zero-indexed
## Summary of issue
The first index for universes is 1, likely taken directly from the result file burnup index. This misconstrues some of the style and pythonicness of the layout, as python arrays start at 0.

## Code for reproducing the issue
Read a result file with burnup and view the `universes` attribute

## Expected outcome
Universe burnup indexes start at 0

## Versions

* Version  from ``serpentTools.__version__`` 0.6.1

* Python version - ``python --version`` N/A

Pull request:
API Zero-indexed universes on results reader
Closes #277 by starting burnup steps at 0 rather than 1. Similarly, for cases where there is no burnup, the values of burnup and days are set as 0, rather than one. 

The former case, zero-indexing, make the getUniv method and the storing of universes more pythonic, as arrays, lists, tuples, etc. start at position 0 in python. This is also consistent with the branching reader
https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/1c730ef70c6f9ce98247e1d8b935d3bb98366e4a/serpentTools/parsers/branching.py#L114

This is a breaking change, as the storage of universes has been changed, and thus 
- this is included in the next major release, 0.7.0
- documentation has been updated to clearly indicate the change, both in the changelog and in the class [docstring](https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/2efb1486116f08ae5cef4471017fb7f0d8fa3ec2/serpentTools/parsers/results.py#L115)

Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index dd57dbc..90d13c3 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -39,6 +39,15 @@ Next
    The API for the |branchCollector| may be subject to change
    through revisions until ``0.7.0``
 
+Incompatible API Changes
+------------------------
+
+* |homogUniv| objects are now stored on |resultReader| with 
+  zero-based indexing for burnup. The previous first value of 
+  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.
+
 Pending Deprecations
 --------------------
 
diff --git a/docs/examples/ResultsReader.rst b/docs/examples/ResultsReader.rst
index 3065fba..733c602 100644
--- a/docs/examples/ResultsReader.rst
+++ b/docs/examples/ResultsReader.rst
@@ -301,27 +301,27 @@ Universe data is stored for each state point, i.e.
 
 .. parsed-literal::
  
-    dict_keys([('3101', 0.0, 1, 0.0), ('3102', 0.0, 1, 0.0), ('0', 0.0, 1, 0.0),
-    ('3101', 0.10000000000000001, 2, 1.20048), ('3102', 0.10000000000000001, 2,
-    1.20048), ('0', 0.10000000000000001, 2, 1.20048), ('3101', 1.0, 3,
-    12.004799999999999), ('3102', 1.0, 3, 12.004799999999999), ('0', 1.0, 3,
-    12.004799999999999), ('3101', 2.0, 4, 24.009599999999999), ('3102', 2.0, 4,
-    24.009599999999999), ('0', 2.0, 4, 24.009599999999999), ('3101', 3.0, 5,
-    36.014400000000002), ('3102', 3.0, 5, 36.014400000000002), ('0', 3.0, 5,
-    36.014400000000002), ('3101', 4.0, 6, 48.019199999999998), ('3102', 4.0, 6,
-    48.019199999999998), ('0', 4.0, 6, 48.019199999999998)])
+    dict_keys([('3101', 0.0, 0,  0.0), ('3102', 0.0, 0,  0.0), ('0', 0.0, 0,  0.0),
+    ('3101', 0.10000000000000001, 1, 1.20048), ('3102', 0.10000000000000001, 1,
+    1.20048), ('0', 0.10000000000000001, 1, 1.20048), ('3101', 1.0, 2,
+    12.004799999999999), ('3102', 1.0, 2, 12.004799999999999), ('0', 1.0, 2,
+    12.004799999999999), ('3101', 2.0, 3, 24.009599999999999), ('3102', 2.0, 3,
+    24.009599999999999), ('0', 2.0, 3, 24.009599999999999), ('3101', 3.0, 4,
+    36.014400000000002), ('3102', 3.0, 4, 36.014400000000002), ('0', 3.0, 4,
+    36.014400000000002), ('3101', 4.0, 5, 48.019199999999998), ('3102', 4.0, 5,
+    48.019199999999998), ('0', 4.0, 5, 48.019199999999998)])
 
 
 
 .. code:: 
 
     >>> # Let's use the following unique state
-    >>> print(res.universes[('3102', 0.0, 1, 0.0)])
+    >>> print(res.universes[('3102', 0.0, 2, 0.0)])
 
 
 .. parsed-literal::
 
-    <HomogUniv 3102: burnup: 0.000 MWd/kgu, step: 1, 0.000 days>
+    <HomogUniv 3102: burnup: 0.000 MWd/kgu, step: 0, 0.000 days>
     
 
 Each state contains the same data fields, which can be obtained by using
@@ -355,7 +355,7 @@ interest. In order to obtain the data, the user needs to pass the
 
 ``univ`` must be a string ``burnup`` is a float or int with the units
 MWd/kgU ``time`` is a float or int with the units Days ``index`` is a
-positive integer (i.e. 1, 2, ...)
+non-negative integer (i.e. 0, 1, 2, ...)
 
 The method requires to insert the universe and burnup or time or index
 (only one of these is actually used to retrieve the data). If more than
@@ -365,7 +365,7 @@ priority), burnup, time (lowest priority)
 .. code:: 
     
     >>> # Examples to use various time entries
-    >>> univ3101 = res.getUniv('3101', index=4) # obtain the results for universe=3101 and index=4 
+    >>> univ3101 = res.getUniv('3101', index=3) # obtain the results for universe=3101 and index=3 
     >>> univ3102 = res.getUniv('3102', burnup=0.1) # obtain the results for universe=3102 and index=0.1 MWd/kgU
     >>> univ0 = res.getUniv('0', timeDays=24.0096) # obtain the results for universe=0 and index=24.0096 days
 
@@ -379,22 +379,22 @@ priority), burnup, time (lowest priority)
 
 .. parsed-literal::
  
-    <HomogUniv 3101: burnup: 2.000 MWd/kgu, step: 4, 24.010 days>
+    <HomogUniv 3101: burnup: 2.000 MWd/kgu, step: 3, 24.010 days>
     <HomogUniv 3102:
-    burnup: 0.100 MWd/kgu, step: 2, 1.200 days>
+    burnup: 0.100 MWd/kgu, step: 1, 1.200 days>
     <HomogUniv 0: burnup: 2.000
-    MWd/kgu, step: 4, 24.010 days>
+    MWd/kgu, step: 3, 24.010 days>
 
 .. code:: 
     
-    >>> # obtain the results for universe=0 and index=1 (burnup and timeDays are inserted but not used)
-    >>> univ0 = res.getUniv('0', burnup=0.0, index=1, timeDays=0.0)  
+    >>> # obtain the results for universe=0 and index=0 (burnup and timeDays are inserted but not used)
+    >>> univ0 = res.getUniv('0', burnup=0.0, index=0, timeDays=0.0)  
     >>> print(univ0)
 
 
 .. parsed-literal::
  
-    <HomogUniv 0: burnup: 0.000 MWd/kgu, step: 1, 0.000 days>
+    <HomogUniv 0: burnup: 0.000 MWd/kgu, step: 0, 0.000 days>
 
 .. code:: 
     
@@ -422,7 +422,7 @@ priority), burnup, time (lowest priority)
 .. code:: 
     
     >>> # The values are all energy dependent 
-    >>> univ0.infExp['infAbs'] # obtain the infinite macroscopic xs for ('0', 0.0, 1, 0.0)
+    >>> univ0.infExp['infAbs'] # obtain the infinite macroscopic xs for ('0', 0.0, 0, 0.0)
 
 
 .. parsed-literal::
@@ -435,7 +435,7 @@ priority), burnup, time (lowest priority)
 
 .. code:: 
     
-    >>> # Obtain the infinite flux for ('0', 0.0, 1, 0.0)
+    >>> # Obtain the infinite flux for ('0', 0.0, 0, 0.0)
     >>> univ0.infExp['infFlx']
 
 .. parsed-literal::
@@ -516,7 +516,7 @@ be zeros.
 
 .. code:: 
     
-    >>> # Obtain the b1 fluxes for ('3101', 0.0, 1, 0.0)
+    >>> # Obtain the b1 fluxes for ('3101', 0.0, 0, 0.0)
     >>> univ3101.b1Exp['b1Flx']
 
 .. parsed-literal::
@@ -532,7 +532,7 @@ be zeros.
 
 .. code:: 
     
-    >>> # Obtain the b1 fluxes for ('3101', 0.0, 1, 0.0)
+    >>> # Obtain the b1 absorption cross section for ('3101', 0.0, 0, 0.0)
     >>> univ3101.b1Exp['b1Abs']
 
 .. parsed-literal::
diff --git a/examples/ResultsReader.ipynb b/examples/ResultsReader.ipynb
index 02dd9fc..10be322 100644
--- a/examples/ResultsReader.ipynb
+++ b/examples/ResultsReader.ipynb
@@ -518,7 +518,7 @@
     {
      "data": {
       "text/plain": [
-       "dict_keys([('3101', 0.0, 1, 0.0), ('3102', 0.0, 1, 0.0), ('0', 0.0, 1, 0.0), ('3101', 0.1, 2, 1.20048), ('3102', 0.1, 2, 1.20048), ('0', 0.1, 2, 1.20048), ('3101', 1.0, 3, 12.0048), ('3102', 1.0, 3, 12.0048), ('0', 1.0, 3, 12.0048), ('3101', 2.0, 4, 24.0096), ('3102', 2.0, 4, 24.0096), ('0', 2.0, 4, 24.0096), ('3101', 3.0, 5, 36.0144), ('3102', 3.0, 5, 36.0144), ('0', 3.0, 5, 36.0144), ('3101', 4.0, 6, 48.0192), ('3102', 4.0, 6, 48.0192), ('0', 4.0, 6, 48.0192)])"
+       "dict_keys([('3101', 0.0, 0, 0.0), ('3102', 0.0, 0, 0.0), ('0', 0.0, 0, 0.0), ('3101', 0.1, 1, 1.20048), ('3102', 0.1, 1, 1.20048), ('0', 0.1, 1, 1.20048), ('3101', 1.0, 2, 12.0048), ('3102', 1.0, 2, 12.0048), ('0', 1.0, 3, 12.0048), ('3101', 2.0, 3, 24.0096), ('3102', 2.0, 3, 24.0096), ('0', 2.0, 3, 24.0096), ('3101', 3.0, 4, 36.0144), ('3102', 3.0, 4, 36.0144), ('0', 3.0, 4, 36.0144), ('3101', 4.0, 5, 48.0192), ('3102', 4.0, 5, 48.0192), ('0', 4.0, 5, 48.0192)])"
       ]
      },
      "execution_count": 18,
@@ -541,13 +541,13 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "<HomogUniv 3102: burnup: 0.000 MWd/kgu, step: 1, 0.000 days>\n"
+      "<HomogUniv 3102: burnup: 0.000 MWd/kgu, step: 0, 0.000 days>\n"
      ]
     }
    ],
    "source": [
     "# Let's use the following unique state\n",
-    "print(res.universes[('3102', 0.0, 1, 0.0)])"
+    "print(res.universes[('3102', 0.0, 0, 0.0)])"
    ]
   },
   {
@@ -592,7 +592,7 @@
     "`univ` must be a string\n",
     "`burnup` is a float or int with the units MWd/kgU\n",
     "`time` is a float or int with the units Days\n",
-    "`index` is a positive integer (i.e. 1, 2, ...)\n",
+    "`index` is a non-negative integer (i.e. 0, 1, 2, ...)\n",
     "\n",
     "The method requires to insert the universe and burnup or time or index (only one of these is actually used to retrieve the data). \n",
     "If more than one time parameter is given, the hierarchy of search is:\n",
@@ -606,7 +606,7 @@
    "outputs": [],
    "source": [
     "# Examples to use various time entries\n",
-    "univ3101 = res.getUniv('3101', index=4) # obtain the results for universe=3101 and index=4 \n",
+    "univ3101 = res.getUniv('3101', index=3) # obtain the results for universe=3101 and index=3 \n",
     "univ3102 = res.getUniv('3102', burnup=0.1) # obtain the results for universe=3102 and index=0.1 MWd/kgU\n",
     "univ0 = res.getUniv('0', timeDays=24.0096) # obtain the results for universe=0 and index=24.0096 days"
    ]
@@ -620,9 +620,9 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "<HomogUniv 3101: burnup: 2.000 MWd/kgu, step: 4, 24.010 days>\n",
-      "<HomogUniv 3102: burnup: 0.100 MWd/kgu, step: 2, 1.200 days>\n",
-      "<HomogUniv 0: burnup: 2.000 MWd/kgu, step: 4, 24.010 days>\n"
+      "<HomogUniv 3101: burnup: 2.000 MWd/kgu, step: 3, 24.010 days>\n",
+      "<HomogUniv 3102: burnup: 0.100 MWd/kgu, step: 1, 1.200 days>\n",
+      "<HomogUniv 0: burnup: 2.000 MWd/kgu, step: 3, 24.010 days>\n"
      ]
     }
    ],
@@ -642,13 +642,13 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "<HomogUniv 0: burnup: 0.000 MWd/kgu, step: 1, 0.000 days>\n"
+      "<HomogUniv 0: burnup: 0.000 MWd/kgu, step: 0, 0.000 days>\n"
      ]
     }
    ],
    "source": [
-    "# obtain the results for universe=0 and index=1 (burnup and timeDays are inserted but not used)\n",
-    "univ0 = res.getUniv('0', burnup=0.0, index=1, timeDays=0.0)  \n",
+    "# obtain the results for universe=0 and index=0 (burnup and timeDays are inserted but not used)\n",
+    "univ0 = res.getUniv('0', burnup=0.0, index=0, timeDays=0.0)  \n",
     "print(univ0)"
    ]
   },
@@ -802,7 +802,7 @@
     }
    ],
    "source": [
-    "# Obtain the b1 fluxes for ('3101', 0.0, 1, 0.0)\n",
+    "# Obtain the b1 fluxes for ('3101', 0.0, 0, 0.0)\n",
     "univ3101.b1Exp['b1Flx']"
    ]
   },
@@ -827,7 +827,7 @@
     }
    ],
    "source": [
-    "# Obtain the b1 fluxes for ('3101', 0.0, 1, 0.0)\n",
+    "# Obtain the b1 absorption cross section for ('3101', 0.0, 0, 0.0)\n",
     "univ3101.b1Exp['b1Abs']"
    ]
   },
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index a1f0aec..366959a 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -31,7 +31,7 @@ from serpentTools.utils import (
     magicPlotDocDecorator,
 )
 from serpentTools.messages import (
-    warning, debug, SerpentToolsException,
+    warning, SerpentToolsException,
     info,
 )
 
@@ -111,7 +111,9 @@ class ResultsReader(XSReader):
         :py:class:`~serpentTools.objects.containers.HomogUniv`
         objects. The keys describe a unique state:
         'universe', burnup (MWd/kg), burnup index, time (days)
-        ('0', 0.0, 1, 0.0)
+        ('0', 0.0, 0, 0.0).
+        Burnup indexes are zero-indexed, meaning the first
+        step is index 0.
 
     Parameters
     ----------
@@ -209,9 +211,10 @@ class ResultsReader(XSReader):
         vals = str2vec(varVals)  # convert the string to float numbers
         if varNamePy in self.resdata.keys():  # extend existing matrix
             currVar = self.resdata[varNamePy]
-            ndim = 1
             if len(currVar.shape) == 2:
                 ndim = currVar.shape[0]
+            else:
+                ndim = 1
             if ndim < self._counter['rslt']:
                 # append this data only once!
                 try:
@@ -234,21 +237,23 @@ class ResultsReader(XSReader):
 
     def _getBUstate(self):
         """Define unique branch state"""
-        days = self._counter['meta']
-        burnup = self._counter['meta']
-        burnIdx = self._counter['rslt']  # assign indices
+        burnIdx = self._counter['rslt'] - 1
         varPyDays = convertVariableName(self._keysVersion['days'])  # Py style
         varPyBU = convertVariableName(self._keysVersion['burn'])
         if varPyDays in self.resdata.keys():
-            if burnIdx > 1:
+            if burnIdx > 0:
                 days = self.resdata[varPyDays][-1, 0]
             else:
                 days = self.resdata[varPyDays][-1]
+        else:
+            days = self._counter['meta'] - 1
         if varPyBU in self.resdata.keys():
-            if burnIdx > 1:
+            if burnIdx > 0:
                 burnup = self.resdata[varPyBU][-1, 0]
             else:
                 burnup = self.resdata[varPyBU][-1]
+        else:
+            burnup = self._counter['meta'] - 1
         return self._univlist[-1], burnup, burnIdx, days
 
     def _getVarName(self, tline):
@@ -312,14 +317,8 @@ class ResultsReader(XSReader):
                        else 3)
         searchValue = (index if index is not None
                        else burnup if burnup is not None else timeDays)
-        if searchIndex == 2 and searchValue < 1:  # index must be non-zero
-            raise KeyError(
-                'Index read is {}, however only integers above zero are '
-                'allowed'.format(searchValue))
         for key, dictUniv in iteritems(self.universes):
             if key[0] == univ and key[searchIndex] == searchValue:
-                debug('Found universe that matches with keys {}'
-                      .format(key))
                 return self.universes[key]
         searchName = ('index ' if index else 'burnup ' if burnup
                       else 'timeDays ')


Test patch:
diff --git a/serpentTools/tests/test_ResultsReader.py b/serpentTools/tests/test_ResultsReader.py
index 1384c4d..50209a5 100644
--- a/serpentTools/tests/test_ResultsReader.py
+++ b/serpentTools/tests/test_ResultsReader.py
@@ -122,11 +122,6 @@ class TestGetUniv(unittest.TestCase):
         with self.assertRaises(SerpentToolsException):
             self.reader.getUniv('0', burnup=None, index=None, timeDays=None)
 
-    def test_nonPostiveIndex(self):
-        """Verify that the reader raises error when the time index is not positive""" # noqa
-        with self.assertRaises(KeyError):
-            self.reader.getUniv('0', burnup=None, index=0, timeDays=None)
-
     def test_noUnivState(self):
         """Verify that the reader raises error when the state tuple does not exist""" # noqa
         with self.assertRaises(KeyError):
@@ -134,7 +129,7 @@ class TestGetUniv(unittest.TestCase):
 
     def test_validUniv(self):
         """Verify that getUniv returns the correct universe"""
-        xsDict = self.reader.getUniv('0', burnup=0.0, index=1, timeDays=0.0)
+        xsDict = self.reader.getUniv('0', burnup=0.0, index=0, timeDays=0.0)
         assert_equal(xsDict.infExp['infAbs'], self.expectedinfValAbs)
 
 
@@ -337,7 +332,7 @@ class TestFilterResults(TesterCommonResultsReader):
     expectedInfVals = array([2.46724E+18, 2.98999E+17])
     expectedInfUnc = array([0.00115, 0.00311])
 
-    expectedStates = (('0', 0.0, 1, 0.0), ('0', 500, 2, 5.0))
+    expectedStates = (('0', 0.0, 0, 0.0), ('0', 500, 1, 5.0))
 
     def setUp(self):
         self.file = getFile('pwr_res.m')
@@ -447,7 +442,7 @@ class TestReadAllResults(TesterCommonResultsReader):
         [1.00000E+37, 6.25000E-07, 0.00000E+00])
     expectedInfVals = array([2.46724E+18, 2.98999E+17])
     expectedInfUnc = array([0.00115, 0.00311])
-    expectedStates = (('0', 0.0, 1, 0.0), ('0', 500, 2, 5.0))
+    expectedStates = (('0', 0.0, 0, 0.0), ('0', 500, 1, 5.0))
 
     def setUp(self):
         self.file = getFile('pwr_filter_res.m')
@@ -564,7 +559,7 @@ class TestFilterResultsNoBurnup(TesterCommonResultsReader):
         [1.00000E+37, 6.25000E-07, 0.00000E+00])
     expectedInfVals = array([8.71807E+14, 4.80974E+13])
     expectedInfUnc = array([0.00097, 0.00121])
-    expectedStates = (('0', 1, 1, 1), ('0', 1, 1, 1))
+    expectedStates = (('0', 0, 0, 0), ('0', 0, 0, 0))
 
     def setUp(self):
         self.file = getFile('pwr_noBU_res.m')
@@ -605,7 +600,7 @@ class RestrictedResultsReader(unittest.TestCase):
     dataFile = "pwr_res.m"
 
     def _testUnivFlux(self, reader):
-        univ = reader.getUniv('0', index=1)
+        univ = reader.getUniv('0', index=0)
         assert_equal(self.expectedInfFlux_bu0, univ.get("infFlx"))
 
     def test_justFlux(self):