Repository: openmc-dev/openmc

Issue:
Hexagonal Lattices Don't Roundtrip
## Bug Description

Our hexagonal lattices don't currently roundtrip from XML correctly for multiple axial layers. The first layer is fine, but layers beyond that are incorrect.

```
  <hex_lattice id="100" n_axial="2" n_rings="	  <hex_lattice id="100" n_axial="2" n_rings="
    <pitch>1.0 1.0</pitch>			    <pitch>1.0 1.0</pitch>
    <center>0.0 0.0 0.0</center>		    <center>0.0 0.0 0.0</center>
    <universes>					    <universes>
    16  17  18					    16  17  18
  15  6   7   19				  15  6   7   19
14  5   1   2   8 				14  5   1   2   8 
  13  4   3   9 				  13  4   3   9 
    12  11  10					    12  11  10
    10  16  17				      |	    16  17  18
  18  15  6   7 			      |	  15  6   7   19
19  14  5   1   2 			      |	14  5   1   2   8 
  8   13  4   3 			      |	  13  4   3   9 
    9   12  11</universes>		      |	    12  11  10</universes>
  </hex_lattice>				  </hex_lattice>
</geometry>					</geometry>
````

<!--A clear and concise description of the problem (Note: A missing feature is not a bug).-->


## Steps to Reproduce

```python
# coding: utf-8
import openmc

openmc.reset_auto_ids()

latt = openmc.HexLattice(lattice_id=100)
latt.pitch = (1.0, 1.0)
latt.center = (0.0, 0.0, 0.0)

u19 = [openmc.Universe(cells=[openmc.Cell()]) for i in range(19)]
axial_one = [u19[-12:], u19[1:7], u19[0:1]]
latt.orientation = 'x'

u19 = [openmc.Universe(cells=[openmc.Cell()]) for i in range(19)]
axial_two = [u19[-12:], u19[1:7], u19[0:1]]

latt.universes = [axial_one, axial_one]

outer_univ = openmc.Universe(cells=[openmc.Cell(fill=latt)])
geom = openmc.Geometry(root=outer_univ)
geom.export_to_xml()

geom2 = openmc.Geometry.from_xml(materials=openmc.Materials())

print("Original Lattice:")
print(latt)
print("Roundtrip Lattice:")
print(geom2.get_all_lattices()[latt.id])

rt_latt = geom2.get_all_lattices()[latt.id]

for aixal_og, axial_rt in zip(latt.universes, rt_latt.universes):
    for ring_og, ring_rt in zip(aixal_og, axial_rt):
        assert [u.id for u in ring_og] == [u.id for u in ring_rt]
```


Discussion:

Resolving patch:
diff --git a/openmc/lattice.py b/openmc/lattice.py
--- a/openmc/lattice.py
+++ b/openmc/lattice.py
@@ -1564,6 +1564,7 @@ def from_xml_element(cls, elem, get_universe):
                             alpha -= 1
                             if not lat.is_valid_index((x, alpha, z)):
                                 # Reached the bottom
+                                j += 1
                                 break
                     j += 1
             else:
@@ -1583,6 +1584,7 @@ def from_xml_element(cls, elem, get_universe):
 
                         # Check if we've reached the bottom
                         if y == -n_rings:
+                            j += 1
                             break
 
                         while not lat.is_valid_index((alpha, y, z)):


Test patch:
diff --git a/tests/unit_tests/test_lattice.py b/tests/unit_tests/test_lattice.py
--- a/tests/unit_tests/test_lattice.py
+++ b/tests/unit_tests/test_lattice.py
@@ -377,3 +377,46 @@ def test_unset_universes():
     hex_lattice.pitch = (1.,)
     with pytest.raises(ValueError):
         hex_lattice.create_xml_subelement(elem)
+
+
+@pytest.mark.parametrize("orientation", ['x', 'y'])
+def test_hex_lattice_roundtrip(orientation):
+    openmc.reset_auto_ids()
+
+    # ensure that the lattice universes are the same on all axial levels
+    def check_lattice_universes(og_lattice, xml_lattice):
+        for axial_og, axial_rt in zip(og_lattice.universes, xml_lattice.universes):
+            for ring_og, ring_rt in zip(axial_og, axial_rt):
+                assert [u.id for u in ring_og] == [u.id for u in ring_rt]
+
+    latt = openmc.HexLattice()
+    latt.pitch = (1.0, 1.0)
+    latt.center = (0.0, 0.0, 0.0)
+    latt.orientation = orientation
+
+    # fill the lattice with universes in increasing order and repeat for
+    # the second actial level
+    lvl_one_univs = [openmc.Universe(cells=[openmc.Cell()]) for _ in range(19)]
+    lvl_one_univs = [lvl_one_univs[-12:], lvl_one_univs[1:7], lvl_one_univs[:1]]
+    latt.universes = [lvl_one_univs, lvl_one_univs]
+
+    geom = openmc.Geometry([openmc.Cell(fill=latt)])
+    geom.export_to_xml()
+
+    xml_geom = openmc.Geometry.from_xml(materials=openmc.Materials())
+
+    xml_latt = xml_geom.get_all_lattices()[latt.id]
+
+    check_lattice_universes(latt, xml_latt)
+
+    # same test but with unique universes for each axial level
+    lvl_two_univs = [openmc.Universe(cells=[openmc.Cell()]) for _ in range(19)]
+    lvl_two_univs = [lvl_two_univs[-12:], lvl_two_univs[1:7], lvl_two_univs[:1]]
+    latt.universes = [lvl_one_univs, lvl_two_univs]
+
+    geom.export_to_xml()
+
+    xml_geom = openmc.Geometry.from_xml(materials=openmc.Materials())
+    xml_latt = xml_geom.get_all_lattices()[latt.id]
+
+    check_lattice_universes(latt, xml_latt)