Repository: CORE-GATECH-GROUP/serpent-tools

Issue:
BUG ResultsReader drops last digit of scalar data
## Summary of issue
When reading a result file, we expect scalar data to have a space between the last digit and the colon due to
https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/dd886ff2b2cd19e8dc681245134cef8815d61f31/serpentTools/utils/core.py#L14 and https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/dd886ff2b2cd19e8dc681245134cef8815d61f31/serpentTools/parsers/results.py#L280-L282

The `- 2` was done to remove the colon and the trailing space. However, there are some values that don't have this trailing space
https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/a69471eb211a18768aa55c695be906e006b4b4a6/data/pwr_res.m#L110-L118

## Code for reproducing the issue
```python
>>> from serpentTools.data import readDataFile
>>> res = readDataFile("pwr_res.m")  # read the above data file
>>> res.resdata["miscMemsize"]
array([[6.5], [6.5]])
```

## Actual outcome including console output and error traceback if applicable
See above
## Expected outcome
For this value, we should see `miscMemsize` give us `[[6.59], [6.59]]`

## Versions

Please provide the following:

* Version  from ``serpentTools.__version__`` `0.10.0.dev0` and `0.9.3`
* Python version - ``python --version`` `3.8`

## Potential fix
Improving the flexibility of the regular expressions. Possibly even using a match group to present the string / scalar / vector data in `match.groups()` rather than relying on `match.span`


Discussion:

Patch:
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index aa730d8..6e5e05a 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -262,7 +262,7 @@ class DepletionReader(DepPlotMixin, MaterialReader):
                 if varName == 'NAMES':
                     values = [item[1:item.find(" ")] for item in cleaned]
                 else:
-                    values = str2vec(cleaned, int, list)
+                    values = [int(v) for v in cleaned]
             else:
                 line = self._cleanSingleLine(chunk)
                 values = str2vec(line)
diff --git a/serpentTools/parsers/microxs.py b/serpentTools/parsers/microxs.py
index 039480f..0002e43 100644
--- a/serpentTools/parsers/microxs.py
+++ b/serpentTools/parsers/microxs.py
@@ -128,15 +128,15 @@ class MicroXSReader(BaseReader):
         if 'E' in currVar.split('_')[-1]:  # e.g., NFY_902270_1E
             sclVal = SCALAR_REGEX.search(chunk[0])
             # energy must be stored on the reader
-            self._energyFY = float(str2vec(
-                chunk[0][sclVal.span()[0] + 1:sclVal.span()[1] - 2]))
+            self._energyFY = float(sclVal.groups()[1])
             return  # thermal/epi/fast
         for tline in chunk:
             if '[' in tline or ']' in tline:
                 continue
             tline = tline[:tline.find('%')]
-            if len(tline.split()) == 3:
-                val1, val2, val3 = str2vec(tline, out=list)
+            items = tline.split()
+            if len(items) == 3:
+                val1, val2, val3 = (float(i) for i in items)
                 fissProd.append(val1)
                 indYield.append(val2)
                 cumYield.append(val3)
@@ -151,7 +151,7 @@ class MicroXSReader(BaseReader):
         # obtain the universe id
         univ = currVar.split('_')[-1]
         search = VEC_REGEX.search(chunk0)  # group flux values
-        vals = str2vec(chunk0[search.span()[0] + 1:search.span()[1] - 2])
+        vals = str2vec(search.groups()[1])
         self.fluxRatio[univ], self.fluxUnc[univ] = splitValsUncs(vals)
 
     def _storeMicroXS(self, chunk):
diff --git a/serpentTools/parsers/results.py b/serpentTools/parsers/results.py
index f0444a0..557e2b5 100644
--- a/serpentTools/parsers/results.py
+++ b/serpentTools/parsers/results.py
@@ -133,8 +133,8 @@ def varTypeFactory(key):
             return lambda x: (typeFunc(x), None)
     # Perform array conversion, return expected values and uncertainties
     if UNIV_K_RE.search(key) is not None:
-        return lambda x: str2vec(x, out=tuple)
-    return lambda x: splitValsUncs(x)
+        return lambda s: tuple(float(i) for i in s.split())
+    return splitValsUncs
 
 
 __all__ = ['ResultsReader', ]
