Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #259 (2018-12-21 18:46:36)

ENH Add more clarity into supported readers
If you attempt to pass a reader string to read, e.g.
```
serpentTools.read('myfilePath', 'results')
```
the function picks the reader class based upon the second argument. This is a good way to enforce a reader type, regardless of file extension. Now, the supported flags and their resulting readers are printed in the error message.


Patch:
diff --git a/docs/api/depletion.rst b/docs/api/depletion.rst
index 83b09f5..b2d35da 100644
--- a/docs/api/depletion.rst
+++ b/docs/api/depletion.rst
@@ -4,12 +4,6 @@
 Depletion Reader
 ================
 
-.. warning::
-
-    Does not support depleted materials with underscores,
-    i.e. ``fuel_1`` will not be matched with the current methods.
-    Follow up on, or claim, the issue on GitHub: :issue:`58`
-
 .. autoclass:: serpentTools.parsers.depletion.DepletionReader
     :special-members: __getitem__
 
diff --git a/docs/changelog.rst b/docs/changelog.rst
index e62847f..2d2a76a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -13,7 +13,11 @@ Changelog
 
 Next
 ====
-* :meth:`serpentTools.settings.rc.loadYaml` uses ``safe_load``
+* :pull:`256` :meth:`serpentTools.settings.rc.loadYaml` uses ``safe_load``
+* :pull:`257` |depletionReader| now can utilize 
+  :meth:`~serpentTools.parsers.depletion.DepletionReader.saveAsMatlab` for
+  exporting data to a binary ``.mat`` file
+
 .. _v0.6.0:
 
 0.6.0
diff --git a/docs/develop/checklist.rst b/docs/develop/checklist.rst
index cc83651..1ba776f 100644
--- a/docs/develop/checklist.rst
+++ b/docs/develop/checklist.rst
@@ -36,7 +36,7 @@ project with
 
 Individual test files can also be run with
 
-..code::
+.. code::
 
     $ python my_test_scipt.py
 
diff --git a/docs/glossary.rst b/docs/glossary.rst
index b1d14c9..cb97808 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -12,24 +12,28 @@ Glossary
         packages. For a smaller, more minimal approach, see
         :term:`Miniconda`. For more information, see
         `<https://www.anaconda.com/>`_.
+
     Anaconda prompt
         Modified Windows prompt to be used with the :term:`Anaconda` python
         distrubution. Reconfigures python paths to point to packages
         installed via :term:`conda`
-    Conda
-        From `<https://conda.io>`_:
 
-            The package and environment manager bundled with Anaconda
-            that installs and updates conda packages and their
-            dependencies.
+    conda
+        From `<https://www.conda.io/docs/>`_: 
+
+          Conda is an open source package management system and environment
+          management system that runs on Windows, macOS, and Lunix. Conda
+          quickly installs, runs, and updates packages and their dependncies
 
     CI
         Continuous integration. Software we utilize to check that pull
         requests don't break the code.
+
     git
         Distributed version control system. Allows us to test and implement
         new features with ease. For more information, see
         `<https://git-scm.com>`_.
+
     Jupyter notebook
         Powerful web application used in this project, for examples and
         tutorials containing real python code. From `<https://jupyter.org>`_:
@@ -37,37 +41,40 @@ Glossary
             The Jupyter Notebook is an open-source web application that allows
             you to create and share documents that contain live code, equations,
             visualizations and narrative text.
-    
+
     lint
         Bits of potentially erroneous code. Can be identified by a :term:`linter`
+
     linter
         Program that analyzes source code to check for errors, bugs,
         stylistic issues, and other potential hangups.
+
     matplotlib
         Primary python package for plotting data. Highly customizable
         and extensible. More information at `<https://matplotlib.org>`_
+
     Miniconda
-        Mininal installer for :term:`conda`. From `<https://conda.io>`_:
+        Minimal installer for :term:`conda`. From `<https://conda.io>`_:
 
             Miniconda is a small, bootstrap version of Anaconda that only
             includes conda, Python, the packages they depend on, and a
             small number of useful packages
 
-        For more information, see `<https://conda.io/miniconda.html>`_
-
     numpy
         Widely-used python package that allows multidimensional arrays
         and linear algebra routines. More information at
