Repository: UXARRAY/uxarray

Issue:
MPAS Edge & Face Distances Parsed in Meters
Turns out the issue with the `30km` MPAS grid was the `edge_node_distances` were represented in `meters`, instead of `radians`, which is inconsistent with our `Grid.edge_node_distances` definition. The same behavior can be see in the `meshfiles\mpas\QU\oQU480.231010.nc` grid, but the result is still correct. This was causing the hash cell size to be much larger than exepcted. 

It also appears we have a typo in our `Grid.edge_node_distances` definition. The unit should be `radians` instead of `degrees` here 

https://github.com/UXARRAY/uxarray/blob/d9f091f6223e5881b3b3a5784ba4bd0869b20437/uxarray/grid/grid.py#L1386-L1394

I'll raise an issue and get this fixed. Glad to see that the issue wasn't anything with your implementation.

_Originally posted by @philipc2 in https://github.com/UXARRAY/uxarray/issues/1169#issuecomment-2711671533_

Discussion:

Solution patch:
diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py
index 1266a7f14..9cae36bac 100644
--- a/uxarray/grid/grid.py
+++ b/uxarray/grid/grid.py
@@ -1387,7 +1387,7 @@ def node_face_connectivity(self, value):
 
     @property
     def edge_node_distances(self):
-        """Distances between the two nodes that surround each edge in degrees.
+        """Distances between the two nodes that surround each edge in radians.
 
         Dimensions ``(n_edge, )``
         """
@@ -1404,7 +1404,7 @@ def edge_node_distances(self, value):
     @property
     def edge_face_distances(self):
         """Distances between the centers of the faces that saddle each edge in
-        degrees.
+        radians.
 
         Dimensions ``(n_edge, )``
         """
@@ -1782,6 +1782,10 @@ def get_spatial_hash(
         self._spatialhash : grid.Neighbors.SpatialHash
             SpatialHash instance
 
+        Note
+        ----
+        Does not currently support queries on periodic elements.
+
         Examples
         --------
         Open a grid from a file path:
diff --git a/uxarray/grid/neighbors.py b/uxarray/grid/neighbors.py
index 5bdc56609..1212c8a29 100644
--- a/uxarray/grid/neighbors.py
+++ b/uxarray/grid/neighbors.py
@@ -792,6 +792,10 @@ class SpatialHash:
         Source grid used to construct the hash grid and hash table
     reconstruct : bool, default=False
         If true, reconstructs the spatial hash
+
+    Note
+    ----
+    Does not currently support queries on periodic elements.
     """
 
     def __init__(
@@ -806,11 +810,21 @@ def __init__(
 
         # Hash grid size
         self._dh = self._hash_cell_size()
+
         # Lower left corner of the hash grid
-        self._xmin = np.deg2rad(self._source_grid.node_lon.min().to_numpy()) - self._dh
-        self._ymin = np.deg2rad(self._source_grid.node_lat.min().to_numpy()) - self._dh
-        self._xmax = np.deg2rad(self._source_grid.node_lon.max().to_numpy()) + self._dh
-        self._ymax = np.deg2rad(self._source_grid.node_lat.max().to_numpy()) + self._dh
+        lon_min = np.deg2rad(self._source_grid.node_lon.min().to_numpy())
+        lat_min = np.deg2rad(self._source_grid.node_lat.min().to_numpy())
+        lon_max = np.deg2rad(self._source_grid.node_lon.max().to_numpy())
+        lat_max = np.deg2rad(self._source_grid.node_lat.max().to_numpy())
+
+        self._xmin = lon_min - self._dh
+        self._ymin = lat_min - self._dh
+        self._xmax = lon_max + self._dh
+        self._ymax = lat_max + self._dh
+
+        print(self._xmin, self._xmax)
+        print(self._ymin, self._ymax)
+
         # Number of x points in the hash grid; used for
         # array flattening
         Lx = self._xmax - self._xmin
@@ -866,10 +880,15 @@ def _initialize_face_hash_table(self):
             )
             i2, j2 = self._hash_index2d(coords)
 
-            for eid in range(self._source_grid.n_face):
-                for j in range(j1[eid], j2[eid] + 1):
-                    for i in range(i1[eid], i2[eid] + 1):
-                        index_to_face[i + self._nx * j].append(eid)
+            try:
+                for eid in range(self._source_grid.n_face):
+                    for j in range(j1[eid], j2[eid] + 1):
+                        for i in range(i1[eid], i2[eid] + 1):
+                            index_to_face[i + self._nx * j].append(eid)
+            except IndexError:
+                raise IndexError(
+                    "list index out of range. This may indicate incorrect `edge_node_distances` values."
+                )
 
             return index_to_face
 
diff --git a/uxarray/io/_mpas.py b/uxarray/io/_mpas.py
index 26caa5967..0177f7bce 100644
--- a/uxarray/io/_mpas.py
+++ b/uxarray/io/_mpas.py
@@ -403,7 +403,7 @@ def _parse_edge_faces(in_ds, out_ds, mesh_type):
 
 def _parse_edge_node_distances(in_ds, out_ds):
     """Parses ``edge_node_distances``"""
-    edge_node_distances = in_ds["dvEdge"]
+    edge_node_distances = in_ds["dvEdge"] / in_ds.attrs["sphere_radius"]
 
     out_ds["edge_node_distances"] = edge_node_distances.assign_attrs(
         descriptors.EDGE_NODE_DISTANCES_ATTRS
@@ -412,7 +412,7 @@ def _parse_edge_node_distances(in_ds, out_ds):
 
 def _parse_edge_face_distances(in_ds, out_ds):
     """Parses ``edge_face_distances``"""
-    edge_face_distances = in_ds["dcEdge"]
+    edge_face_distances = in_ds["dcEdge"] / in_ds.attrs["sphere_radius"]
 
     out_ds["edge_face_distances"] = edge_face_distances.assign_attrs(
         descriptors.EDGE_FACE_DISTANCES_ATTRS


Test patch:
diff --git a/test/test_mpas.py b/test/test_mpas.py
index cf1a915b3..011892123 100644
--- a/test/test_mpas.py
+++ b/test/test_mpas.py
@@ -4,6 +4,7 @@
 import uxarray as ux
 import xarray as xr
 from pathlib import Path
+import numpy.testing as nt
 from uxarray.constants import INT_DTYPE, INT_FILL_VALUE
 from uxarray.io._mpas import _replace_padding, _replace_zeros, _to_zero_index, _read_mpas
 
@@ -110,3 +111,14 @@ def test_face_area():
 
     assert "face_areas" in uxgrid_primal._ds
     assert "face_areas" in uxgrid_dual._ds
+
+
+def test_distance_units():
+    xrds = xr.open_dataset(mpas_ocean_mesh)
+    uxgrid = ux.open_grid(mpas_ocean_mesh)
+
+    assert "edge_node_distances" in uxgrid._ds
+    assert "edge_face_distances" in uxgrid._ds
+
+    nt.assert_array_almost_equal(uxgrid['edge_node_distances'].values, (xrds['dvEdge'].values / xrds.attrs['sphere_radius']))
+    nt.assert_array_almost_equal(uxgrid['edge_face_distances'].values, (xrds['dcEdge'].values / xrds.attrs['sphere_radius']))