Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #261 (2019-01-20 20:38:51)

[CLI/API] Subcommand for converting to matlab; Detector support
Moderately large PR here, but the changes are related.

## ``io`` module

One item on our [road map](https://github.com/CORE-GATECH-GROUP/serpent-tools/wiki/Road-Map) is work on exporting data from `serpentTools` to other data types. Depletion :arrow_right: MATLAB was added in #257. The ``io`` module, added in e696b5eb02d4d0f1e99c5d2581aed8420d076e0d, will contain classes to facilitate this transition. There are some file types, MATLAB `.mat` for one, that don't require a ton of writing implementation on our end, because we simply pass off to `scipy.io.savemat`. The `MatlabConverter` class added in `serpentTools.io.base` collects data from arbitrary objects, provided the objects have a `_gather_matlab` method, and writes the data to `.mat` files. A more succinct function, `serpentTools.io.toMatlab`, utilizes this class and offers a simpler, functional API for the user. Now, all we have to do is add a `_gather_matlab` method for any parser or container, and `toMatlab` can be used for that object.

## Detector :arrow_right: MATLAB

To demonstrate this capabilities, the `_gather_matlab` method was implemented for `Detector` objects and the `DetectorReader`. This allows all detector data to be written to MATLAB files very easily. Support for preserving the original names, e.g. `DEThex2` and `DETxymeshX`, is given. Alternatively, the detector quantities can be written as `hex2_bins` and `xymesh_X`.

## Command line converter

Added the `to-matlab` subcommand so that users can convert outputs to matlab without opening a python interpreter, using
```
$ python -m serpentTools to-matlab my/depletion_dep.m
```

The primary default options are to save the file under a new extension, `.m` :arrow_right: `.mat`, and to compress arrays. Support for all options from `serpentTools.io.toMatlab` are allowed, except variable names are always preserved from output to `.mat` file.

## Documentation

* Developer docs to explain converters and general philosphy
* New CLI features

## Other

Also tweaked the `cli` to exit with a specific error code in 0b04d1b4123cc31076c062263c3779e9cdc815c8. Now, should a subcommand fail with a specific error code, that is passed to the shell. 


Patch:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 5be6b1b..18a756f 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -27,3 +27,4 @@ API
    DepletionMatrix.rst
    exceptions.rst
    seed.rst
+   io.rst
diff --git a/docs/api/io.rst b/docs/api/io.rst
new file mode 100644
index 0000000..42db298
--- /dev/null
+++ b/docs/api/io.rst
@@ -0,0 +1,7 @@
+.. _api-io:
+
+===
+I/O
+===
+
+.. automodule:: serpentTools.io
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4feed0e..08f332d 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,8 @@
 .. |detectorReader| replace:: :class:`~serpentTools.parsers.detector.DetectorReader`
 .. |depletionReader| replace:: :class:`~serpentTools.parsers.depletion.DepletionReader`
 .. |depmtxReader| replace:: :class:`~serpentTools.parsers.depmatrix.DepmtxReader`
+.. |toMatlab-short| replace:: :func:`~serpentTools.io.toMatlab`
+.. |toMatlab-full| replace:: :func:`serpentTools.io.toMatlab`
 
 .. _changelog:
 
@@ -11,6 +13,26 @@
 Changelog
 =========
 
+Next
+====
+
+* CLI interface for converting some output files to matlab files - 
+  :ref:`cli-to-matlab`
+* Add :mod:`serpentTools.io` module for converting objects to
+  other data types. Currently a general function for converting
+  |toMatlab-short|
+* |detectorReader| and |detector| objects can be writen to 
+  MATLAB files using |toMatlab-full|
+
+Pending Deprecations
+--------------------
+
+* :meth:`~serpentTools.parsers.depletion.DepletionReader.saveAsMatlab` 
+  in favor of |toMatlab-full| with::
+
+      >>> from serpentTools.io import toMatlab
+      >>> toMatlab(depR)
+ 
 .. _v0.6.1:
 
 :release-tag:`0.6.1`
diff --git a/docs/command-line.rst b/docs/command-line.rst
index e40dcfe..875f92c 100644
--- a/docs/command-line.rst
+++ b/docs/command-line.rst
@@ -10,9 +10,9 @@ access this interface with::
 
     $ python -m serpentTools
 
-.. note:
+.. note::
     
-    Outputs here are accurate up to version 0.5.2+14.
+    Outputs here are accurate up to version ``0.6.1+12``
     Please alert the developers if any major differences
     are found, or a method to automatically update this
     can be found
@@ -20,21 +20,23 @@ access this interface with::
 Display the available options by passing the ``-h`` flag::
 
     $ python -m serpentTools -h
-    usage: serpentTools [-h] [--version] [-v | -q] [-c CONFIG_FILE]
-                        {seed,list} ...
+     usage: serpentTools [-h] [--version] [-v | -q] [-c CONFIG_FILE]
+                         {seed,list,to-matlab} ...
 
-    optional arguments:
-      -h, --help            show this help message and exit
-      --version             show program's version number and exit
-      -v                    increase verbosity v: info, vv: debug
-      -q                    suppress warnings and errors q: error, qq: critical
-      -c CONFIG_FILE, --config-file CONFIG_FILE
-                            path to settings file
+     optional arguments:
+       -h, --help            show this help message and exit
+       --version             show program's version number and exit
+       -v                    increase verbosity v: info, vv: debug
+       -q                    suppress warnings and errors q: error, qq: critical
+       -c CONFIG_FILE, --config-file CONFIG_FILE
+                             path to settings file
 
-    sub-commands:
-      {seed,list}           sub-command help
-        seed                copy an input file with unique random number seeds
-        list                show the default settings
+     sub-commands:
+       {seed,list,to-matlab}
+                             sub-command help
+         seed                copy an input file with unique random number seeds
+         list                show the default settings
+         to-matlab           convert output file to .mat file
 
 Random Seed Generation
 ======================
@@ -68,3 +70,47 @@ and new output directories are afforded to the user.
                             Copy files into this directory
       --link                Reference input file with include statement, rather
                             than copying the whole file
+
+.. _cli-to-matlab:
+
+Conversion to Binary ``.mat`` files
+===================================
+
+Starting in version :release-tag:`0.6.2`, ``serpentTools`` can convert text
+SERPENT output files and convert them to binary ``.mat`` files. This relies upon
+:term:`scipy`, and can be called from the command line as follows:
+
+.. code::
+
+    $ python -m serpentTools to-matlab -h
+    usage: serpentTools to-matlab [-h] [-o OUTPUT] [-a] [--format {4,5}] [-l]
+                                  [--large] [--oned {col,row}]
+                                  file
+    
+    positional arguments:
+      file                  output file to read and convert
+    
+    optional arguments:
+      -h, --help            show this help message and exit
+      -o OUTPUT, --output OUTPUT
+                            Name of output file to write. If not given, replace
+                            extension with .mat
+      -a, --append          Append to existing file if found. Otherwise overwrite.
+                            Default: False
+      --format {4,5}        Format of file to write. 4 for MATLAB 4, 5 for MATLAB
+                            5+. Default: 5
+      -l, --longNames       Allow variable names up to 63 characters. Otherwise,
+                            enforce 31 character names. Default: False
+      --large               Don't compress arrays when writing.
+      --oned {col,row}      Write 1D arrays are row or column vectors
+
+Conversion will exit with no errors if the file is able to be converted, or with
+the following exit codes:
+
+   * ``1``: :term:`scipy` not found
+   * ``3``: That file type is not supported at this time.
+
+If you desperately need a file type to be converted, please reach out to the developers
+on the `GH Issue board <https://www.github.com/CORE-GATECH-GROUP/serpent-tools/issues>`_.
+Alternatively, if you're feeling ambitious, follow through the :ref:`dev-guide` for guidelines
+on adding the feature and submitting a pull request.
diff --git a/docs/develop/converter.rst b/docs/develop/converter.rst
new file mode 100644
index 0000000..4bf7018
--- /dev/null
+++ b/docs/develop/converter.rst
@@ -0,0 +1,49 @@
+.. |matlabConv| replace:: :class:`~serpentTools.io.base.MatlabConverter`
+
+.. _dev-convert:
+
+==================
+Converter Utilites
+==================
+
+One goal for this project is to allow users to use ``serpentTools`` as a
+pipeline for taking :term:`SERPENT` data to other programs.
+The first demonstration of this is creating binary ``.mat`` files that can
+be read into MATLAB, when a large text file would cause issues, done with
+the |matlabConv|.
+
+Classes intending do first look for specific methods for gathering data 
+on each object. These are obscured from the public API as they don't serve
+a direct purpose besides a simple method by which to translate data
+from one place to the next.
+This collection is called inside the primary ``convert`` method,
+which is reponsible for gathering and writing the data into the new
+form.
+
+Keeping with the |matlabConv| as an example, it looks for
+``_gather_matlab`` method. This method simply places data into
+a dictionary, but do to the layout of each object, must be reimplemented
+as a unique method.
+Due to the simplicity of :func:`scipy.io.savemat`, the main writing function,
+:meth:`serpentTools.io.base.MatlabConverter.convert`, is rather light.
+However, some data forms may require more depth to ensure fidelity in the 
+converted data.
+
+There may be some cases where a conversion does not make sense
+for all data types. Detector objects are the few pieces of data that
+can have a spatial grid attached, yet this grid varies across detectors.
+It may be advantageous to implement converters directly as instance methods.
+The decision will be up to the developer and the review process in that case.
+
+Converters should also make sure, prior to potentially expensive collection
+steps are taken, that the user is in the best position to execute this
+operation. As of this writing and :release-tag:`0.6.1`, :term:`scipy`
+is not a requirement for this package, yet it is required for MATLAB
+conversion. Therefore, the |matlabConv| implements a check for :term:`scipy`
+in :meth:`~serpentTools.io.base.MatlabConverter.checkImports`. The other 
+check method, :meth:`~serpentTools.io.base.MatlabConverter.checkContainerReq`,
+is responsible for checking the container has the required data gathering
+routines, e.g. ``_gather_matlab``.
+These checks are called at creation of the converter object.
+
+.. autoclass:: serpentTools.io.base.MatlabConverter
diff --git a/docs/develop/index.rst b/docs/develop/index.rst
index e616b27..e9e00c9 100644
--- a/docs/develop/index.rst
+++ b/docs/develop/index.rst
@@ -19,6 +19,7 @@ without any loss of comprehension.
     plotting.rst
     utils.rst
     plot-routines.rst
+    converter.rst
     engines.rst
     documentation.rst
     checklist.rst
diff --git a/serpentTools/__main__.py b/serpentTools/__main__.py
index cb72ed2..442c381 100644
--- a/serpentTools/__main__.py
+++ b/serpentTools/__main__.py
@@ -8,11 +8,15 @@ Called with::
 """
 import re
 import argparse
+from os.path import splitext
 
 import six
 
 import serpentTools
 from serpentTools import settings
+from serpentTools.messages import info, debug, error
+from serpentTools.parsers import inferReader
+from serpentTools.io import MatlabConverter
 
 _VERB_MAP = {'v': {1: 'info', 2: 'debug'},
              'q': {1: 'error', 2: 'critical'}}
@@ -69,14 +73,82 @@ def __buildParser():
                                help='show settings that match this pattern')
     listSettingsP.set_defaults(func=_listDefaults)
 
+    # convert input file to matlab
+    matlabParser = subParsers.add_parser(
+        'to-matlab', help="convert output file to .mat file")
+    matlabParser.add_argument('file', type=str,
+                              help="output file to read and convert")
+    matlabParser.add_argument(
+        '-o', '--output', type=str, default=None,
+        help='Name of output file to write. '
+             'If not given, replace extension with .mat')
+    matlabParser.add_argument(
+        '-a', '--append', default=False, action='store_true',
+        help="Append to existing file if found. Otherwise overwrite. "
+             "Default: False")
+    matlabParser.add_argument(
+        '--format', type=str, choices={'5', '4'}, default='5',
+        help="Format of file to write. 4 for MATLAB 4, 5 for MATLAB 5+. "
+             "Default: 5")
+    matlabParser.add_argument(
+        '-l', '--longNames', default=False, action='store_true',
+        help="Allow variable names up to 63 characters. "
+        "Otherwise, enforce 31 character names. Default: False")
+    matlabParser.add_argument(
+        '--large', help="Don't compress arrays when writing.",
+        action='store_true', default=False)
+    matlabParser.add_argument(
+        '--oned', type=str, choices={'row', 'col'}, default='row',
+        help="Write 1D arrays are row or column vectors")
+    matlabParser.set_defaults(func=_toMatlab)
+
     return mainParser
 
 
+def _toMatlab(args):
+    """
+    Write contents of a file to matlab.
+
+    Return codes:
+        0: all good
+        1: need scipy
+        3: conversion for file type not supported yet
+    """
+    inFile = args.file
+    outFile = args.output
+    if not outFile:
+        base = splitext(inFile)[0]
+        outFile = base + '.mat'
+        herald = info
+    else:
+        herald = debug
+
+    # inferReader returns the class, but we need an instance
+    reader = inferReader(inFile)(inFile)
+    try:
+        converter = MatlabConverter(reader, outFile)
+    except ImportError:
+        error("scipy >= 1.0 required to convert to matlab")
+        return 1
+    except NotImplementedError:
+        error("Conversion not supported for {} reader at this time. "
+              .format(reader.__class__.__name__))
+        error("Please alert the developers of your need.")
+        return 3
+    reader.read()
+    converter.convert(True, append=args.append,
+                      format=args.format, longNames=args.longNames,
+                      compress=not args.large, oned=args.oned)
+    herald("Wrote contents from {} to {}".format(inFile, outFile))
+    return 0
+
+
 def _seedInterface(args):
     """Interface to launch the uniquely-seeded file generation"""
     from serpentTools.seed import seedFiles
-    seedFiles(args.file, args.N, seed=args.seed, outputDir=args.output_dir,
-              link=args.link, length=args.length)
+    return seedFiles(args.file, args.N, seed=args.seed,
+                     outputDir=args.output_dir,
+                     link=args.link, length=args.length)
 
 
 def _listDefaults(args):
@@ -112,8 +184,9 @@ def main():
     args = parser.parse_args()
     __process(args)
     if 'func' in args:
-        args.func(args)
+        return args.func(args)
 
 
 if __name__ == '__main__':
-    main()
+    from sys import exit
+    exit(main())
diff --git a/serpentTools/io/__init__.py b/serpentTools/io/__init__.py
new file mode 100644
index 0000000..2ccae95
--- /dev/null
+++ b/serpentTools/io/__init__.py
@@ -0,0 +1,81 @@
+"""
+.. versionadded:: 0.6.2
+
+Module for converting/creating ``serpentTools`` objects to/from other sources
+
+High-level functions implemented here, such as :func:`toMatlab`, help the
+:ref:`cli` in quickly converting files without launching a Python interpreter.
+For example,
+
+.. code::
+
+    $ python -m serpentTools -v to-matlab my/depletion_dep.m
+    INFO  : serpentTools: Wrote contents of my/depletion_dep.m to
+    my/depletion_dep.mat
+
+"""
+
+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/base.py b/serpentTools/io/base.py
new file mode 100644
index 0000000..faeab3e
--- /dev/null
+++ b/serpentTools/io/base.py
@@ -0,0 +1,113 @@
+"""
+Classes for converting readers and containers to other data types
+"""
+
+from abc import ABCMeta, abstractmethod
+from six import add_metaclass
+from serpentTools.utils import checkScipy
+
+__all__ = [
+    'MatlabConverter',
+]
+
+
+@add_metaclass(ABCMeta)
+class BaseConverter(object):
+    """
+    Base class for setting up conversion routines
+
+    Each subclass should implement
+
+    * ``checkContainerReq`` for checking if the container is
+      capable of writing
+    * ``checkImports`` for checking if the user has the required
+      libraries required, at required versions
+    """
+
+    def __init__(self, container, output):
+        self.checkContainerReq(container)
+        self.checkImports()
+        self.container = container
+        self.output = output
+
+    def checkContainerReq(self, container):
+        """Ensure that the requirements to convert are met"""
+        pass
+
+    def checkImports(self):
+        """Ensure that any required libraries are included"""
+        pass
+
+    @abstractmethod
+    def convert(self, *args, **kwargs):
+        """Perform the conversion"""
+        raise NotImplementedError
+
+
+class MatlabConverter(BaseConverter):
+    """
+    Class for assisting in writing container data to MATLAB
+
+    Parameters
+    ----------
+    obj: ``serpentTools`` container
+        Parser or container to be written to file
+    fileP: str or file-like object
+        Name of the file to write
+
+    See Also
+    --------
+    * :func:`serpentTools.io.toMatlab` - high level function for
+      writing to MATLAB that uses this class
+    """
+
+    def checkContainerReq(self, container):
+        """Ensure that data from the container can be collected."""
+        if not hasattr(container, '_gather_matlab'):
+            raise NotImplementedError(
+                "Gathering method not implemented for {}."
+                .format(container.__class__.__name__))
+
+    def checkImports(self):
+        """Ensure that :term:`scipy` >= 1.0 is installed."""
+        if not checkScipy('1.0'):
+            raise ImportError("scipy >= 1.0 required")
+
+    def convert(self, reconvert, append=True, format='5', longNames=True,
+                compress=True, oned='row'):
+        """
+        Save contents of object to ``.mat`` file
+
+        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``.
+        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
+
+        See Also
+        --------
+        :func:`scipy.io.savemat`
+        """
+        from scipy.io import savemat
+        data = self.container._gather_matlab(reconvert)
+        return savemat(self.output, data, append, format, longNames,
+                       compress, oned)
diff --git a/serpentTools/objects/detectors.py b/serpentTools/objects/detectors.py
index 32a6706..717db5a 100644
--- a/serpentTools/objects/detectors.py
+++ b/serpentTools/objects/detectors.py
@@ -133,6 +133,20 @@ class Detector(DetectorBase):
             return self._INDEX_MAP[indexPos]
         return DET_COLS[indexPos]
 
+    def _gather_matlab(self, reconvert):
+        """Gather bins and grids for exporting to matlab"""
+
+        converter = deconvert if reconvert else prepToMatlab
+
+        data = {
+            converter(self.name, 'bins'): self.bins,
+        }
+
+        for key, value in iteritems(self.grids):
+            data[converter(self.name, key)] = value
+
+        return data
+
 
 class CartesianDetector(Detector):
     __doc__ = """
@@ -522,3 +536,21 @@ def checkClearKwargs(key, fName, **kwargs):
     if key in kwargs and kwargs[key] is not None:
         warning("{} will be set by {}. Worry not.".format(key, fName))
         kwargs[key] = None
+
+
+# Functions for converting varible names for matlab
+
+_DET_FMT_ORIG = "DET{}{}"
+_DET_FMT_CC = "{}_{}"
+
+
+def deconvert(detectorName, quantity):
+    """Restore the original name of the detector data"""
+    if quantity == 'bins':
+        return _DET_FMT_ORIG.format(detectorName, '')
+    return _DET_FMT_ORIG.format(detectorName, quantity)
+
+
+def prepToMatlab(detectorName, quantity):
+    """Create a name of a variable for MATLAB"""
+    return _DET_FMT_CC.format(detectorName, quantity)
diff --git a/serpentTools/parsers/base.py b/serpentTools/parsers/base.py
index a4c1794..cfba981 100644
--- a/serpentTools/parsers/base.py
+++ b/serpentTools/parsers/base.py
@@ -15,7 +15,7 @@ from numpy import array
 from serpentTools.messages import info
 from serpentTools.settings import rc
 from serpentTools.objects.base import BaseObject
-from serpentTools.utils import hasScipy
+from serpentTools.utils import checkScipy
 
 
 @add_metaclass(ABCMeta)
@@ -138,7 +138,7 @@ class SparseReaderMixin(object):
     """
 
     def __init__(self, sparse):
-        HAS_SCIPY = hasScipy()
+        HAS_SCIPY = checkScipy()
         if sparse is None:
             self.__sparse = HAS_SCIPY
         elif sparse is True and not HAS_SCIPY:
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 269f229..dbd2e7f 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -1,5 +1,6 @@
 """Parser responsible for reading the ``*dep.m`` files"""
 import re
+from warnings import warn
 
 from matplotlib import pyplot
 from six import iteritems
@@ -24,6 +25,7 @@ from serpentTools.utils import (
     DEF_COMP_UPPER,
     DEF_COMP_SIGMA,
 )
+from serpentTools.io import MatlabConverter
 
 METADATA_KEYS = {'ZAI', 'NAMES', 'BU', 'DAYS'}
 
@@ -354,19 +356,12 @@ class DepletionReader(DepPlotMixin, MaterialReader):
             lower, upper, 'burnup')
         return similar
 
-    def saveAsMatlab(self, fileP, reconvert=True, metadata=True,
-                     **savematKwargs):
+    def saveAsMatlab(self, fileP, reconvert=True, metadata=None,
+                     append=True, format='5', longNames=True,
+                     compress=True, oned='row'):
         """
         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
@@ -379,9 +374,21 @@ class DepletionReader(DepPlotMixin, MaterialReader):
         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`
+        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
         ------
@@ -392,8 +399,28 @@ class DepletionReader(DepPlotMixin, MaterialReader):
         --------
         :func:`scipy.io.savemat`
         """
-        from scipy.io import savemat
+        if metadata is not None:
+            # need this path to perserve selecting not to write
+            # metadata
+            from scipy.io import savemat
+            data = self._gather_matlab(reconvert, metadata)
+            return savemat(fileP, data, append, format, longNames,
+                           compress, oned)
+        converter = MatlabConverter(self, fileP)
+        return converter.convert(reconvert, append, format, longNames,
+                                 compress, oned)
+
+    def _gather_matlab(self, reconvert, metadata=None):
+        """
+        Return a dictionary with all data
+
+        Should only be used by developers for converting to new
+        output file types
+        """
 
+        if metadata is not None:
+            warn("version 0.7.0+ will not use metadata argument and write "
+                 "all by default.", FutureWarning)
         # set these here to reduce number of if/elses
 
         if reconvert:
@@ -401,7 +428,8 @@ class DepletionReader(DepPlotMixin, MaterialReader):
         else:
             converter = prepToMatlab
 
-        if metadata:
+        # remove for v0.7.0
+        if metadata and self.metadata:
             data = {k.upper() if reconvert else k: v
                     for k, v in iteritems(self.metadata)}
         else:
@@ -411,7 +439,7 @@ class DepletionReader(DepPlotMixin, MaterialReader):
             for varName, varData in iteritems(material.data):
                 data[converter(matName, varName)] = varData
 
-        savemat(fileP, data, **savematKwargs)
+        return data
 
 
 def getMaterialNameAndVariable(mlabName):
diff --git a/serpentTools/parsers/detector.py b/serpentTools/parsers/detector.py
index e63bfaa..516dd89 100644
--- a/serpentTools/parsers/detector.py
+++ b/serpentTools/parsers/detector.py
@@ -110,6 +110,15 @@ class DetectorReader(BaseReader):
             similar &= myDetector.compare(otherDetector, lower, upper, sigma)
         return similar
 
+    def _gather_matlab(self, reconvert):
+        """Collect data from all detectors for exporting to matlab"""
+        data = {}
+
+        for detector in self.detectors.values():
+            data.update(detector._gather_matlab(reconvert))
+
+        return data
+
 
 def cleanDetChunk(chunk):
     """
diff --git a/serpentTools/utils/__init__.py b/serpentTools/utils/__init__.py
index 160e617..cc2bd39 100644
--- a/serpentTools/utils/__init__.py
+++ b/serpentTools/utils/__init__.py
@@ -1,11 +1,7 @@
 """
 Commonly used functions and utilities
 """
-try:
-    import scipy  # noqa
-    __HAS_SCIPY = True
-except ImportError:
-    __HAS_SCIPY = False
+from distutils.version import StrictVersion
 
 from serpentTools.utils.core import *  # noqa
 from serpentTools.utils.docstrings import *  # noqa
@@ -13,6 +9,12 @@ from serpentTools.utils.compare import *  # noqa
 from serpentTools.utils.plot import *  # noqa
 
 
-def hasScipy():
-    """Return ``True`` if :term:`scipy` is installed."""
-    return __HAS_SCIPY is True
+def checkScipy(version=None):
+    """Return ``True`` if the given version of :term:`scipy` is installed"""
+    try:
+        import scipy
+    except ImportError:
+        return False
+    if version is None:
+        return True
+    return StrictVersion(scipy.__version__) >= StrictVersion(version)
diff --git a/setup.py b/setup.py
index ff0624e..fb579d0 100644
--- a/setup.py
+++ b/setup.py
@@ -57,7 +57,7 @@ pythonRequires = '>=2.7,!=3.0,!=3.1,!=3.2,!=3.3,!=3.4'
 setupArgs = {
     'name': 'serpentTools',
     'packages': ['serpentTools', 'serpentTools.parsers', 'serpentTools.utils',
-                 'serpentTools.data', 'serpentTools.tests',
+                 'serpentTools.data', 'serpentTools.tests', 'serpentTools.io',
                  'serpentTools.objects', 'serpentTools.samplers'],
     'url': 'https://github.com/CORE-GATECH-GROUP/serpent-tools',
     'description': ('A suite of parsers designed to make interacting with '


Test patch:
diff --git a/serpentTools/tests/test_depletion.py b/serpentTools/tests/test_depletion.py
index 7063f1b..83d3ea8 100644
--- a/serpentTools/tests/test_depletion.py
+++ b/serpentTools/tests/test_depletion.py
@@ -13,7 +13,7 @@ from serpentTools.parsers.depletion import (
     DepletionReader, getMaterialNameAndVariable, getMatlabVarName,
     prepToMatlab, deconvert,
 )
-from serpentTools.utils import hasScipy
+from serpentTools.utils import checkScipy
 from serpentTools.tests.utils import LoggerMixin
 
 
@@ -280,7 +280,7 @@ class DepMatlabExportHelper(TestCase):
             1 if self.SAVE_METADATA else 0,
         )
 
-    @skipIf(not hasScipy(), "scipy not installed")
+    @skipIf(not checkScipy(), "scipy not installed")
     def test_saveMatlab(self):
         """
         Verify that the reader can write data to a .mat file
diff --git a/serpentTools/tests/test_toMatlab.py b/serpentTools/tests/test_toMatlab.py
new file mode 100644
index 0000000..92514be
--- /dev/null
+++ b/serpentTools/tests/test_toMatlab.py
@@ -0,0 +1,82 @@
+"""
+Tests for testing the MATLAB conversion functions
+"""
+
+from os import remove
+from unittest import TestCase, SkipTest
+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
+
+HAS_SCIPY = checkScipy('1.0')
+
+
+class Det2MatlabHelper(TestCase):
+    """Helper class for testing detector to matlab conversion"""
+
+    NBINS = 10
+    NCOLS = 12
+    # approximate some detector data
+    BINS = arange(
+        NCOLS * NBINS, dtype=float).reshape(NBINS, NCOLS)
+    # emulate energy grid
+    GRID = arange(3 * NBINS).reshape(NBINS, 3)
+    GRID_KEY = 'E'
+
+    @classmethod
+    def setUpClass(cls):
+        cls.detector = Detector('matlabtest')
+        cls.detector.bins = cls.BINS
+        cls.detector.grids[cls.GRID_KEY] = cls.GRID
+
+    def setUp(self):
+        # skip tests if scipy not installed
+        if not HAS_SCIPY:
+            raise SkipTest("scipy needed to test matlab conversion")
+
+        from serpentTools.objects.detectors import deconvert, prepToMatlab
+        # instance methods and/or rename them
+        # potential issues sending putting many such functions in this
+        # test suite
+
+        self.converterFunc = deconvert if self.CONVERT else prepToMatlab
+
+    def test_det2Matlab(self):
+        """Test the conversion to matlab files"""
+        from scipy.io import loadmat
+        filePath = 'detector_{}.mat'.format(
+            'conv' if self.CONVERT else 'unconv')
+        toMatlab(self.detector, filePath, self.CONVERT, append=False)
+        fromMatlab = loadmat(filePath)
+
+        binsKey = self.converterFunc(self.detector.name, 'bins')
+        self.assertTrue(binsKey in fromMatlab)
+        assert_array_equal(fromMatlab[binsKey], self.detector.bins)
+
+        gridKey = self.converterFunc(self.detector.name, self.GRID_KEY)
+        self.assertTrue(gridKey in fromMatlab)
+        assert_array_equal(fromMatlab[gridKey],
+                           self.detector.grids[self.GRID_KEY])
+
+        remove(filePath)
+
+
+class ConvertedDet2MatlabTester(Det2MatlabHelper):
+    """Test the process of writing detector data w/ original names"""
+
+    CONVERT = True
+
+
+class UnconvertedDet2MatlabTester(Det2MatlabHelper):
+    """Test the process of writing detector data w/ custom names"""
+
+    CONVERT = False
+
+
+del Det2MatlabHelper
+
+if __name__ == '__main__':
+    from unittest import main
+    main()