Repository: CORE-GATECH-GROUP/serpent-tools

Issue:
Days are still returned in time points are given for a vector quantity on materials, e.g. burnup
Fix in #2 did not take in to account quantities like `burnup` and `volume` that do not return arrays for isotope quantities.
If the user specifies one of these quantities from a depleted material, the time points are still returned.

```
if allY.shape[0] == 1 or len(allY.shape) == 1:  # vector
    return xVals, allY[colIndices] if colIndices else allY
```
change to
```
if allY.shape[0] == 1 or len(allY.shape) == 1:  # vector
    yVals = allY[colIndices] if colIndices else allY
else:
    yVals = numpy.empty((len(rowIndices), len(xVals)), dtype=float)
    for isoID, rowId in enumerate(rowIndices):
        yVals[isoID, :] = (allY[rowId][colIndices] if colIndices
                                 else allY[rowId][:])`
```
and fix unit tests

Discussion:

Patch:
diff --git a/serpentTools/__init__.py b/serpentTools/__init__.py
index 8e3f32c..3e8e8de 100644
--- a/serpentTools/__init__.py
+++ b/serpentTools/__init__.py
@@ -1,7 +1,7 @@
 from serpentTools import settings
 from serpentTools import parsers
 
-__version__ = '0.1.3'
+__version__ = '0.1.4'
 
 # List TODOS/feature requests here for now
 # Messages/Errors
diff --git a/serpentTools/objects/__init__.py b/serpentTools/objects/__init__.py
index 8f2e2cd..2ef74d4 100644
--- a/serpentTools/objects/__init__.py
+++ b/serpentTools/objects/__init__.py
@@ -1,8 +1,5 @@
 """Objects used to support the parsing."""
 
-import numpy
-from matplotlib import pyplot
-
 
 class _SupportingObject(object):
     """
@@ -17,14 +14,12 @@ class _SupportingObject(object):
 
     """
 
-    def __init__(self, container, name):
+    def __init__(self, container):
         self._container = container
-        self.name = name
         self._filePath = container.filePath
 
     def __repr__(self):
-        return '<{} {} from {}>'.format(self.whatAmI(),
-                                        self.name, self._filePath)
+        return '<{} from {}>'.format(self.whatAmI(), self._filePath)
 
     def whatAmI(self):
         return type(self).__name__
@@ -47,194 +42,13 @@ class _SupportingObject(object):
                                              for item in lowerSplits[1:]])
 
 
-class DepletedMaterial(_SupportingObject):
-    """Class for storing material data from ``_dep.m`` files.
-
-    Parameters
-    ----------
-    parser: :py:class:`~serpentTools.parsers.depletion.DepletionReader`
-        Parser that found this material.
-        Used to obtain file metadata like isotope names and burnup
-    name: str
-        Name of this material
-
-    Attributes
-    ----------
-    zai: numpy.array
-        Isotope id's
-    names: numpy.array
-        Names of isotopes
-    days: numpy.array
-        Days overwhich the material was depleted
-    adens: numpy.array
-        Atomic density over time for each nuclide
-
-    :note:
-
-        These attributes only exist if the pasers was instructed to
-        read in this data. I.e. if ``readers.depletion.metadataKeys``
-        does not contain ``ZAI``, then this object will not have
-        the ``zai`` data.
-
-    """
-
-    def __init__(self, parser, name):
-        _SupportingObject.__init__(self, parser, name)
-        self._varData = {}
-
-    def __getattr__(self, item):
-        """
-        Allows the user to get items like ``zai`` and ``adens``
-        with ``self.zai`` and ``self.adens``, respectively.
-        """
-        if item in self._varData:
-            return self._varData[item]
-        return _SupportingObject.__getattr__(self, item)
-
-    def __getitem__(self, item):
-        if item not in self._varData:
-            if item not in self._container.metadata:
-                raise KeyError('{} has no item {}'.format(self, item))
-            return self._container.metadata[item]
-        return self._varData[item]
-
-    def addData(self, variable, rawData):
-        """Add data straight from the file onto a variable.
-
-        Parameters
-        ----------
-        variable: str
-            Name of the variable directly from ``SERPENT``
-        rawData: list
-            List of strings corresponding to the raw data from the file
-        """
-        newName = self._convertVariableName(variable)
-        if isinstance(rawData, str):
-            scratch = [float(item) for item in rawData.split()]
-        else:
-            scratch = []
-            for line in rawData:
-                if line:
-                    scratch.append([float(item) for item in line.split()])
-        self._varData[newName] = numpy.array(scratch)
-
-    def getXY(self, xUnits, yUnits, timePoints=None, names=None):
-        """Return x values for given time, and corresponding isotope values.
-
-        Parameters
-        ----------
-        xUnits: str
-            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
-        yUnits: str
-            name of y value to return, e.g. ``'adens'``, ``'burnup'``
-        timePoints: list or None
-            If given, select the time points according to those specified here.
-            Otherwise, select all points
-        names: list or None
-            If given, return y values corresponding to these isotope names.
-            Otherwise, return values for all isotopes.
-
-        Returns
-        -------
-        numpy.array
-            Array of values.
-        numpy.array
-            Vector of time points only if ``timePoints`` is ``None``
-
-        Raises
-        ------
-        AttributeError
-            If the names of the isotopes have not been obtained and specific
-            isotopes have been requested
-        KeyError
-            If at least one of the days requested is not present
-        """
-        if timePoints is not None:
-            returnX = False
-            timeCheck = self._checkTimePoints(xUnits, timePoints)
-            if any(timeCheck):
-                raise KeyError('The following times were not present in file {}'
-                               '\n{}'.format(self._container.filePath,
-                                             ', '.join(timeCheck)))
-        else:
-            returnX = True
-        if names and 'names' not in self._container.metadata:
-            raise AttributeError('Parser {} has not stored the isotope names.'
-                                 .format(self._container))
-        xVals, colIndices = self._getXSlice(xUnits, timePoints)
-        rowIndices = self._getIsoID(names)
-        allY = self[yUnits]
-        if allY.shape[0] == 1 or len(allY.shape) == 1:  # vector
-            return xVals, allY[colIndices] if colIndices else allY
-        yVals = numpy.empty((len(rowIndices), len(xVals)), dtype=float)
-        for isoID, rowId in enumerate(rowIndices):
-            yVals[isoID, :] = (allY[rowId][colIndices] if colIndices
-                               else allY[rowId][:])
-        if returnX:
-            return yVals, xVals
-        return yVals
-
-    def _checkTimePoints(self, xUnits, timePoints):
-        valid = self[xUnits]
-        badPoints = [str(time) for time in timePoints if time not in valid]
-        return badPoints
-
-
-    def _getXSlice(self, xUnits, timePoints):
-        allX = self[xUnits]
-        if timePoints is not None:
-            colIndices = [indx for indx, xx in enumerate(allX)
-                          if xx in timePoints]
-            xVals = allX[colIndices]
-        else:
-            colIndices = None
-            xVals = allX
-        return xVals, colIndices
-
-    def _getIsoID(self, isotopes):
-        """Return the row indices that correspond to specfic isotopes."""
-        # TODO: List comprehension to make rowIDs then return array
-        if not isotopes:
-            return numpy.array(list(range(len(self.names))), dtype=int)
-        isoList = [isotopes] if isinstance(isotopes, (str, int)) else isotopes
-        rowIDs = numpy.empty_like(isoList, dtype=int)
-        for indx, isotope in enumerate(isoList):
-            rowIDs[indx] = self.names.index(isotope)
-        return rowIDs
-
-    def plot(self, xUnits, yUnits, timePoints=None, names=None, ax=None):
-        """Plot some data as a function of time for some or all isotopes.
-
-        Parameters
-        ----------
-        xUnits: str
-            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
-        yUnits: str
-            name of y value to return, e.g. ``'adens'``, ``'burnup'``
-        timePoints: list or None
-            If given, select the time points according to those
-            specified here. Otherwise, select all points
-        names: list or None
-            If given, return y values corresponding to these isotope
-            names. Otherwise, return values for all isotopes.
-        ax: None or ``matplotlib axes``
-            If given, add the data to this plot.
-            Otherwise, create a new plot
-
-        Returns
-        -------
-        ``matplotlib axes``
-            Axes corresponding to the figure that was plotted
+class _NamedObject(_SupportingObject):
+    """Class for named objects like materials and detectors."""
 
-        See Also
-        --------
-        getXY
-
-        """
-        xVals, yVals = self.getXY(xUnits, yUnits, timePoints, names)
-        ax = ax or pyplot.subplots(1, 1)[1]
-        labels = names or [None]
-        for row in range(yVals.shape[0]):
-            ax.plot(xVals, yVals[row], label=labels[row])
+    def __init__(self, container, name):
+        _SupportingObject.__init__(self, container)
+        self.name = name
 
-        return ax
+    def __repr__(self):
+        return '<{} {} from {}>'.format(self.whatAmI(),
+                                        self.name, self._filePath)
\ No newline at end of file
diff --git a/serpentTools/objects/materials.py b/serpentTools/objects/materials.py
new file mode 100644
index 0000000..be2fe02
--- /dev/null
+++ b/serpentTools/objects/materials.py
@@ -0,0 +1,199 @@
+"""Classes for storing material data."""
+
+import numpy
+from matplotlib import pyplot
+
+
+from serpentTools.objects import _NamedObject
+
+
+class DepletedMaterial(_NamedObject):
+    """Class for storing material data from ``_dep.m`` files.
+
+    Parameters
+    ----------
+    parser: :py:class:`~serpentTools.parsers.depletion.DepletionReader`
+        Parser that found this material.
+        Used to obtain file metadata like isotope names and burnup
+    name: str
+        Name of this material
+
+    Attributes
+    ----------
+    zai: numpy.array
+        Isotope id's
+    names: numpy.array
+        Names of isotopes
+    days: numpy.array
+        Days overwhich the material was depleted
+    adens: numpy.array
+        Atomic density over time for each nuclide
+
+    :note:
+
+        These attributes only exist if the pasers was instructed to
+        read in this data. I.e. if ``readers.depletion.metadataKeys``
+        does not contain ``ZAI``, then this object will not have
+        the ``zai`` data.
+
+    """
+
+    def __init__(self, parser, name):
+        _NamedObject.__init__(self, parser, name)
+        self._varData = {}
+
+    def __getattr__(self, item):
+        """
+        Allows the user to get items like ``zai`` and ``adens``
+        with ``self.zai`` and ``self.adens``, respectively.
+        """
+        if item in self._varData:
+            return self._varData[item]
+        return _NamedObject.__getattr__(self, item)
+
+    def __getitem__(self, item):
+        if item not in self._varData:
+            if item not in self._container.metadata:
+                raise KeyError('{} has no item {}'.format(self, item))
+            return self._container.metadata[item]
+        return self._varData[item]
+
+    def addData(self, variable, rawData):
+        """Add data straight from the file onto a variable.
+
+        Parameters
+        ----------
+        variable: str
+            Name of the variable directly from ``SERPENT``
+        rawData: list
+            List of strings corresponding to the raw data from the file
+        """
+        newName = self._convertVariableName(variable)
+        if isinstance(rawData, str):
+            scratch = [float(item) for item in rawData.split()]
+        else:
+            scratch = []
+            for line in rawData:
+                if line:
+                    scratch.append([float(item) for item in line.split()])
+        self._varData[newName] = numpy.array(scratch)
+
+    def getXY(self, xUnits, yUnits, timePoints=None, names=None):
+        """Return x values for given time, and corresponding isotope values.
+
+        Parameters
+        ----------
+        xUnits: str
+            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
+        yUnits: str
+            name of y value to return, e.g. ``'adens'``, ``'burnup'``
+        timePoints: list or None
+            If given, select the time points according to those specified here.
+            Otherwise, select all points
+        names: list or None
+            If given, return y values corresponding to these isotope names.
+            Otherwise, return values for all isotopes.
+
+        Returns
+        -------
+        numpy.array
+            Array of values.
+        numpy.array
+            Vector of time points only if ``timePoints`` is ``None``
+
+        Raises
+        ------
+        AttributeError
+            If the names of the isotopes have not been obtained and specific
+            isotopes have been requested
+        KeyError
+            If at least one of the days requested is not present
+        """
+        if timePoints is not None:
+            returnX = False
+            timeCheck = self._checkTimePoints(xUnits, timePoints)
+            if any(timeCheck):
+                raise KeyError('The following times were not present in file {}'
+                               '\n{}'.format(self._container.filePath,
+                                             ', '.join(timeCheck)))
+        else:
+            returnX = True
+        if names and 'names' not in self._container.metadata:
+            raise AttributeError('Parser {} has not stored the isotope names.'
+                                 .format(self._container))
+        xVals, colIndices = self._getXSlice(xUnits, timePoints)
+        rowIndices = self._getIsoID(names)
+        allY = self[yUnits]
+        if allY.shape[0] == 1 or len(allY.shape) == 1:  # vector
+            yVals = allY[colIndices] if colIndices else allY
+        else:
+            yVals = numpy.empty((len(rowIndices), len(xVals)), dtype=float)
+            for isoID, rowId in enumerate(rowIndices):
+                yVals[isoID, :] = (allY[rowId][colIndices] if colIndices
+                                   else allY[rowId][:])
+        if returnX:
+            return yVals, xVals
+        return yVals
+
+    def _checkTimePoints(self, xUnits, timePoints):
+        valid = self[xUnits]
+        badPoints = [str(time) for time in timePoints if time not in valid]
+        return badPoints
+
+    def _getXSlice(self, xUnits, timePoints):
+        allX = self[xUnits]
+        if timePoints is not None:
+            colIndices = [indx for indx, xx in enumerate(allX)
+                          if xx in timePoints]
+            xVals = allX[colIndices]
+        else:
+            colIndices = None
+            xVals = allX
+        return xVals, colIndices
+
+    def _getIsoID(self, isotopes):
+        """Return the row indices that correspond to specfic isotopes."""
+        # TODO: List comprehension to make rowIDs then return array
+        if not isotopes:
+            return numpy.array(list(range(len(self.names))), dtype=int)
+        isoList = [isotopes] if isinstance(isotopes, (str, int)) else isotopes
+        rowIDs = numpy.empty_like(isoList, dtype=int)
+        for indx, isotope in enumerate(isoList):
+            rowIDs[indx] = self.names.index(isotope)
+        return rowIDs
+
+    def plot(self, xUnits, yUnits, timePoints=None, names=None, ax=None):
+        """Plot some data as a function of time for some or all isotopes.
+
+        Parameters
+        ----------
+        xUnits: str
+            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
+        yUnits: str
+            name of y value to return, e.g. ``'adens'``, ``'burnup'``
+        timePoints: list or None
+            If given, select the time points according to those
+            specified here. Otherwise, select all points
+        names: list or None
+            If given, return y values corresponding to these isotope
+            names. Otherwise, return values for all isotopes.
+        ax: None or ``matplotlib axes``
+            If given, add the data to this plot.
+            Otherwise, create a new plot
+
+        Returns
+        -------
+        ``matplotlib axes``
+            Axes corresponding to the figure that was plotted
+
+        See Also
+        --------
+        getXY
+
+        """
+        xVals, yVals = self.getXY(xUnits, yUnits, timePoints, names)
+        ax = ax or pyplot.subplots(1, 1)[1]
+        labels = names or [None]
+        for row in range(yVals.shape[0]):
+            ax.plot(xVals, yVals[row], label=labels[row])
+        return ax
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 8a3b695..002f516 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -6,7 +6,7 @@ import numpy
 from drewtils.parsers import KeywordParser
 
 from serpentTools.objects.readers import MaterialReader
-from serpentTools.objects import DepletedMaterial
+from serpentTools.objects.materials import DepletedMaterial
 
 
 class DepletionReader(MaterialReader):


Test patch:
diff --git a/serpentTools/tests/test_depletion.py b/serpentTools/tests/test_depletion.py
index d7f6d52..6cb911d 100644
--- a/serpentTools/tests/test_depletion.py
+++ b/serpentTools/tests/test_depletion.py
@@ -124,13 +124,12 @@ class DepletedMaterialTester(_DepletionTestHelper):
         """
         Verify the material can produce the full burnup vector through getXY.
         """
-        _days, actual = self.material.getXY('days', 'burnup', )
+        actual, _days = self.material.getXY('days', 'burnup', )
         numpy.testing.assert_equal(actual, self.fuelBU)
 
     def test_getXY_burnup_slice(self):
         """Verify depletedMaterial getXY correctly slices a vector."""
-        _days, actual = self.material.getXY('days', 'burnup',
-                                            self.requestedDays)
+        actual = self.material.getXY('days', 'burnup', self.requestedDays)
         expected = [0.0E0, 1.90317E-2, 3.60163E-2, 1.74880E-1, 3.45353E-01,
                     8.49693E-01, 1.66071E0]
         numpy.testing.assert_equal(actual, expected)