-        `<www.numpy.org>`_
-   scipy
+        `<https://www.numpy.org>`_
+
+    scipy
          Widely-used python package that contains more mathematical support
-         and data structures, such as sparse matrices. Builds off of
-         :term:`numpy`. Not required for this package, but allows the
+         and data structures, such as sparse matrices.
+         Not required for this package, but allows the
          sparsity of some matrices to be exploited. More information at
          `<https://docs.scipy.org/doc/scipy/reference/>`_
+
     SERPENT
         Monte Carlo particle transport code developed at VTT Technical
         Research Centre of Finland, Ltd. Produces output files
         that can be parsed by this project. More information, including
-        distributoin and licensing of ``SERPENT`` can be found at
+        distribution and licensing of ``SERPENT`` can be found at
         `<http://montecarlo.vtt.fi>`_. 
diff --git a/docs/index.rst b/docs/index.rst
index 23daa78..d7b348e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -54,6 +54,7 @@ using :term:`SERPENT`.
    develop/index
    license
    contributors
+   glossary
 
 .. [1] Leppanen, J. et al. (2015) "The Serpent Monte Carlo code: Status,
     development and applications in 2013." Ann. Nucl. Energy, `82 (2015) 142-150
diff --git a/serpentTools/parsers/__init__.py b/serpentTools/parsers/__init__.py
index 3c52319..0021a5e 100644
--- a/serpentTools/parsers/__init__.py
+++ b/serpentTools/parsers/__init__.py
@@ -32,6 +32,11 @@ READERS = {
     'depmtx': DepmtxReader,
 }
 
