Repository: idaholab/MontePy

Issue:
MontePy Cell Parser gets confused by implicit Union operator of two parenthetical groups
**Describe the bug**
`Number conflict error: There was a numbering conflict when attempting to add CELL: -2000`

However, it is clear that -2000 is actually a surface in my cell definition albeit, it is the first surface after the continuation indent. Thus, it appears MontePy assumes cell definitions occupy a single line.  


Discussion:

Patch:
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index eaff2dbe..00fa661a 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -1,6 +1,14 @@
 MontePy Changelog
 =================
 
+#Next Version#
+----------------
+
+**Bug fixes**
+
+* Fixed parser bug with parsing cells with implicit intersection, e.g., ``(1:-2)(3:-4)``. (:issue:`355`).
+
+
 0.3.0
 -------------------
 
diff --git a/montepy/input_parser/cell_parser.py b/montepy/input_parser/cell_parser.py
index 8585a649..a519fe60 100644
--- a/montepy/input_parser/cell_parser.py
+++ b/montepy/input_parser/cell_parser.py
@@ -85,6 +85,7 @@ class CellParser(MCNP_Parser):
             term = syntax_node.GeometryTree("shift", {"left": term}, ">", term)
         return term
 
+    # handle intersection
     @_("geometry_term padding geometry_factor")
     def geometry_term(self, p):
         left = p.geometry_term
@@ -92,6 +93,14 @@ class CellParser(MCNP_Parser):
         nodes = {"left": left, "operator": p.padding, "right": right}
         return syntax_node.GeometryTree("intersection", nodes, "*", left, right)
 