@@ -427,13 +427,13 @@ class ResultsReader(XSReader):
         varType, varVals = [], []
         if strMatch is not None:
             varType = 'string'
-            varVals = tline[strMatch.span()[0] + 1:strMatch.span()[1] - 1]
+            varVals = strMatch.groups()[1]
         elif vecMatch is not None:
             varType = 'vector'
-            varVals = tline[vecMatch.span()[0] + 1:vecMatch.span()[1] - 2]
+            varVals = vecMatch.groups()[1].strip()
         elif sclMatch is not None:
             varType = 'scalar'
-            varVals = tline[sclMatch.span()[0] + 1:sclMatch.span()[1] - 2]
+            varVals = sclMatch.groups()[1]
         return varType, varVals
 
     def getUniv(self, univ, burnup=None, index=None, timeDays=None):
diff --git a/serpentTools/parsers/sensitivity.py b/serpentTools/parsers/sensitivity.py
index 98a249c..ef1c253 100644
--- a/serpentTools/parsers/sensitivity.py
+++ b/serpentTools/parsers/sensitivity.py
@@ -4,7 +4,7 @@ Class to read Sensitivity file
 from collections import OrderedDict
 from itertools import product
 
-from numpy import transpose, hstack
+from numpy import transpose, hstack, array
 from matplotlib.pyplot import gca, axvline
 
 from serpentTools.utils.plot import magicPlotDocDecorator, formatPlot
@@ -219,7 +219,7 @@ class SensitivityReader(BaseReader):
                                         "in energy chunk {}".format(chunk[:3]))
         splitLine = line.split()
         varName = splitLine[0].split('_')[1:]
-        varValues = str2vec(splitLine[3:-1])
+        varValues = array(splitLine[3:-1], dtype=float)
         if varName[0] == 'E':
             self.energies = varValues
         elif varName == ['LETHARGY', 'WIDTHS']:
diff --git a/serpentTools/utils/core.py b/serpentTools/utils/core.py
index c1b35ad..8460b51 100644
--- a/serpentTools/utils/core.py
+++ b/serpentTools/utils/core.py
@@ -9,68 +9,43 @@ from numpy import array, ndarray, fromiter
 
 # Regular expressions
 
-STR_REGEX = compile(r'\'.+\'')  # string
-VEC_REGEX = compile(r'(?<==.)\[.+?\]')  # vector
-SCALAR_REGEX = compile(r'=.+;')  # scalar
+STR_REGEX = compile(r"([A-Z_]+).*'(.*)'")
+VEC_REGEX = compile(r"([A-Z_]+).*=\s+\[\s*([0-9-\+\.Ee ]+)\]")
+SCALAR_REGEX = compile(r"([A-Z_]+).*=\s+([0-9-\+Ee\.]+)")
 FIRST_WORD_REGEX = compile(r'^\w+')  # first word in the line
 
 
-def str2vec(iterable, dtype=float, out=array):
+def str2vec(s, dtype=float):
     """
-    Convert a string or other iterable to vector.
+    Convert a string to a numpy array
 
     Parameters
     ----------
-    iterable: str or iterable
+    s : str
         If a string containing spaces, will be split using
         ```iterable.split()``. If no spaces are found, the
         outgoing type is filled with a single string, e.g.
         a list with a single string as the first and only
         entry. This is returned directly, avoiding conversion
-        with ``dtype``.
-        Every item in this split list, or original
-        iterable, will be iterated over and converted accoring
-        to the other arguments.
-    dtype: type
+        with ``dtype``. Every item in this split list will be iterated
+        over and converted according to the other arguments.
+    dtype : type
         Convert each value in ``iterable`` to this data type.
-    out: type
-        Return data type. Will be passed the iterable of
-        converted items of data dtype ``dtype``.
 
     Returns
     -------
-    vector
-        Iterable of all values of ``iterable``, or split variant,
-        converted to type ``dtype``.
+    numpy.ndarray
+        Vector of all the elements converted to ``dtype``
 
     Examples
     --------
-    ::
-
-        >>> v = "1 2 3 4"
-        >>> str2vec(v)
-        array([1., 2., 3., 4.,])
-
-        >>> str2vec(v, int, list)
-        [1, 2, 3, 4]
-
-        >>> x = [1, 2, 3, 4]
-        >>> str2vec(x)
-        array([1., 2., 3., 4.,])
 
-        >>> str2vec("ADF")
-        array(['ADF', dtype='<U3')
+    >>> v = "1 2 3 4"
+    >>> str2vec(v)
+    array([1., 2., 3., 4.,])
 
     """
