Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #291 (2019-03-06 20:55:17)

API Add toMatlab methods for Depletion, Detector, Depmtx, Sens, Hist readers
This PR provides `toMatlab` methods for `DepletionReader`, `DetectorReader`, `DepmtxReader`, `SensitivityReader`, `HistoryReader`, and `Detector`-type objects. Rather than write a nearly identical function for all objects, the methods were added with what I'm (probably incorrectly) calling ``hooks``. From the `serpentTools.io.hooks` module:

https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/97da7ffdb01d1b6a95e86fc333ccca99aa6f6f62/serpentTools/io/hooks.py#L4-L27

By adding in `serpentTools.parsers.__init__.py` and `serpentTools.objects.detectors` lines like
```
Detector.toMatlab = matlabHook
```
we are able to create a "method" that can be documented and appears just like any other method on the object.

Updated some tests accordingly, since the `io.toMatlab` function isn't needed (almost entirely converted to the `matlabHook`)

Also updated the min numpy version to be `>=1.16`. We can probably go to a lower version, but we should wait until we hear from some users.


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index d4dc4e5..01b8b4c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -13,6 +13,17 @@
 Changelog
 =========
 
+Next
+====
+
+* Easier construction of |branchCollector| objects with
+  :class:`~serpentTools.xs.BranchCollector.fromFile`.
+* Don't require passing branch information to |branchCollector|. Will be infered
+  from file and set with ``(p1, p2, ...)``. State data can be used to 
+  determine which index is a given perturbation type.
+* Direct ``toMatlab`` methods for ``SensitivityReader``, |depmtxReader|
+  |depletionReader|, |detectorReader|, ``HistoryReader``, and |detector| objects.
+
 .. _v0.6.2:
 
 :release-tag:`0.6.2`
diff --git a/requirements.txt b/requirements.txt
index 90efba1..10e1d56 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
 six==1.12.0
-numpy==1.16.0
+numpy>=1.16.0
 matplotlib>=2.2.3
 pyyaml==3.13
diff --git a/serpentTools/io/__init__.py b/serpentTools/io/__init__.py
index 2ccae95..6491d4d 100644
--- a/serpentTools/io/__init__.py
+++ b/serpentTools/io/__init__.py
@@ -16,66 +16,3 @@ For example,
 """
 
 from serpentTools.io.base import *  # noqa
-
-
-def toMatlab(obj, fileP, reconvert=True, append=True,
-             format='5', longNames=True, compress=True,
-             oned='row'):
-        """
-        Write a binary MATLAB file from the contents of this object
-
-        Parameters
-        ----------
-        obj: ``serpentTools`` container
-            Parser or container to be written to file
-        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.
-        append: bool
-            If true and a file exists under ``output``, append to that file.
-            Otherwise the file will be overwritten
-        format: {'5', '4'}
-            Format of file to write. ``'5'`` for MATLAB 5 to 7.2, ``'4'`` for
-            MATLAB 4
-        longNames: bool
-            If true, allow variable names to reach 63 characters,
-            which works with MATLAB 7.6+. Otherwise, maximum length is
-            31 characters
-        compress: bool
-            If true, compress matrices on write
-        oned: {'row', 'col'}:
-            Write one-dimensional arrays as row vectors if
-            ``oned=='row'`` (default), or column vectors
-
-        Raises
-        ------
-        ImportError:
-            If :term:`scipy` is not installed
-        NotImplementedError
-            If the passed object does not support exporting to MATLAB
-
-        See Also
-        --------
-        * :func:`scipy.io.savemat`
-        * :class:`serpentTools.io.base.MatlabConverter`
-
-        Example
-        -------
-
-        >>> import serpentTools
-        >>> from serpentTools.io import toMatlab
-        >>> det = serpentTools.read('my_det0.m')
-        # Write all detectors and grids from file
-        # with their original names
-        >>> toMatlab(det, 'output.mat',
-                     reconvert=True)
-        # Write a single detector to the same file
-        # but under shorter names
-        >>> toMatlab(det['demo'], 'output.mat',
-                     reconvert=False, append=True)
-        """
-        converter = MatlabConverter(obj, fileP)  # noqa imported from *
-        return converter.convert(
-            reconvert, append, format, longNames, compress, oned)
diff --git a/serpentTools/io/hooks.py b/serpentTools/io/hooks.py
new file mode 100644
index 0000000..605f329
--- /dev/null
+++ b/serpentTools/io/hooks.py
@@ -0,0 +1,73 @@
+"""
+Hooks for creating adding direct methods for conversion
+
+These functions should be added to classes in the following
+manner::
+
+    class HookedObj(object):
+
+        def demo(self):
+            print("Hello")
+
+
+    def demoHook(obj, [args], [**kwargs]):
+        # operate as if object is a class instance
+        print("Inside demoHook")
+        obj.demo()
+
+    HookedObj.hooked = demoHook
+
+This allows the following implementation::
+
+    >>> h = HookedObj()
+    >>> h.demo()
+    "Hello"
+    >>> h.hooked()
+    "Inside demoHook"
+    "Hello"
+
+"""
+from serpentTools.io import MatlabConverter
+
+
+def matlabHook(obj, fileP, reconvert=True, append=True,
+               format='5', longNames=True, compress=True, oned='row'):
+    """
+    Write a binary MATLAB file from the contents of this object
+
+    Parameters
+    ----------
+    fileP: str or file-like object
+        Name of the file to write. ``.mat`` extension is not needed
+        if ``append==True``
+    reconvert: bool
+        If this evaluates to true, convert values back into their
+        original form as they appear in the output file.
+    append: bool
+        If true and a file exists under ``output``, append to that file.
+        Otherwise the file will be overwritten
+    format: {'5', '4'}
+        Format of file to write. ``'5'`` for MATLAB 5 to 7.2, ``'4'`` for
+        MATLAB 4
+    longNames: bool
+        If true, allow variable names to reach 63 characters,
+        which works with MATLAB 7.6+. Otherwise, maximum length is
+        31 characters
+    compress: bool
+        If true, compress matrices on write
+    oned: {'row', 'col'}:
+        Write one-dimensional arrays as row vectors if
+        ``oned=='row'`` (default), or column vectors
+
+    Raises
+    ------
+    ImportError:
+        If :term:`scipy` is not installed
+
+    See Also
+    --------
+    * :func:`scipy.io.savemat`
+    """
+    converter = MatlabConverter(obj, fileP)
+    return converter.convert(
+        reconvert, append, format, longNames, compress, oned)
diff --git a/serpentTools/objects/detectors.py b/serpentTools/objects/detectors.py
index 378c117..98b8fc9 100644
--- a/serpentTools/objects/detectors.py
+++ b/serpentTools/objects/detectors.py
@@ -37,6 +37,7 @@ from serpentTools.utils import (
     magicPlotDocDecorator, formatPlot, setAx_xlims, setAx_ylims,
     addColorbar, normalizerFactory, linkToWiki,
 )
+from serpentTools.io.hooks import matlabHook
 
 __all__ = ['Detector', 'CartesianDetector', 'HexagonalDetector',
            'CylindricalDetector', 'SphericalDetector']
@@ -153,6 +154,9 @@ class Detector(DetectorBase):
         return data
 
 
+Detector.toMatlab = matlabHook
+
+
 class CartesianDetector(Detector):
     __doc__ = """
     Class that stores detector data containing cartesian meshing.