+SUPPORTED_READER_MSG = "\n".join(
+    ["{}: {}".format(key, READERS[key].__name__)
+     for key in sorted(READERS.keys())]
+)
+
 REGEXES = {
     r'(.*_dep\.m)': DepletionReader,
     r'(.*\.coe)': BranchingReader,
@@ -78,7 +83,8 @@ def inferReader(filePath):
             return reader
     raise SerpentToolsException(
         'Failed to infer filetype and thus accurate reader from'
-        'file path {}'.format(filePath)
+        'file path {}. Pass one of the below options to ensure '
+        'a specific reader:\n{}'.format(filePath, SUPPORTED_READER_MSG)
     )
 
 
@@ -100,13 +106,16 @@ def read(filePath, reader='infer'):
         String argument Action
         =============== ==========================================
         infer           Infer the correct reader based on the file
-        dep             ``DepletionReader``
         branch          ``BranchingReader``
-        det             ``DetectorReader``
-        results         ``ResultsReader``
         bumat           ``BumatReader``
+        dep             ``DepletionReader``
+        det             ``DetectorReader``
         fission         ``FissionMatrixReader``
-        mdx             ``MdxReader``
+        history         ``HistoryReader``
+        microxs         ``MdxReader``
+        results         ``ResultsReader``
+        sensitivity     ``SensitivityReader``
+        xsplot          ``XSPlotReader``
         =============== ==========================================
 
     Returns
@@ -135,7 +144,8 @@ def read(filePath, reader='infer'):
                 loader = READERS[reader]
             else:
                 raise SerpentToolsException(
-                    'Reader type {} not supported'.format(reader)
+                    'Reader type {} not supported. Try one of the below:\n{}'
+                    .format(reader, SUPPORTED_READER_MSG)
                 )
     else:
         assert callable(reader), (
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 5614d93..269f229 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -12,7 +12,7 @@ from ._collections import DEPLETION_VARIABLES
 from serpentTools.engines import KeywordParser
 from serpentTools.parsers.base import MaterialReader
 from serpentTools.objects.materials import DepletedMaterial
-
+from serpentTools.utils.core import deconvertVariableName
 from serpentTools.messages import (
     warning, debug, error, SerpentToolsException,
 )
@@ -354,6 +354,65 @@ class DepletionReader(DepPlotMixin, MaterialReader):
             lower, upper, 'burnup')
         return similar
 
+    def saveAsMatlab(self, fileP, reconvert=True, metadata=True,
+                     **savematKwargs):
+        """
+        Write a binary MATLAB file from the contents of this reader
+
+
+        .. note::
+
+            Vectors will be stored as either column or row
+            matrices, e.g. ``1xN`` or ``Nx1``. This can be
+            controlled by passing ``oned_as`` to be either
+            ``'row'`` or ``'column'``
+
+        Parameters
+        ----------
+        fileP: str or file-like object
+            Name of the file to write
+        reconvert: bool
+            If this evaluates to true, convert values back into their
+            original form as they appear in the output file, e.g.
+            ``MAT_TOTAL_ING_TOX``. Otherwise, maintain the ``mixedCase``
+            style, ``total_ingTox``.
+        metadata: bool or str or list of strings
+            If this evaluates to true, then write all metadata to the
+            file as well.
+        savematKwargs:
+            Additional keyword arguments to pass to
+            :func:`scipy.io.savemat`
+
+        Raises
+        ------
+        ImportError:
+            If :term:`scipy` is not installed
+
+        See Also
+        --------
+        :func:`scipy.io.savemat`
+        """
+        from scipy.io import savemat
+
+        # set these here to reduce number of if/elses
+
+        if reconvert:
+            converter = deconvert
+        else:
+            converter = prepToMatlab
+
+        if metadata:
+            data = {k.upper() if reconvert else k: v
+                    for k, v in iteritems(self.metadata)}
+        else:
+            data = {}
+
+        for matName, material in iteritems(self.materials):
+            for varName, varData in iteritems(material.data):
+                data[converter(matName, varName)] = varData
+
+        savemat(fileP, data, **savematKwargs)
+
 
 def getMaterialNameAndVariable(mlabName):
     for serpVar in DEPLETION_VARIABLES:
@@ -374,3 +433,17 @@ def getMatlabVarName(chunk):
     if isinstance(chunk, list):
         return getMatlabVarName(chunk[0])
     return chunk[:chunk.index(' ')]
+
+
+_MAT_FMT_ORIG = "MAT_{}_{}"
+_MAT_FMT_CC = "{}_{}"
+
+
+def deconvert(material, variable):
+    """Restore the original name as present in the depletion file"""
+    return _MAT_FMT_ORIG.format(material, deconvertVariableName(variable))
+
+
+def prepToMatlab(material, variable):
+    """Create the name of a single variable for MATLAB"""
+    return _MAT_FMT_CC.format(material, variable)
diff --git a/serpentTools/utils/core.py b/serpentTools/utils/core.py
index 3a19ea7..e56468e 100644
--- a/serpentTools/utils/core.py
+++ b/serpentTools/utils/core.py
@@ -153,6 +153,17 @@ def convertVariableName(variable):
                                      for item in lowerSplits[1:]])
 
 
+def deconvertVariableName(variable):
+    """Convert ``mixedCase`` to ``SERPENT_CASE``"""
+    out = ""
+    for char in variable:
+        if char.isupper():
+            out += '_' + char
+            continue
+        out += char.upper()
+    return out
+
+
 LEADER_TO_WIKI = "http://serpent.vtt.fi/mediawiki/index.php/"
 
 


Test patch:
diff --git a/serpentTools/tests/test_depletion.py b/serpentTools/tests/test_depletion.py
index 32d91a8..7063f1b 100644
--- a/serpentTools/tests/test_depletion.py
+++ b/serpentTools/tests/test_depletion.py
@@ -1,5 +1,5 @@
 """Test the depletion file."""
-from unittest import TestCase
+from unittest import TestCase, skipIf
 from os import remove
 
 from numpy import array
@@ -11,7 +11,9 @@ from serpentTools.data import getFile
 from serpentTools.settings import rc
 from serpentTools.parsers.depletion import (
     DepletionReader, getMaterialNameAndVariable, getMatlabVarName,
+    prepToMatlab, deconvert,
 )
+from serpentTools.utils import hasScipy
 from serpentTools.tests.utils import LoggerMixin
 
 
@@ -260,6 +262,132 @@ class DepletionUtilTester(TestCase):
         self.assertEqual(self.BAD_0, getMatlabVarName(self.VAR_CHUNK[0]))
 
 
