Repository: openmc-dev/openmc

Issue:
feature request: RegularMesh from universe class method
I find myself making RegularMesh that encompass the entire geometry quite often. When making weight windows, flux maps or dose maps.

We have ```Universe.bounding_box``` property and I was wondering if anyone else would like a new class method on the ```RegularMesh``` class so that regular meshes can be made from a universe.

This would automatically set ```lower_left``` and the ```upper_right``` using the universe.bounding_box

Perhaps not a huge saving for the user but let me know if this is a worthwhile upgrade and I can make a PR

usage
```python
mesh = openmc.RegularMesh.from_universe(my_universe, dimension=[100, 100, 100])
```

current method
```python
mesh = openmc.RegularMesh()
mesh.dimension = [100, 100, 100]
mesh.lower_left = geometry.bounding_box[0]
mesh.upper_right = geometry.bounding_box[1]
```

additional code required in mesh.py
```python
    @classmethod
    def from_universe(cls, universe, dimension=[10, 10, 10], mesh_id=None, name=''):
        """Create mesh from an existing openmc universe

        Parameters
        ----------
        universe : openmc.Universe
            Open used as a template for this mesh
        dimension : Iterable of int
            The number of mesh cells in each direction.
        mesh_id : int
            Unique identifier for the mesh
        name : str
            Name of the mesh

        Returns
        -------
        openmc.RegularMesh
            RegularMesh instance

        """
        cv.check_type('rectangular lattice', universe, openmc.Universe)

        mesh = cls(mesh_id, name)
        mesh.lower_left = universe.bounding_box[0]
        mesh.upper_right = universe.bounding_box[1]
        mesh.dimension = dimension

        return mesh
```


Discussion:
`Universe` is one of several classes that has a `bounding_box` property (`Cell`, `Region`, `Geometry` also do), so if we were to do this, we'd probably want to make it more general
Agreed @paulromano. A few instance type checks, new parameter name for `universe`, and an update to the docstrings should get us pretty close I think.

Resolving patch:
diff --git a/openmc/mesh.py b/openmc/mesh.py
--- a/openmc/mesh.py
+++ b/openmc/mesh.py
@@ -279,6 +279,7 @@ def write_data_to_vtk(self, points, filename, datasets, volume_normalization=Tru
 
         return vtk_grid
 
+
 class RegularMesh(StructuredMesh):
     """A regular Cartesian mesh in one, two, or three dimensions
 
@@ -509,6 +510,49 @@ def from_rect_lattice(cls, lattice, division=1, mesh_id=None, name=''):
 
         return mesh
 
+    @classmethod
+    def from_domain(
+        cls,
+        domain,
+        dimension=[100, 100, 100],
+        mesh_id=None,
+        name=''
+    ):
+        """Create mesh from an existing openmc cell, region, universe or
+        geometry by making use of the objects bounding box property.
+
+        Parameters
+        ----------
+        domain : {openmc.Cell, openmc.Region, openmc.Universe, openmc.Geometry}
+            The object passed in will be used as a template for this mesh. The
+            bounding box of the property of the object passed will be used to
+            set the lower_left and upper_right of the mesh instance
+        dimension : Iterable of int
+            The number of mesh cells in each direction.
+        mesh_id : int
+            Unique identifier for the mesh
+        name : str
+            Name of the mesh
+
+        Returns
+        -------
+        openmc.RegularMesh
+            RegularMesh instance
+
+        """
+        cv.check_type(
+            "domain",
+            domain,
+            (openmc.Cell, openmc.Region, openmc.Universe, openmc.Geometry),
+        )
+
+        mesh = cls(mesh_id, name)
+        mesh.lower_left = domain.bounding_box[0]
+        mesh.upper_right = domain.bounding_box[1]
+        mesh.dimension = dimension
+
+        return mesh
+
     def to_xml_element(self):
         """Return XML representation of the mesh
 


Test patch:
diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py
new file mode 100644
--- /dev/null
+++ b/tests/unit_tests/test_mesh_from_domain.py
@@ -0,0 +1,63 @@
+import numpy as np
+import openmc
+import pytest
+
+
+def test_reg_mesh_from_cell():
+    """Tests a RegularMesh can be made from a Cell and the specified dimensions
+    are propagated through. Cell is not centralized"""
+    surface = openmc.Sphere(r=10, x0=2, y0=3, z0=5)
+    cell = openmc.Cell(region=-surface)
+
+    mesh = openmc.RegularMesh.from_domain(cell, dimension=[7, 11, 13])
+    assert isinstance(mesh, openmc.RegularMesh)
+    assert np.array_equal(mesh.dimension, (7, 11, 13))
+    assert np.array_equal(mesh.lower_left, cell.bounding_box[0])
+    assert np.array_equal(mesh.upper_right, cell.bounding_box[1])
+
+
+def test_reg_mesh_from_region():
+    """Tests a RegularMesh can be made from a Region and the default dimensions
+    are propagated through. Region is not centralized"""
+    surface = openmc.Sphere(r=1, x0=-5, y0=-3, z0=-2)
+    region = -surface
+
+    mesh = openmc.RegularMesh.from_domain(region)
+    assert isinstance(mesh, openmc.RegularMesh)
+    assert np.array_equal(mesh.dimension, (100, 100, 100))  # default values
+    assert np.array_equal(mesh.lower_left, region.bounding_box[0])
+    assert np.array_equal(mesh.upper_right, region.bounding_box[1])
+
+
+def test_reg_mesh_from_universe():
+    """Tests a RegularMesh can be made from a Universe and the default dimensions
+    are propagated through. Universe is centralized"""
+    surface = openmc.Sphere(r=42)
+    cell = openmc.Cell(region=-surface)
+    universe = openmc.Universe(cells=[cell])
+
+    mesh = openmc.RegularMesh.from_domain(universe)
+    assert isinstance(mesh, openmc.RegularMesh)
+    assert np.array_equal(mesh.dimension, (100, 100, 100))  # default values
+    assert np.array_equal(mesh.lower_left, universe.bounding_box[0])
+    assert np.array_equal(mesh.upper_right, universe.bounding_box[1])
+
+
+def test_reg_mesh_from_geometry():
+    """Tests a RegularMesh can be made from a Geometry and the default dimensions
+    are propagated through. Geometry is centralized"""
+    surface = openmc.Sphere(r=42)
+    cell = openmc.Cell(region=-surface)
+    universe = openmc.Universe(cells=[cell])
+    geometry = openmc.Geometry(universe)
+
+    mesh = openmc.RegularMesh.from_domain(geometry)
+    assert isinstance(mesh, openmc.RegularMesh)
+    assert np.array_equal(mesh.dimension, (100, 100, 100))  # default values
+    assert np.array_equal(mesh.lower_left, geometry.bounding_box[0])
+    assert np.array_equal(mesh.upper_right, geometry.bounding_box[1])
+
+
+def test_error_from_unsupported_object():
+    with pytest.raises(TypeError):
+        openmc.RegularMesh.from_domain("vacuum energy")