diff --git a/serpentTools/parsers/__init__.py b/serpentTools/parsers/__init__.py
index 0021a5e..55ff6f5 100644
--- a/serpentTools/parsers/__init__.py
+++ b/serpentTools/parsers/__init__.py
@@ -17,6 +17,7 @@ from serpentTools.parsers.xsplot import XSPlotReader
 from serpentTools.parsers.sensitivity import SensitivityReader
 from serpentTools.parsers.microxs import MicroXSReader
 from serpentTools.parsers.depmatrix import DepmtxReader, readDepmtx
+from serpentTools.io.hooks import matlabHook
 
 READERS = {
     'dep': DepletionReader,
@@ -194,3 +195,14 @@ def depmtx(fileP):
     n0 = n0.reshape(n0.size, 1)
     n1 = n1.reshape(n1.size, 1)
     return t, n0, zai, a, n1
+
+
+#
+# Hooks for exporting to other data types
+#
+
+DepletionReader.toMatlab = matlabHook
+DetectorReader.toMatlab = matlabHook
+SensitivityReader.toMatlab = matlabHook
+HistoryReader.toMatlab = matlabHook
+DepmtxReader.toMatlab = matlabHook


Test patch:
diff --git a/serpentTools/tests/test_sensivity.py b/serpentTools/tests/test_sensivity.py
index 7d39651..0e2f999 100644
--- a/serpentTools/tests/test_sensivity.py
+++ b/serpentTools/tests/test_sensivity.py
@@ -1,21 +1,21 @@
 """
 Test the sensitivity reader
 """
-from os import remove
 from unittest import TestCase, skipUnless
 from collections import OrderedDict
 from itertools import product
 
-from six import iteritems
+from six import iteritems, BytesIO
 from numpy import array, inf
 from numpy.testing import assert_allclose, assert_array_equal
 
 from serpentTools.data import getFile
 from serpentTools.parsers.sensitivity import SensitivityReader
-from serpentTools.io import toMatlab
 from serpentTools.utils import checkScipy
 from serpentTools.tests import compareDictOfArrays
-from serpentTools.tests.utils import (plotTest, getLegendTexts)
+from serpentTools.tests.utils import (
+    plotTest, getLegendTexts, MatlabTesterHelper,
+)
 
 TEST_FILE = getFile('bwr_sens0.m')
 
@@ -288,7 +288,7 @@ class SensitivityPlotTester(SensitivityTestHelper):
             self.reader.plot("THIS SHOULD FAIL")
 
 
-class Sens2MatlabHelper(SensitivityTestHelper):
+class Sens2MatlabHelper(MatlabTesterHelper, SensitivityTestHelper):
     """Base class for comparing sensitivity reader to matlab"""
 
     def setUp(self):
@@ -300,6 +300,7 @@ class Sens2MatlabHelper(SensitivityTestHelper):
             key: value[convertIx] for key, value in iteritems(
                 SensitivityReader._RECONVERT_LIST_MAP)}
         self.sensFmts = SensitivityReader._RECONVERT_SENS_FMT[convertIx]
