Skip to content

Commit 95fcfce

Browse files
rajeejaRajeev Jainphilipc2
authored
Fix MPAS reader bug: Normalize after loading (#1344)
* o MPAS reader bug: Normalize after loading * Add test for MPAS grid normalization verification * Add face area and edge length normalization tests for unit sphere * o Fix area normalization in MPAS reader * o MPAS files might not have the sphere_radius attribute fix reader * io/mpas: guard normalization of dvEdge/dcEdge by radius!=1.0 for consistency with XYZ/areas; no functional change when unit sphere. Verified by test/test_mpas.py (11 passed). --------- Co-authored-by: Rajeev Jain <rajeeja@@gmail.com> Co-authored-by: Philip Chmielowiec <[email protected]>
1 parent 2e68c1a commit 95fcfce

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

test/test_mpas.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,100 @@ def test_distance_units():
133133

134134
nt.assert_array_almost_equal(uxgrid['edge_node_distances'].values, (xrds['dvEdge'].values / xrds.attrs['sphere_radius']))
135135
nt.assert_array_almost_equal(uxgrid['edge_face_distances'].values, (xrds['dcEdge'].values / xrds.attrs['sphere_radius']))
136+
137+
138+
def test_ocean_mesh_normalization():
139+
"""Test that MPAS ocean mesh with non-unit sphere radius is properly normalized."""
140+
# Ocean mesh has sphere_radius = 6371229.0 meters
141+
uxgrid = ux.open_grid(mpas_ocean_mesh, use_dual=False)
142+
143+
# Check node coordinates are normalized to unit sphere
144+
node_x = uxgrid._ds['node_x'].values
145+
node_y = uxgrid._ds['node_y'].values
146+
node_z = uxgrid._ds['node_z'].values
147+
148+
# Compute radius for each node
149+
radii = np.sqrt(node_x**2 + node_y**2 + node_z**2)
150+
151+
# All radii should be 1.0 (unit sphere)
152+
nt.assert_array_almost_equal(radii, np.ones_like(radii), decimal=10)
153+
154+
# Check face areas are normalized for unit sphere
155+
# Ocean mesh only covers ~70% of sphere (ocean area)
156+
face_areas = uxgrid.face_areas.values
157+
158+
# Check that all face areas are positive
159+
assert np.all(face_areas > 0), "All face areas should be positive"
160+
161+
# Total area should be less than full sphere (4*pi) but reasonable
162+
total_area = np.sum(face_areas)
163+
full_sphere_area = 4.0 * np.pi
164+
assert 0.5 < total_area < full_sphere_area, f"Total area {total_area} should be between 0.5 and {full_sphere_area}"
165+
166+
# Check that individual face areas are reasonable for unit sphere
167+
max_face_area = np.max(face_areas)
168+
min_face_area = np.min(face_areas)
169+
assert max_face_area < 0.1 * full_sphere_area, "Maximum face area seems too large for unit sphere"
170+
assert min_face_area > 1e-10, "Minimum face area seems too small"
171+
172+
# Check edge lengths are normalized for unit sphere
173+
if "edge_node_distances" in uxgrid._ds:
174+
edge_lengths = uxgrid._ds["edge_node_distances"].values
175+
176+
# All edge lengths should be positive and <= pi
177+
assert np.all(edge_lengths > 0), "All edge lengths should be positive"
178+
assert np.max(edge_lengths) <= np.pi, "Edge lengths should not exceed pi on unit sphere"
179+
180+
181+
def test_grid_normalization():
182+
"""Test that MPAS grid coordinates are properly normalized."""
183+
uxgrid = ux.open_grid(mpas_grid_path, use_dual=False)
184+
185+
# Check node coordinates are normalized
186+
node_lon = uxgrid._ds['node_lon'].values
187+
node_lat = uxgrid._ds['node_lat'].values
188+
node_x = uxgrid._ds['node_x'].values
189+
node_y = uxgrid._ds['node_y'].values
190+
node_z = uxgrid._ds['node_z'].values
191+
192+
# Compute radius for each node
193+
radii = np.sqrt(node_x**2 + node_y**2 + node_z**2)
194+
195+
# All radii should be 1.0 (unit sphere)
196+
nt.assert_array_almost_equal(radii, np.ones_like(radii), decimal=10)
197+
198+
# Check face areas are normalized for unit sphere
199+
# Total surface area of unit sphere is 4*pi
200+
expected_total_area = 4.0 * np.pi
201+
202+
# Get face areas
203+
face_areas = uxgrid.face_areas.values
204+
205+
# Check that all face areas are positive
206+
assert np.all(face_areas > 0), "All face areas should be positive"
207+
208+
# Check that total area equals surface area of unit sphere
209+
total_area = np.sum(face_areas)
210+
nt.assert_almost_equal(total_area, expected_total_area, decimal=7)
211+
212+
# Check that face areas are reasonable (not too small or too large)
213+
# For a unit sphere, face areas should be much smaller than total area
214+
max_face_area = np.max(face_areas)
215+
min_face_area = np.min(face_areas)
216+
217+
assert max_face_area < 0.1 * expected_total_area, "Maximum face area seems too large for unit sphere"
218+
assert min_face_area > 1e-10, "Minimum face area seems too small"
219+
220+
# Check edge lengths are normalized for unit sphere
221+
# Edge lengths should be reasonable for a unit sphere (max possible is pi for antipodal points)
222+
if "edge_node_distances" in uxgrid._ds:
223+
edge_lengths = uxgrid._ds["edge_node_distances"].values
224+
225+
# All edge lengths should be positive
226+
assert np.all(edge_lengths > 0), "All edge lengths should be positive"
227+
228+
# Maximum edge length on unit sphere cannot exceed pi
229+
assert np.max(edge_lengths) <= np.pi, "Edge lengths should not exceed pi on unit sphere"
230+
231+
# Minimum edge length should be reasonable
232+
assert np.min(edge_lengths) > 1e-10, "Minimum edge length seems too small"

uxarray/io/_mpas.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ def _parse_node_xyz_coords(in_ds, out_ds, mesh_type):
190190
node_y = node_y.rename({"nCells": ugrid.NODE_DIM})
191191
node_z = node_z.rename({"nCells": ugrid.NODE_DIM})
192192

193+
# Normalize coordinates to unit sphere if needed
194+
radius = in_ds.attrs.get("sphere_radius", 1.0)
195+
if radius != 1.0:
196+
node_x = node_x / radius
197+
node_y = node_y / radius
198+
node_z = node_z / radius
199+
193200
out_ds["node_x"] = node_x.assign_attrs(ugrid.NODE_X_ATTRS)
194201
out_ds["node_y"] = node_y.assign_attrs(ugrid.NODE_Y_ATTRS)
195202
out_ds["node_z"] = node_z.assign_attrs(ugrid.NODE_Z_ATTRS)
@@ -237,6 +244,13 @@ def _parse_face_xyz_coords(in_ds, out_ds, mesh_type):
237244
face_y = face_y.rename({"nVertices": ugrid.FACE_DIM})
238245
face_z = face_z.rename({"nVertices": ugrid.FACE_DIM})
239246

247+
# Normalize coordinates to unit sphere if needed
248+
radius = in_ds.attrs.get("sphere_radius", 1.0)
249+
if radius != 1.0:
250+
face_x = face_x / radius
251+
face_y = face_y / radius
252+
face_z = face_z / radius
253+
240254
out_ds["face_x"] = face_x.assign_attrs(ugrid.FACE_X_ATTRS)
241255
out_ds["face_y"] = face_y.assign_attrs(ugrid.FACE_Y_ATTRS)
242256
out_ds["face_z"] = face_z.assign_attrs(ugrid.FACE_Z_ATTRS)
@@ -266,6 +280,13 @@ def _parse_edge_xyz_coords(in_ds, out_ds, mesh_type):
266280
edge_y = edge_y.rename({"nEdges": ugrid.EDGE_DIM})
267281
edge_z = edge_z.rename({"nEdges": ugrid.EDGE_DIM})
268282

283+
# Normalize coordinates to unit sphere if needed
284+
radius = in_ds.attrs.get("sphere_radius", 1.0)
285+
if radius != 1.0:
286+
edge_x = edge_x / radius
287+
edge_y = edge_y / radius
288+
edge_z = edge_z / radius
289+
269290
out_ds["edge_x"] = edge_x.assign_attrs(ugrid.EDGE_X_ATTRS)
270291
out_ds["edge_y"] = edge_y.assign_attrs(ugrid.EDGE_Y_ATTRS)
271292
out_ds["edge_z"] = edge_z.assign_attrs(ugrid.EDGE_Z_ATTRS)
@@ -380,7 +401,10 @@ def _parse_face_faces(in_ds, out_ds, mesh_type):
380401

381402
def _parse_edge_node_distances(in_ds, out_ds):
382403
"""Parses ``edge_node_distances``"""
383-
edge_node_distances = in_ds["dvEdge"] / in_ds.attrs["sphere_radius"]
404+
radius = in_ds.attrs.get("sphere_radius", 1.0)
405+
edge_node_distances = in_ds["dvEdge"]
406+
if radius != 1.0:
407+
edge_node_distances = edge_node_distances / radius
384408

385409
out_ds["edge_node_distances"] = edge_node_distances.assign_attrs(
386410
descriptors.EDGE_NODE_DISTANCES_ATTRS
@@ -389,7 +413,10 @@ def _parse_edge_node_distances(in_ds, out_ds):
389413

390414
def _parse_edge_face_distances(in_ds, out_ds):
391415
"""Parses ``edge_face_distances``"""
392-
edge_face_distances = in_ds["dcEdge"] / in_ds.attrs["sphere_radius"]
416+
radius = in_ds.attrs.get("sphere_radius", 1.0)
417+
edge_face_distances = in_ds["dcEdge"]
418+
if radius != 1.0:
419+
edge_face_distances = edge_face_distances / radius
393420

394421
out_ds["edge_face_distances"] = edge_face_distances.assign_attrs(
395422
descriptors.EDGE_FACE_DISTANCES_ATTRS
@@ -408,6 +435,12 @@ def _parse_face_areas(in_ds, out_ds, mesh_type):
408435
else:
409436
face_area = in_ds["areaTriangle"]
410437

438+
# Normalize face areas to unit sphere if needed
439+
radius = in_ds.attrs.get("sphere_radius", 1.0)
440+
if radius != 1.0:
441+
# Area scales with radius squared
442+
face_area = face_area / (radius * radius)
443+
411444
out_ds["face_areas"] = face_area.assign_attrs(descriptors.FACE_AREAS_ATTRS).rename(
412445
{face_area.dims[0]: ugrid.FACE_DIM}
413446
)

0 commit comments

Comments
 (0)