Repository: CORE-GATECH-GROUP/serpent-tools

Issue:
BUG DetectorReader misses detectors that start with and contain identical names
## Summary of issue
A colleague has a detector file with the following energy dependent detectors: `spectrum`, `spectrumA`, and `spectrumB`. The DetectorReader fails to capture the later two detectors, and messes up the data stored on `spectrum`.

## Code for reproducing the issue
Process a detector file with the above names. One has been created for reproducibility using the `bwr_det0.m` file and the following commands
```sh
$ sed "s/spectrum/spectrumA/" bwr_det0.m > a
$ sed "s/spectrum/spectrumB/" bwr_det0.m > b
$ cat bwr_det0.m a b > joined_det0.m
```
Process with python
```python
>>> import serpentTools
>>> det = serpentTools.read("joined_det0.m")
```
## Actual outcome including console output and error traceback if applicable
```python
>>> det.detectors
{'spectrum' : <serpentTools.detector.Detector object at ...>}
>>> det["spectrum"].grids.keys()
dict_keys(["E", "A", "AE", "B", "BE"])
```
## Expected outcome
All three detectors are properly defined.

## Versions

Please provide the following:

* Version  from ``serpentTools.__version__`` `0.9.1`
* Python version - ``python --version`` - `3.7`

## Likely culprit
https://github.com/CORE-GATECH-GROUP/serpent-tools/blob/2dc5a9d03bc510d358032f270575f0ab3120584f/serpentTools/parsers/detector.py#L63

# Workaround

If the file has already been produced, then users can change the detector names to be more distinct, e.g.
```sh
$ sed -e "s/spectrumA/Aspectrum/" -e "s/spectrumB/Bspectrum/" joined_det0.m > mod_det0.m
```
then serpentTools reveals that `spectrum`, `Aspectrum`, and `Bspectrum` detectors have been found.

If the detector names must be present in the file, users can put the longer names first, or break up the ordering with other detectors. This will "confuse" the matching. Putting `spectrum` last yields the correct detectors.


Discussion:

Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index e651937..3862113 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -6,6 +6,18 @@
 Changelog
 =========
 
+.. _v0.9.2:
+
+0.9.2
+=====
+
+.. _v0.9.2-bugs:
+
+Bug Fixes
+---------
+
+* Detector reader can handle sequential detectors with very similar
+  names - :issue:`374`.
 
 .. _v0.9.1:
 
@@ -21,7 +33,6 @@ Bug Fixes
   overwrite the primary result arrays - :pull:`366`. These arrays are not 
   currently stored - :issue:`367`
 
-
 .. _v0.9.0:
 
 :release-tag:`0.9.0`
diff --git a/serpentTools/parsers/detector.py b/serpentTools/parsers/detector.py
index 52f6f43..c5a28c4 100644
--- a/serpentTools/parsers/detector.py
+++ b/serpentTools/parsers/detector.py
@@ -30,6 +30,9 @@ class DetectorReader(BaseReader):
         instances
     """
 
+    # Update this if new detector grids are introduced
+    _KNOWN_GRIDS = ("E", "X", "Y", "Z", "T", "COORD", "R", "PHI", "THETA")
+
     def __init__(self, filePath):
         BaseReader.__init__(self, filePath, 'detector')
         self.detectors = {}
@@ -60,7 +63,18 @@ class DetectorReader(BaseReader):
         with KeywordParser(self.filePath, ["DET"], ["\n", "];"]) as parser:
             for chunk in parser.yieldChunks():
                 name, data = cleanDetChunk(chunk)
-                if currentName and name[:len(currentName)] != currentName:
+
+                # Determine if this is a new detector
+                if not currentName:
+                    isNewDetector = False
+                elif not name.startswith(currentName):
+                    isNewDetector = True
+                else:
+                    isNewDetector = not any(
+                        name == "".join((currentName, g))
+                        for g in self._KNOWN_GRIDS)
+
+                if isNewDetector:
                     self._processDet(currentName, bins, grids)
                     bins = data
                     grids = {}


Test patch:
diff --git a/tests/test_detectors.py b/tests/test_detectors.py
index f60aaeb..0462578 100644
--- a/tests/test_detectors.py
+++ b/tests/test_detectors.py
@@ -4,8 +4,10 @@ Test various aspects of the detectors classes
 
 import numpy
 import pytest
+import serpentTools
 from serpentTools import detectors
 
+
 @pytest.fixture(scope="module")
 def meshedBinData():
     bins = numpy.ones((25, 12), order="f")
@@ -46,6 +48,7 @@ def testDetectorProperties(meshedBinData):
     with pytest.raises(ValueError, match="indexes"):
         detector.indexes = detector.indexes[:1]
 
+
 @pytest.mark.parametrize("how", ["bins", "grids", "bare", "init"])
 def testCartesianDetector(meshedBinData, how):
 
@@ -115,6 +118,7 @@ def testCartesianDetector(meshedBinData, how):
     with pytest.raises(ValueError, match="indexes"):
         detector.indexes = detector.indexes[:1]
 
+
 @pytest.mark.parametrize("how", ["grids", "bins", "bare", "init"])
 def testHexagonalDetector(meshedBinData, how):
     centers = numpy.array([
@@ -221,6 +225,7 @@ def binsWithScores():
     bins[0, -3] = 1.5
     return bins
 
+
 def testNoScores(binsWithScores):
 
     with pytest.raises(ValueError, match=".*scores"):
@@ -231,3 +236,41 @@ def testNoScores(binsWithScores):
 
     with pytest.raises(ValueError, match=".*scores"):
         detectors.Detector.fromTallyBins("scored", bins=binsWithScores)
+
+
+@pytest.fixture
+def similarDetectorFile(tmp_path):
+    detFile = tmp_path / "similar_det0.m"
+    detFile.absolute()
+
+    with detFile.open("w") as stream:
+        stream.write("""DETspectrum = [
+    1    1    1    1    1    1    1    1    1    1  8.19312E+17 0.05187
+];
+
+DETspectrumA = [
+    1    1    1    1    1    1    1    1    1    1  8.19312E+17 0.05187
+];
+
+DETspectrumAE = [
+ 0.00000E-11  4.13994E-07  2.07002E-07
+];
+
+DETspectrumACOORD = [
+ 0.00000E-11  4.13994E-07
+];
+
+DETspectrumB = [
+    1    1    1    1    1    1    1    1    1    1  8.19312E+17 0.05187
+];""")
+
+    yield str(detFile)
+
+    detFile.unlink()
+
+
+def test_similarDetectors(similarDetectorFile):
+    reader = serpentTools.read(similarDetectorFile)
+
+    assert set(reader.detectors) == {"spectrum", "spectrumA", "spectrumB"}
+    assert isinstance(reader["spectrumA"], detectors.HexagonalDetector)