+        MatlabTesterHelper.setUp(self)
 
     def _testGathered(self, gathered):
         """Test the contents of the gathered data"""
@@ -340,9 +341,9 @@ class Sens2MatlabHelper(SensitivityTestHelper):
     def test_toMatlab(self):
         """Verify the contents of the reader can be written to matlab"""
         from scipy.io import loadmat
-        fp = "sens2matlab_r{}.mat".format(int(self.RECONVERT))
-        toMatlab(self.reader, fp)
-        gathered = loadmat(fp)
+        stream = BytesIO()
+        self.reader.toMatlab(stream)
+        gathered = loadmat(stream)
         # some vectors will be written as 2D row/column vectors
         # need to reshape them to 1D arrays
         keys = gathered.keys()
@@ -353,8 +354,6 @@ class Sens2MatlabHelper(SensitivityTestHelper):
             if value.size > 1 and 1 in value.shape:
                 gathered[key] = value.flatten()
 
-        remove(fp)
-
 
 class ReconvertedSens2MatlabTester(Sens2MatlabHelper):
     """Class for testing the sens - matlab conversion with original names"""
diff --git a/serpentTools/tests/test_toMatlab.py b/serpentTools/tests/test_toMatlab.py
index 33be5fc..bf51ef1 100644
--- a/serpentTools/tests/test_toMatlab.py
+++ b/serpentTools/tests/test_toMatlab.py
@@ -2,28 +2,14 @@
 Tests for testing the MATLAB conversion functions
 """
 
-from unittest import TestCase, SkipTest
 from six import BytesIO, iteritems
 from numpy import arange
 from numpy.testing import assert_array_equal
 from serpentTools.objects import Detector
-from serpentTools.utils import checkScipy
-from serpentTools.io import toMatlab
+from serpentTools.tests.utils import MatlabTesterHelper
 from serpentTools.data import getFile
 from serpentTools.parsers import DepmtxReader
 
-HAS_SCIPY = checkScipy('1.0')
-
-
-class MatlabTesterHelper(TestCase):
-    """Helper class for matlab conversion"""
-
-    def setUp(self):
-        """Call this from subclasses to skip if scipy is unavailable"""
-        # skip tests if scipy not installed
-        if not HAS_SCIPY:
-            raise SkipTest("scipy needed to test matlab conversion")
-
 
 class Det2MatlabHelper(MatlabTesterHelper):
     """Helper class for testing detector to matlab conversion"""
@@ -57,7 +43,7 @@ class Det2MatlabHelper(MatlabTesterHelper):
         """Test the conversion to matlab files"""
         from scipy.io import loadmat
         writer = BytesIO()
-        toMatlab(self.detector, writer, self.CONVERT, append=False)
+        self.detector.toMatlab(writer, self.CONVERT, append=False)
         fromMatlab = loadmat(writer)
 
         binsKey = self.converterFunc(self.detector.name, 'bins')
@@ -115,7 +101,7 @@ class DepmtxMatlabHelper(MatlabTesterHelper):
     def test_depmtxToMatlab(self):
         """Verify the depmtx reader can be written to matlab file"""
         from scipy.io import loadmat
-        toMatlab(self.reader, self.outFile, self.RECONVERT)
+        self.reader.toMatlab(self.outFile, self.RECONVERT)
         written = loadmat(self.outFile)
         self.assertEqual(self.reader.deltaT, written['t'])
         for expKey, expValue in iteritems(self.expected):
diff --git a/serpentTools/tests/utils.py b/serpentTools/tests/utils.py
index 5fcc4ae..c8fcbc3 100644
--- a/serpentTools/tests/utils.py
+++ b/serpentTools/tests/utils.py
@@ -3,12 +3,13 @@ Utilities to make testing easier
 """
 
 from functools import wraps
-from unittest import TestCase
+from unittest import TestCase, SkipTest
 from logging import NOTSET
 
 from serpentTools.messages import (
     DictHandler, __logger__, removeHandler, addHandler,
 )
+from serpentTools.utils import checkScipy
 
 
 class LoggerMixin(object):
@@ -166,6 +167,19 @@ class TestCaseWithLogCapture(TestCase, LoggerMixin):
                          msg=failMsg.format(matchType, msg, level))
 
 
+HAS_SCIPY = checkScipy('1.0')
+
+
+class MatlabTesterHelper(TestCase):
+    """Helper class for matlab conversion"""
+
+    def setUp(self):
+        """Call this from subclasses to skip if scipy is unavailable"""
+        # skip tests if scipy not installed
+        if not HAS_SCIPY:
+            raise SkipTest("scipy needed to test matlab conversion")
+
+
 def plotTest(f):
     """Decorator that clears up existing plots prior to test."""
     from matplotlib.pyplot import close, figure