+class DepMatlabExportHelper(TestCase):
+    """
+    Class that tests the saveAsMatlab method for DepletionReaders
+    """
+
+    MAT_FILE_TEMPLATE = "dep_c{}_m{}.mat"
+
+    @classmethod
+    def setUpClass(cls):
+        cls.reader = DepletionReader(DEP_FILE_PATH)
+        cls.reader.read()
+
+    def setUp(self):
+        self.MAT_FILE = self.MAT_FILE_TEMPLATE.format(
+            1 if self.RECONVERT else 0,
+            1 if self.SAVE_METADATA else 0,
+        )
+
+    @skipIf(not hasScipy(), "scipy not installed")
+    def test_saveMatlab(self):
+        """
+        Verify that the reader can write data to a .mat file
+        """
+        from scipy.io import loadmat
+        self.reader.saveAsMatlab(self.MAT_FILE, self.RECONVERT,
+                                 self.SAVE_METADATA)
+        loaded = loadmat(self.MAT_FILE)
+        self.check(loaded)
+        if self.SAVE_METADATA:
+            self.checkMetadata(loaded)
+        remove(self.MAT_FILE)
+
+    def check(self, loaded):
+        """Check the contents of the data loaded from the .mat file"""
+        for materialName, material in iteritems(self.reader.materials):
+            for variableName, data in iteritems(material.data):
+                expectedName = self.constructExpectedVarName(
+                    materialName, variableName)
+
+                # savemat saves vectors as row vectors by default
+
+                if len(data.shape) == 1:
+                    data = data.reshape(1, data.size)
+                assert expectedName in loaded, (
+                    "Variable {} from material {}"
+                    .format(materialName, variableName))
+                assert_equal(
+                    loaded[expectedName], data,
+                    err_msg="{} {}".format(materialName, variableName))
+
+    def checkMetadata(self, loaded):
+        """Check the contents of the metadata loaded from mat file"""
+        pass
+
+    def constructExpectedVarName(self, material, variable):
+        """
+        Return name of the array in the .mat file for a material and variable
+        """
+        raise NotImplementedError
+
+
+class DepMatlabExportConverter(object):
+    """Mixin class that converts variables back to orgiginal form"""
+
+    RECONVERT = True
+
+    @staticmethod
+    def constructExpectedVarName(material, variable):
+        return deconvert(material, variable)
+
+
+class DepMatlabExportNoConverter(object):
+    """Mixin class that does not convert to original form"""
+
+    RECONVERT = False
+
+    @staticmethod
+    def constructExpectedVarName(material, variable):
+        return prepToMatlab(material, variable)
+
+
+class DepMatlabExportMetadataTester(DepMatlabExportHelper):
+    """Class that sends metadata to the matlab file and checks"""
+
+    SAVE_METADATA = True
+
+    def checkMetadata(self, loaded):
+        for key, data in iteritems(self.reader.metadata):
+            expected = key.upper() if self.RECONVERT else key
+            assert expected in loaded, (
+                "Missing {} from metadata".format(expected))
+
+
+class DepMatlabExportWithoutMetadataTester(DepMatlabExportHelper):
+    """Class that does not send metadata to the matlab file and checks"""
+
+    SAVE_METADATA = False
+
+    def checkMetadata(self, loaded):
+        for key, data in iteritems(self.reader.metadata):
+            assert key not in loaded, (
+                "{} from metadata present and should not be".format(key))
+
+
+class DepMatlabExportConvWithMetadata(
+        DepMatlabExportConverter, DepMatlabExportMetadataTester):
+    """Class that saves a matlab file with converted names and metadata"""
+    pass
+
+
+class DepMatlabExportConvWithoutMetadata(
+        DepMatlabExportConverter, DepMatlabExportWithoutMetadataTester):
+    """Class that saves a matlab file with converted names and no metadata"""
+    pass
+
+
+class DepMatlabExportNoConvWithMetadata(
+        DepMatlabExportNoConverter, DepMatlabExportMetadataTester):
+    """Class that saves a matlab file without converted names and metadata"""
+    pass
+
+
+del DepMatlabExportMetadataTester, DepMatlabExportWithoutMetadataTester
+del DepMatlabExportHelper
+
+
 if __name__ == '__main__':
     from unittest import main
     main()