Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #390 (2020-03-29 22:05:07)

API Expose DepletedMaterial data arrays as attributes
The following attributes map directly to contents of the data dictionary, returning None if not present:
```
activity -> a
adens -> adens
burnup -> burnup
decayHeat -> h
ingTox -> ingTox
inhTox -> inhTox
mdens -> mdens
photonProdRate -> gsrc
spontaneousFissionRate -> sf
volume -> volume
```
This allows the user to perform `m.adens` or `m.photonProdRate` to directly fetch data from the data dictionary. In the future, these might be the preferred way to store / fetch data, but that's to be seen. 

The docstrings have also been improved with these new quantities.


Patch:
diff --git a/serpentTools/data/plut_xs0.m b/serpentTools/data/plut_xs0.m
index f7f5610..bceb792 100644
--- a/serpentTools/data/plut_xs0.m
+++ b/serpentTools/data/plut_xs0.m
@@ -191,24 +191,6 @@ i94239_03c_xs = [
 5.87774E+00 2.27284E-03 2.72848E+00 2.41217E+00 2.27284E-03 9.49498E-02 1.42231E-01 1.91357E-02 2.38983E-02 6.89030E-03 0.00000E+00 8.01346E-03 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 4.33261E-03 0.00000E+00 5.77543E-03 0.00000E+00 0.00000E+00 7.03469E-02 1.68538E-03 5.14204E-03 2.52079E-03 6.96677E-03 6.93165E-03 1.49274E-02 6.96715E-03 7.46602E-03 5.97098E-03 7.96117E-03 1.07498E-02 1.34154E-02 3.67423E-04 4.65750E-03 5.32589E-03 4.56426E-03 4.43752E-03 1.66154E-03 1.81176E-03 1.87937E-03 1.40625E-03 1.56047E-03 2.24766E-01 1.60937E-02 0.00000E+00 
 ];
 