+    # handle implicit intersection of: ( )( )
+    @_("geometry_term geometry_factory")
+    def geometry_term(self, p):
+        left = p.geometry_term
+        right = p.geometry_factory
+        nodes = {"left": left, "operator": syntax_node.PaddingNode(), "right": right}
+        return syntax_node.GeometryTree("intersection", nodes, "*", left, right)
+
     @_(
         "geometry_term NUM_REPEAT",
         "geometry_term REPEAT",


Test patch:
diff --git a/tests/test_cell_problem.py b/tests/test_cell_problem.py
index 2c352c98..6c6d1098 100644
--- a/tests/test_cell_problem.py
+++ b/tests/test_cell_problem.py
@@ -1,5 +1,6 @@
 # Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
 from unittest import TestCase
+import pytest
 
 import montepy
 from montepy.cell import Cell
@@ -12,56 +13,6 @@ class TestCellClass(TestCase):
         with self.assertRaises(TypeError):
             Cell("5")
 
-    def test_init(self):
-        # test invalid cell number
-        in_str = "foo"
-        card = Input([in_str, f"c {in_str}"], BlockType.CELL)
-        with self.assertRaises(montepy.errors.MalformedInputError):
-            in_str = "foo bar"
-            cell = Cell(card)
-        # test like feature unsupported
-        in_str = "1 like 2"
-        card = Input([in_str, f"c {in_str}"], BlockType.CELL)
-        with self.assertRaises(montepy.errors.UnsupportedFeature):
-            in_str = "foo bar"
-            cell = Cell(card)
-        # test invalid material number
-        in_str = "1 foo"
-        card = Input([in_str], BlockType.CELL)
-        with self.assertRaises(montepy.errors.MalformedInputError):
-            cell = Cell(card)
-        # test invalid material density
-        in_str = "1 1 foo"
-        card = Input([in_str], BlockType.CELL)
-        with self.assertRaises(montepy.errors.MalformedInputError):
-            cell = Cell(card)
-        # tests void cell
-        in_str = "1 0 2"
-        card = Input([in_str], BlockType.CELL)
-        cell = Cell(card)
-        self.assertEqual(cell.old_number, 1)
-        self.assertEqual(cell.number, 1)
-        self.assertIsNone(cell.material)
-        self.assertEqual(cell.old_mat_number, 0)
-
-        # test material cell
-        for atom_dens, density in [(False, "-0.5"), (True, "0.5")]:
-            in_str = f"1 1 {density} 2"
-            card = Input([in_str], BlockType.CELL)
-            cell = Cell(card)
-            self.assertEqual(cell.old_mat_number, 1)
-            if atom_dens:
-                self.assertAlmostEqual(cell.atom_density, 0.5)
-            else:
-                self.assertAlmostEqual(cell.mass_density, 0.5)
-            self.assertTrue(atom_dens == cell.is_atom_dens)
-
-        # test parameter input
-        in_str = "1 0 #2 u= 5 vol=20 trcl=5"
-        card = Input([in_str], BlockType.CELL)
-        cell = Cell(card)
-        self.assertEqual(cell.parameters["TRCL"]["data"][0].value, 5.0)
-
     # TODO test updating cell geometry once done
     def test_cell_validator(self):
         cell = Cell()
@@ -158,3 +109,65 @@ class TestCellClass(TestCase):
         card = Input([in_str], BlockType.CELL)
         cell = Cell(card)
         self.assertEqual(cell.parameters["PWT"]["data"][0].value, 1.0)
+
+
+@pytest.mark.parametrize(
+    "line, is_void, mat_number, density, atom_dens, parameters",
+    [
+        ("1 0 2", True, 0, None, None, None),
+        ("1 1 -10 2", False, 1, 10.0, False, None),
+        ("1 1 10 2", False, 1, 10.0, True, None),
+        (
+            "1 0 #2 u= 5 vol=20 trcl=5",
+            True,
+            0,
+            None,
+            None,
+            {"TRCL": 5.0, "U": 5.0, "VOL": 20.0},
+        ),
+        (
+            "1 1 -1.0 1000 (-3000:-4000:-5000)(-6000:-7000:8000) -2000 -9000",
+            False,
+            1,
+            1,
+            False,
+            None,
+        ),
+    ],
+)
+def test_init(line, is_void, mat_number, density, atom_dens, parameters):
+    # test like feature unsupported
+    # tests void cell
+    input = Input([line], BlockType.CELL)
+    cell = Cell(input)
+    assert cell.old_number == 1
+    assert cell.number == 1
+    if is_void:
+        assert cell.material is None
+    else:
+        assert cell.old_mat_number != 0
+        if atom_dens:
+            assert cell.is_atom_dens
+            assert cell.atom_density == pytest.approx(density)
+        else:
+            assert not cell.is_atom_dens
+            assert cell.mass_density == pytest.approx(density)
+    assert cell.old_mat_number == mat_number
+    if not parameters:
+        return
+    for parameter, value in parameters.items():
+        assert cell.parameters[parameter]["data"][0].value == pytest.approx(value)
+
+
+@pytest.mark.parametrize("line", ["foo", "foo bar", "1 foo", "1 1 foo"])
+def test_malformed_init(line):
+    with pytest.raises(montepy.errors.MalformedInputError):
+        input = Input([line], BlockType.CELL)
+        Cell(input)
+
+
+@pytest.mark.parametrize("line", ["1 like 2"])
+def test_malformed_init(line):
+    with pytest.raises(montepy.errors.UnsupportedFeature):
+        input = Input([line], BlockType.CELL)
+        Cell(input)
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index ef6ac566..32375dd4 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -5,6 +5,7 @@ import montepy
 from montepy.errors import *
 import os
 
+import pytest
 from unittest import TestCase
 
 
@@ -87,58 +88,6 @@ class EdgeCaseTests(TestCase):
         self.assertEqual(len(cell.parameters), 5)
         self.assertEqual(cell.parameters.nodes.keys(), allowed_keys)
 
-    def test_confused_union_geometry(self):
-        # based on issue #122
-        in_strs = [
-            "9800     10    -0.123000 +101 -200 -905 +213 (-216:+217)",
-        ]
-        input = montepy.input_parser.mcnp_input.Input(
-            in_strs, montepy.input_parser.block_type.BlockType.CELL
-        )
-        cell = montepy.Cell(input)
-
-    def test_confused_trcl(self):
-        for line in [
-            "340 0 (94 -209 -340) fill=2 trcl=(0 0 0  0.7071 -0.7071 0  0.7071 0.7071 0  0 0 1)",
-            "340 0 (94 -209 -340) fill=2 trcl=(0 0 0  0.7071 -0.7071 0  0.7071 0.7071 0  0 0 1)  $ Comment",
-        ]:
-            input = montepy.input_parser.mcnp_input.Input(
-                [line],
-                montepy.input_parser.block_type.BlockType.CELL,
-            )
-            cell = montepy.Cell(input)
-
-    def test_space_after_equals(self):
-        lines = [
-            "35000 3690   0.01000    35100   -35105",
-            "                        35110   -35115",
-            "                        35150   -35155   U = 35100",
-        ]
-        input = montepy.input_parser.mcnp_input.Input(
-            lines,
-            montepy.input_parser.block_type.BlockType.CELL,
-        )
-        cell = montepy.Cell(input)
-
-    def test_interpolate_geometry(self):
-        lines = [
-            "10234   30  1.2456780      -3103  3104 -3133  3136",
-            "                            3201  15i   3217",
-            "                            u=20",
-        ]
-        input = montepy.input_parser.mcnp_input.Input(
-            lines,
-            montepy.input_parser.block_type.BlockType.CELL,
-        )
-        cell = montepy.Cell(input)
-
-    def test_material_float(self):
-        in_strs = ["m171 1001.80c 1E-24"]
-        input = montepy.input_parser.mcnp_input.Input(
-            in_strs, montepy.input_parser.block_type.BlockType.CELL
-        )
-        material = montepy.data_inputs.material.Material(input)
-
     def test_void_cell_set_material(self):
         in_strs = ["2 0 -63 -62 64  imp:n=1 $ COBALT_TARGET_PELLET2"]
         input = montepy.input_parser.mcnp_input.Input(
@@ -182,3 +131,43 @@ class EdgeCaseTests(TestCase):
         problem = montepy.read_input(
             os.path.join("tests", "inputs", "readEdgeCase.imcnp")
         )
+
+    def test_material_float(self):
+        in_strs = ["m171 1001.80c 1E-24"]
+        input = montepy.input_parser.mcnp_input.Input(
+            in_strs, montepy.input_parser.block_type.BlockType.CELL
+        )
+        material = montepy.data_inputs.material.Material(input)
+
+
+@pytest.mark.parametrize(
+    "lines",
+    [
+        # issue #122
+        [
+            "9800     10    -0.123000 +101 -200 -905 +213 (-216:+217)",
+        ],
+        [
+            "340 0 (94 -209 -340) fill=2 trcl=(0 0 0  0.7071 -0.7071 0  0.7071 0.7071 0  0 0 1)",
+        ],
+        [
+            "340 0 (94 -209 -340) fill=2 trcl=(0 0 0  0.7071 -0.7071 0  0.7071 0.7071 0  0 0 1)  $ Comment",
+        ],
+        [
+            "35000 3690   0.01000    35100   -35105",
+            "                        35110   -35115",
+            "                        35150   -35155   U = 35100",
+        ],
+        [
+            "10234   30  1.2456780      -3103  3104 -3133  3136",
+            "                            3201  15i   3217",
+            "                            u=20",
+        ],
+    ],
+)
+def test_complex_cell_parsing(lines):
+    input = montepy.input_parser.mcnp_input.Input(
+        lines,
+        montepy.input_parser.block_type.BlockType.CELL,
+    )
+    montepy.Cell(input)