-    if isinstance(iterable, str):
-        if ' ' in iterable:
-            iterable = iterable.split()
-        else:
-            return out([iterable])
-    cmap = map(dtype, iterable)
-    if out is array:
-        return fromiter(cmap, dtype)
-    return out(cmap)
+    return array(s.split(), dtype=dtype)
 
 
 def splitValsUncs(iterable, copy=False):


Test patch:
diff --git a/tests/test_pt_results.py b/tests/test_pt_results.py
new file mode 100644
index 0000000..5b4fab3
--- /dev/null
+++ b/tests/test_pt_results.py
@@ -0,0 +1,28 @@
+"""pytest tests for reading the result file"""
+
+import pytest
+import serpentTools.data
+from serpentTools.settings import rc
+
+
+@pytest.fixture(scope="module")
+def read_2_1_29():
+    with rc:
+        rc["serpentVersion"] = "2.1.29"
+        yield
+
+
+@pytest.fixture
+def fullPwrFile(read_2_1_29):
+    return serpentTools.data.readDataFile("pwr_res.m")
+
+
+def test_scalars(fullPwrFile):
+    """Ensure that the full precision of scalars are captured
+
+    Related: GH issue #411
+    """
+    assert fullPwrFile["miscMemsize"] == pytest.approx([6.59, 6.59], abs=0, rel=0)
+    assert fullPwrFile["availMem"] == pytest.approx([15935.20, 15935.20], abs=0, rel=0)
+    assert fullPwrFile["totCpuTime"] == pytest.approx([0.282078, 0.494889], abs=0, rel=0)
+
diff --git a/tests/test_pt_utils.py b/tests/test_pt_utils.py
new file mode 100644
index 0000000..e16cc27
--- /dev/null
+++ b/tests/test_pt_utils.py
@@ -0,0 +1,15 @@
+"""Tests for using utility features"""
+
+import pytest
+from numpy import random
+
+import serpentTools.utils as sutils
+
+
+@pytest.mark.parametrize("N", [1, 5, 10])
+def test_str2vec(N):
+    """Compare the string to vector conversion on a random vector"""
+    r = random.random(N)
+    s = " ".join(map(str, r))
+    v = sutils.str2vec(s)
+    assert v == pytest.approx(r, abs=0, rel=0)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 56a2837..1f0c50f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -44,48 +44,6 @@ class VariableConverterTester(TestCase):
             self.assertEqual(expected, actual, msg=serpentStyle)
 
 
-class VectorConverterTester(TestCase):
-    """Class for testing the str2vec function"""
-
-    @classmethod
-    def setUpClass(cls):
-        cls.testCases = ("0 1 2 3", [0, 1, 2, 3], (0, 1, 2, 3), arange(4))
-
-    def test_str2Arrays(self):
-        """Verify that the str2vec converts to arrays."""
-        expected = arange(4)
-        for case in self.testCases:
-            actual = str2vec(case)
-            assert_array_equal(expected, actual, err_msg=case)
-
-    def test_listOfInts(self):
-        """Verify that a list of ints can be produced with str2vec."""
-        expected = [0, 1, 2, 3]
-        self._runConversionTest(int, expected, list)
-
-    def test_vecOfStr(self):
-        """Verify a single word can be converted with str2vec"""
-        key = 'ADF'
-        expected = array('ADF')
-        actual = str2vec(key)
-        assert_array_equal(expected, actual)
-
-    def _runConversionTest(self, valType, expected, outType=None):
-        if outType is None:
-            outType = array
-            compareType = ndarray
-        else:
-            compareType = outType
-        for case in self.testCases:
-            actual = str2vec(case, dtype=valType, out=outType)
-            self.assertIsInstance(actual, compareType, msg=case)
-            ofRightType = [isinstance(xx, valType) for xx in actual]
-            self.assertTrue(all(ofRightType),
-                            msg="{} -> {}, {}".format(case, actual,
-                                                      type(actual)))
-            self.assertEqual(expected, actual, msg=case)
-
-
 class SplitValsTester(TestCase):
     """Class that tests splitValsUncs."""