-i82206_09c_bra_mt = [
- 102 % (n,gamma)
-];
-
-i82206_09c_bra_f = [
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-2.17000E-02 
-];
-
-
 i94239_03c_nu = [
 2.88002 0.00645 
 2.87172 0.00645 
diff --git a/serpentTools/objects/materials.py b/serpentTools/objects/materials.py
index 444fd47..3c9e044 100644
--- a/serpentTools/objects/materials.py
+++ b/serpentTools/objects/materials.py
@@ -65,54 +65,115 @@ class DepletedMaterialBase(NamedObject):
     __doc__ = """
     Base class for storing material data from a depleted material file
 
-    {equiv:s}
+    Many of the instance attributes are shortcuts for accessing data
+    in the underlying dictionary. For example, the following command
+    fetch the same data::
+
+        >>> m.data["adens"]
+        >>> m["adens"]
+        >>> m.adens
+
+    With one exception: the attribute-based accessing is guaranteed to
+    not raise an error if the data has not been loaded into the dictionary.
+    This may be the case if a file was read using filtering settings and
+    ``adens`` was not read.
 
     Parameters
     ----------
-    {params:s}
+    name : str
+        Name of this material
+    metadata : dict
+        Dictionary with file metadata
 
     Attributes
     ----------
-    {attrs:s}
-
-    """.format(equiv=docEquiv, params=docParams, attrs=docAttrs)
+    data : dict
+        Main dictionary of arrays from the output file
+    zai : list of int
+        Isotopic ZZAAAI identifiers like ``922350``. Ordered like
+        the rows of the data arrays
+    names : list of str
+        Isotope names like ``"U235"``. Ordered like the rows of the
+        data arrays
+    days : numpy.ndarray
+        Time in days for each column in the data arrays
+    burnup : numpy.ndarray or None
+        Burnup vector for this specific material
+    volume : numpy.ndarray or None
+        Volume of this material over time
+    adens : numpy.ndarray or None
+        2D array of atom densities in atoms/b-cm
+    mdens : numpy.ndarray or None
+        2D array of mass densites in g/cm3
+    activity : numpy.ndarray or None
+        2D array of activities in Bq
+    decayHeat : numpy.ndarray or None
+        2D array of decay heats in W
+    spontaneousFissionRate : numpy.ndarray or None
+        2D array of spontaneous fission rate in fission per second
+    photonProdRate : numpy.ndarray or None
+        2D array of photon emission rate in photons per second
+    ingTox : numpy.ndarray or None
+        2D array of ingestion toxicity in sieverts
+    inhTox : numpy.ndarray or None
+        2D array of inhalation toxicity in sieverts
+
+    """
 
     def __init__(self, name, metadata):
         NamedObject.__init__(self, name)
         self.data = {}
-        self.__burnup = None
-        self.__mdens = None
-        self.__adens = None
         self.zai = metadata.get('zai', None)
         self.names = metadata.get('names', None)
         self.days = metadata.get('days', None)
 
     def __getitem__(self, item):
-        if item not in self.data:
-            raise KeyError('Key {} not found on material {}'
-                           .format(item, self.name))
+        """Retrieve a value from the data dictionary"""
         return self.data[item]
 
+    def get(self, key, default=None):
+        """Retrive a value from the dictionary, or default if not found"""
+        return self.data.get(key, default)
+
     @property
     def burnup(self):
-        if 'burnup' not in self.data:
-            raise AttributeError('Burnup for material {} has not been loaded'
-                                 .format(self.name))
-        return self.data['burnup']
+        return self.data.get("burnup")
 
     @property
     def adens(self):
-        if 'adens' not in self.data:
-            raise AttributeError('Atomic densities for material {} have not '
-                                 'been loaded'.format(self.name))
-        return self.data['adens']
+        return self.data.get("adens")
 
     @property
     def mdens(self):
-        if 'mdens' not in self.data:
-            raise AttributeError('Mass densities for material {} has not been '
-                                 'loaded'.format(self.name))
-        return self.data['mdens']
+        return self.data.get("mdens")
+
+    @property
+    def volume(self):
+        return self.data.get("volume")
+
+    @property
+    def activity(self):
+        return self.data.get("a")
+
+    @property
+    def ingTox(self):
+        return self.data.get("ingTox")
+
+    @property
+    def inhTox(self):
+        return self.data.get("inhTox")
+
+    @property
+    def decayHeat(self):
+        return self.data.get("h")
+
+    @property
+    def spontaneousFissionRate(self):
+        return self.data.get("sf")
+
+    @property
+    def photonProdRate(self):
+        return self.data.get("gsrc")
 
     def _getIsoID(self, isotopes):
         """Return the row indices that correspond to specfic isotopes."""
@@ -270,7 +331,62 @@ class DepletedMaterialBase(NamedObject):
 
 
 class DepletedMaterial(DepletedMaterialBase):
-    __doc__ = DepletedMaterialBase.__doc__
+    """Base class for storing material data from a depleted material file
+
+    Many of the instance attributes are shortcuts for accessing data
+    in the underlying dictionary. For example, the following command
+    fetch the same data::
+
+        >>> m.data["adens"]
+        >>> m["adens"]
+        >>> m.adens
+
+    With one exception: the attribute-based accessing is guaranteed to
+    not raise an error if the data has not been loaded into the dictionary.
+    This may be the case if a file was read using filtering settings and
+    ``adens`` was not read.
+
+    Parameters
+    ----------
+    name : str
+        Name of this material
+    metadata : dict
+        Dictionary with file metadata
+
+    Attributes
+    ----------
+    data : dict
+        Main dictionary of arrays from the output file
+    zai : list of int
+        Isotopic ZZAAAI identifiers like ``922350``. Ordered like
+        the rows of the data arrays
+    names : list of str
+        Isotope names like ``"U235"``. Ordered like the rows of the
+        data arrays
+    days : numpy.ndarray
+        Time in days for each column in the data arrays
+    burnup : numpy.ndarray or None
+        Burnup vector for this specific material
+    volume : numpy.ndarray or None
+        Volume of this material over time
+    adens : numpy.ndarray or None
+        2D array of atom densities in atoms/b-cm
+    mdens : numpy.ndarray or None
+        2D array of mass densites in g/cm3
+    activity : numpy.ndarray or None
+        2D array of activities in Bq
+    decayHeat : numpy.ndarray or None
+        2D array of decay heats in W
+    spontaneousFissionRate : numpy.ndarray or None
+        2D array of spontaneous fission rate in fission per second
+    photonProdRate : numpy.ndarray or None
+        2D array of photon emission rate in photons per second
+    ingTox : numpy.ndarray or None
+        2D array of ingestion toxicity in sieverts
+    inhTox : numpy.ndarray or None
+        2D array of inhalation toxicity in sieverts
+
+    """
 
     def addData(self, variable, rawData):
         """
diff --git a/serpentTools/parsers/xsplot.py b/serpentTools/parsers/xsplot.py
index f550fb3..6768df4 100644
--- a/serpentTools/parsers/xsplot.py
+++ b/serpentTools/parsers/xsplot.py
@@ -1,8 +1,8 @@
 """Parser responsible for reading the ``*_xsplot.m`` files"""
 from numpy import array, float64
 
-from serpentTools.messages import error, warning
 from serpentTools.engines import KeywordParser
+from serpentTools.messages import warning, info, debug, error
 from serpentTools.parsers.base import BaseReader
 from serpentTools.objects.xsdata import XSData
 
@@ -53,44 +53,51 @@ class XSPlotReader(BaseReader):
 
     def _read(self):
         """Read through the depletion file and store requested data."""
+        info('Preparing to read {}'.format(self.filePath))
         keys = ['E', r'i\d{4,5}', r'm\w']
         separators = ['\n', '];', '\r\n']
 
         with KeywordParser(self.filePath, keys, separators) as parser:
             for chunk in parser.yieldChunks():
 
-                lead = chunk[0].strip()
-                data = chunk[1:]
-                if lead.startswith("E = ["):
+                if chunk[0][:5] == 'E = [':
                     # The energy grid
-                    self.metadata['egrid'] = array(data, dtype=float64)
+                    self.metadata['egrid'] = array(chunk[1:], dtype=float64)
 
-                elif lead.endswith('majorant_xs = ['):
+                elif chunk[0][:15] == 'majorant_xs = [':
                     # L-inf norm on all XS on all materials
-                    self.metadata['majorant_xs'] = array(data,
+                    self.metadata['majorant_xs'] = array(chunk[1:],
                                                          dtype=float64)
 
-                elif lead.endswith('_mt = ['):
-                    xsname = lead[:lead.index("_mt")]
-                    isiso = lead[0] == 'i'
+                elif chunk[0][-7:] == 'mt = [\n':
+                    debug('found mt specification')
+                    xsname = chunk[0][:-8]
+                    isiso = True if chunk[0][0] == 'i' else False
                     self.xsections[xsname] = XSData(xsname, self.metadata,
                                                     isIso=isiso)
                     self.xsections[xsname].setMTs(chunk)
 
-                elif lead.endswith('_xs = ['):
-                    xsname = lead[:lead.index("_xs")]
+                elif chunk[0][-7:] == 'xs = [\n':
+                    debug('found xs specification')
+                    xsname = chunk[0][:-8]
                     self.xsections[xsname].setData(chunk)
 
-                elif lead.endswith('_nu = ['):
-                    xsname = lead[:lead.index("_nu")]
+                elif chunk[0][-7:] == 'nu = [\n':
+                    debug('found nu specification')
+                    xsname = chunk[0][:-8]
                     self.xsections[xsname].setNuData(chunk)
 
-                elif lead.endswith("bra_f = ["):
-                    xsname = lead[:lead.index("_f")]
-                    self.xsections[xsname].setData(chunk)
+                elif 'bra_f' in chunk[0]:
+                    warning("There is this weird 'bra_f' XS. these seem to be"
+                            " constant. recording to metadata instead.")
+                    self.metadata[xsname].setData(chunk)
 
                 else:
-                    raise ValueError("Unidentifiable entry\n{}".format(chunk))
+                    print(chunk)
+                    error('Unidentifiable entry {}'.format(chunk[0]))
+
+        info('Done reading xsplot file')
+        debug('  found {} xs listings'.format(len(self.xsections)))
 
     def _precheck(self):
         """do a quick scan to ensure this looks like a xsplot file."""


Test patch:
diff --git a/tests/test_depletion.py b/tests/test_depletion.py
index 90d9000..89299fa 100644
--- a/tests/test_depletion.py
+++ b/tests/test_depletion.py
@@ -208,6 +208,35 @@ class DepletedMaterialTester(_DepletionTestHelper):
         assert_equal(self.material.adens, expectedAdens)
         assert_equal(self.material['ingTox'], expectedIngTox)
 
+    def test_attributes(self):
+        """Verify the attribute and dictionary based fetching"""
+        # Populate data that is not contained in reference file
+        # to test the attribute-based fetching
+        material = self.material
+        for key in ["mdens", "inhTox", "a", "sf", "gsrc", "h"]:
+            material.data[key] = material.data["adens"]
+        material.data["volume"] = material.data["burnup"]
+
+        for attr in ["burnup", "adens", "mdens", "volume", "ingTox", "inhTox"]:
+            self.assertIs(self.material.data[attr], self.material[attr])
+            self.assertIs(self.material[attr], getattr(self.material, attr))
+            previous = self.material.data.pop(attr)
+            self.assertIs(getattr(self.material, attr), None)
+            self.material.data[attr] = previous
+
+        for key, attr in [
+                ["a", "activity"],
+                ["h", "decayHeat"],
+                ["sf", "spontaneousFissionRate"],
+                ["sf", "spontaneousFissionRate"],
+        ]:
+            self.assertIs(self.material[key], self.material.data[key])
+            self.assertIs(getattr(self.material, attr), self.material.data[key])
+            previous = self.material.data.pop(key)
+            self.assertIs(getattr(self.material, attr), None)
+            self.material.data[key] = previous
+
+
     def test_getValues_burnup_full(self):
         """ Verify that getValues can produce the full burnup vector."""
         actual = self.material.getValues('days', 'burnup', )
diff --git a/tests/test_xsplot.py b/tests/test_xsplot.py
index b87a39c..57b0ac0 100644
--- a/tests/test_xsplot.py
+++ b/tests/test_xsplot.py
@@ -1,6 +1,6 @@
 """Test the xsplot reader."""
 import unittest
-from numpy import ndarray, array, newaxis
+from numpy import ndarray, array
 
 from numpy.testing import assert_array_equal
 
@@ -302,8 +302,8 @@ class XSPlotTester(unittest.TestCase):
             },
             'i8016_03c': {
                 'MT': [
-                    1, 101, 2, 102, 107, 51, 52, 53, 54, 22, 55, 91,
-                    103, 104, 56, 57, 28, 108, 105, 23, 16],
+                    1, 101, 2, 102, 107, 51, 52, 53, 54, 22, 55,
+                    3, 104, 56, 57, 28, 108, 105, 23, 16],
                 'MTdescrip': [
                     ' Total', ' Sum of absorption',
                     ' elastic scattering', ' (n,gamma)',
@@ -314,14 +314,11 @@ class XSPlotTester(unittest.TestCase):
                     ' inelastic scatt. to 4. excited state',
                     ' (n,nalpha)',
                     ' inelastic scatt. to 5. excited state',
-                    ' inelastic scattering to continuum',
-                    ' (n,p)',
+                    ' inelastic scattering to continuum', ' (n,p)',
                     ' (n,d)',
                     ' inelastic scatt. to 6. excited state',
                     ' inelastic scatt. to 7. excited state',
-                    ' (n,np)',
-                    ' (n,2alpha)',
-                    ' (n,t)',
+                    ' (n,np)', ' (n,2alpha)', ' (n,t)',
                     ' (n,n3alpha)',
                     ' (n,2n)',
                 ],
@@ -736,29 +733,6 @@ class XSPlotTester(unittest.TestCase):
                     [2.93887e-01, 1.36424e-01, 1.13642e-04,
                      1.20609e-01, 5.96505e-01, 1.28477e-02]]),
             },
-            "i82206_09c_bra": {
-                "MT": [102],
-                "MTdescrip": [" (n,gamma)"],
-                "hasNuData": False,
-                "isIso": True,
-                'metadata': {
-                    'egrid': array(
-                        [1.00000e-08, 1.03891e-07, 1.07934e-06,
-                         1.12135e-05, 1.16498e-04, 1.21032e-03,
-                         1.25742e-02, 1.30635e-01, 1.35719e+00,
-                         1.41000e+01]),
-                    'majorant_xs': array(
-                        [78.4253, 36.1666, 2.54417, 13.0654,
-                         4.27811, 0.822536, 0.781066, 0.598564,
-                         0.34175, 0.293887]),
-                },
-                "name": "i82206_09c_bra",
-                "xsdata": array([
-                    2.17000E-02, 2.17000E-02, 2.17000E-02, 2.17000E-02,
-                    2.17000E-02, 2.17000E-02, 2.17000E-02, 2.17000E-02,
-                    2.17000E-02, 2.17000E-02]
-                )[:, newaxis],  # Promote to 2D
-            },
         }
 
         # newly obtained results: