Repository: openmc-dev/openmc

Issue:
Spherical mesh VTK coordinates bug
As I was working on #2397, I noticed that spherical meshes with a `theta_grid` property covering only part of  [0, $\pi$] resulted in an incorrect mesh

```python
import openmc
import numpy as np
sphere_mesh = openmc.SphericalMesh()
sphere_mesh.r_grid = np.linspace(0, 50, 31)
sphere_mesh.theta_grid = np.linspace(0, 0.5 * np.pi, 11)
sphere_mesh.phi_grid = np.linspace(0, 2*np.pi, 21)
sphere_mesh.write_data_to_vtk('test.vtk', datasets={})
```

![image](https://user-images.githubusercontent.com/4563941/223874698-279dc6a5-c318-4cb0-a035-ec4437ea9f14.png)


This is caused by incorrect use of $\phi$ and $\theta$ in the `SphericalMesh.write_data_to_vtk` method:

https://github.com/openmc-dev/openmc/blob/9768e34b869e9d698282778d2397bf9a982ed24f/openmc/mesh.py#L1536-L1538

The meaning of these variables in spherical coordinates have opposite conventions in mathematics and physics. This can be confusing. Our implementation adopts the one used in physics. After correcting these lines, the VTK file is correct:

![image](https://user-images.githubusercontent.com/4563941/223875754-502c54a1-15ab-4bd9-85b0-d46f4491ef49.png)

I'll submit a PR with this fix and some additional test coverage shortly.


Discussion:
Closed by #2417
This bug was reintroduced in #2256. I'll open a PR shortly to correct the issue.
And I suppose I'll improve the related test since reverting the code didn't trigger a failure...

Resolving patch:
diff --git a/openmc/mesh.py b/openmc/mesh.py
--- a/openmc/mesh.py
+++ b/openmc/mesh.py
@@ -1338,7 +1338,7 @@ def write_data_to_vtk(self, filename, datasets, volume_normalization=True):
         pts_cartesian[:, 0] = r * np.cos(phi) + self.origin[0]
         pts_cartesian[:, 1] = r * np.sin(phi) + self.origin[1]
         pts_cartesian[:, 2] += self.origin[2]
-        
+
         return super().write_data_to_vtk(
             points=pts_cartesian,
             filename=filename,
@@ -1585,9 +1585,9 @@ def write_data_to_vtk(self, filename, datasets, volume_normalization=True):
 
         r, theta, phi = pts_spherical[:, 0], pts_spherical[:, 1], pts_spherical[:, 2]
 
-        pts_cartesian[:, 0] = r * np.sin(phi) * np.cos(theta) + self.origin[0]
-        pts_cartesian[:, 1] = r * np.sin(phi) * np.sin(theta) + self.origin[1]
-        pts_cartesian[:, 2] = r * np.cos(phi) + self.origin[2]
+        pts_cartesian[:, 0] = r * np.sin(theta) * np.cos(phi) + self.origin[0]
+        pts_cartesian[:, 1] = r * np.sin(theta) * np.sin(phi) + self.origin[1]
+        pts_cartesian[:, 2] = r * np.cos(theta) + self.origin[2]
 
         return super().write_data_to_vtk(
             points=pts_cartesian,


Test patch:
diff --git a/tests/unit_tests/test_mesh_to_vtk.py b/tests/unit_tests/test_mesh_to_vtk.py
--- a/tests/unit_tests/test_mesh_to_vtk.py
+++ b/tests/unit_tests/test_mesh_to_vtk.py
@@ -273,3 +273,45 @@ def in_geom(cell):
             # need to get flat index with axes reversed due to ordering passed into the VTK file
             flat_idx = np.ravel_multi_index(tuple(ijk[::-1]), mesh.dimension[::-1])
             assert vtk_data[flat_idx] == 0.0, err_msg
+
+
+def test_sphere_mesh_coordinates(run_in_tmpdir):
+    mesh = openmc.SphericalMesh()
+    mesh.r_grid = np.linspace(0.1, 10, 30)
+    mesh.phi_grid = np.linspace(0, 1.5*np.pi, 25)
+    mesh.theta_grid = np.linspace(0, np.pi / 2, 15)
+
+    # write the data to a VTK file (no data)
+    vtk_filename = 'test.vtk'
+    mesh.write_data_to_vtk(vtk_filename, {})
+
+    # read file
+    reader = vtk.vtkStructuredGridReader()
+    reader.SetFileName(str(vtk_filename))
+    reader.Update()
+
+    vtk_grid = reader.GetOutput()
+
+    # create a region that matches the spherical mesh description
+    x = openmc.XPlane()
+    z = openmc.ZPlane()
+    y = openmc.YPlane()
+    s = openmc.Sphere(r=10.0)
+
+    region = +z & +y & -s | -x & -y & +z & -s
+
+    # the VTK interface will update this list when GetCentroid is called
+    centroid = np.zeros(3)
+
+    # ensure all centroids of the sphere mesh are inside the cell region
+    for i in range(vtk_grid.GetNumberOfCells()):
+        # get the cell from the stuctured mesh object
+        cell = vtk_grid.GetCell(i)
+        cell.GetCentroid(centroid)
+
+        # if the coordinate conversion is happening correctly,
+        # every one of the cell centroids should be in the CSG region
+        assert centroid in region, \
+            f'Cell centroid {centroid} not in equivalent ' \
+            f'CSG region for spherical mesh {mesh}'
+