Skip to content

Fix silent lon wrapping for projected (non-spherical) UGRID grids#1525

Open
rajeeja wants to merge 2 commits into
mainfrom
feat/projected-grid-support
Open

Fix silent lon wrapping for projected (non-spherical) UGRID grids#1525
rajeeja wants to merge 2 commits into
mainfrom
feat/projected-grid-support

Conversation

@rajeeja

@rajeeja rajeeja commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #1524.

Problem

_set_desired_longitude_range applied (x + 180) % 360 - 180 unconditionally whenever node_lon.max() > 180 — no check on units or standard_name. Projected coordinates in meters (e.g. x ~ 1.5e6) were silently corrupted to garbage degree values with no warning.

Fix

Add _is_projected_grid() in coordinates.py that reads CF metadata already present on the renamed node_lon variable, in priority order:

  1. standard_name = "projection_x_coordinate" → projected
  2. standard_name = "longitude" → geographic (short-circuit)
  3. units is a length unit (m, km, ft, …) → projected
  4. units is angular (degrees_east, …) → geographic
  5. grid_mapping variable with grid_mapping_name != "latitude_longitude" → projected
  6. No signal → geographic (backward-compatible default)

_set_desired_longitude_range skips wrapping when projected and emits a UserWarning listing which spherical operations are invalid (face areas, GCA intersections, zonal mean, bounds, cross-sections). Loading, plotting, and connectivity are unaffected.

Notes

  • No new public API — is_projected/is_geographic properties are deferred to a future CRS-design PR
  • from_topology limitation: raw arrays carry no CF attrs, so detection cannot fire; callers must use from_dataset with an attributed xr.Dataset
  • MPAS unaffected: reader assigns NODE_LON_ATTRS with standard_name="longitude" explicitly
  • 4 tests, 522 existing tests pass

Fixes #1524.

_is_projected_grid() in coordinates.py detects projected coordinates from
CF standard_name ("projection_x_coordinate"), length units (m, km, ...),
or a non-latlon grid_mapping variable — in that priority order, falling
back to geographic (backward-compatible).

_set_desired_longitude_range skips wrapping and emits a UserWarning when
projected coordinates are detected, naming which spherical operations are
invalid. Coordinates are preserved as-is.

4 tests: no-wrap on load, units detection, grid_mapping detection,
geographic wrapping still works.
@rajeeja rajeeja force-pushed the feat/projected-grid-support branch from fa4d70b to 52e767e Compare June 12, 2026 21:33
@rajeeja rajeeja changed the title Add Grid.is_projected/is_geographic; fix silent lon wrapping for projected grids Fix silent lon wrapping for projected (non-spherical) UGRID grids Jun 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where UXarray would silently wrap node_lon into [-180, 180] whenever values exceeded 180, even when the grid’s coordinates are projected (e.g., meters) rather than geographic degrees, corrupting projected UGRID meshes (Fixes #1524).

Changes:

  • Added CF-metadata-based projected-grid detection (_is_projected_grid) and skip longitude wrapping for projected coordinates with a UserWarning.
  • Added tests covering projected detection via standard_name, units, and grid_mapping, plus a regression test ensuring geographic wrapping still occurs.
  • Documented projected (non-spherical) UGRID behavior and limitations in the user guide.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
uxarray/grid/coordinates.py Adds projected-grid detection and guards longitude wrapping with a warning for projected coordinates.
test/grid/grid/test_projected.py Adds regression/unit tests for projected detection and safe loading behavior.
docs/user-guide/grid-formats.rst Documents projected-coordinate detection signals, warning behavior, and limitations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +740 to +754
if _is_projected_grid(uxgrid):
import warnings

warnings.warn(
"Projected (non-spherical) coordinates detected on this grid "
"(standard_name='projection_x_coordinate' or length units). "
"UXarray's geometry algorithms (face areas, GCA intersections, "
"zonal mean, bounds, cross-sections) assume a unit sphere and "
"will produce incorrect results on projected grids. "
"Loading, plotting, and connectivity operations are unaffected. "
"Check Grid.is_projected for the detected coordinate type.",
UserWarning,
stacklevel=3,
)
return
Comment on lines +714 to +716
units = attrs.get("units", "").lower().strip()
_ANGULAR_UNITS = {"degrees_east", "degrees", "degree", "deg", "rad", "radians"}
_LENGTH_UNITS = {"m", "km", "meter", "meters", "metre", "metres", "ft", "feet"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Projected (Cartesian/meter) node coordinates are silently modulo-wrapped into [-180, 180]

2 participants