uxarray 2026.4.0__tar.gz → 2026.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {uxarray-2026.4.0 → uxarray-2026.6.0}/PKG-INFO +1 -1
  2. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/__init__.py +2 -0
  3. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/dataarray.py +153 -14
  4. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/dataset.py +26 -5
  5. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/gradient.py +26 -1
  6. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/zonal.py +230 -71
  7. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/coordinates.py +84 -1
  8. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/geometry.py +6 -6
  9. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/grid.py +3 -0
  10. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/plot/accessor.py +6 -4
  11. uxarray-2026.6.0/uxarray/remap/__init__.py +13 -0
  12. uxarray-2026.6.0/uxarray/remap/accessor.py +374 -0
  13. uxarray-2026.6.0/uxarray/remap/apply_weights.py +120 -0
  14. uxarray-2026.6.0/uxarray/remap/structured.py +216 -0
  15. uxarray-2026.6.0/uxarray/remap/weights.py +194 -0
  16. uxarray-2026.6.0/uxarray/remap/yac.py +516 -0
  17. uxarray-2026.6.0/uxarray/tutorial/__init__.py +208 -0
  18. uxarray-2026.6.0/uxarray/tutorial/registry.py +102 -0
  19. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray.egg-info/PKG-INFO +1 -1
  20. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray.egg-info/SOURCES.txt +6 -0
  21. uxarray-2026.4.0/uxarray/remap/__init__.py +0 -7
  22. uxarray-2026.4.0/uxarray/remap/accessor.py +0 -112
  23. {uxarray-2026.4.0 → uxarray-2026.6.0}/.codecov.yml +0 -0
  24. {uxarray-2026.4.0 → uxarray-2026.6.0}/.git_archival.txt +0 -0
  25. {uxarray-2026.4.0 → uxarray-2026.6.0}/.gitattributes +0 -0
  26. {uxarray-2026.4.0 → uxarray-2026.6.0}/CITATION.cff +0 -0
  27. {uxarray-2026.4.0 → uxarray-2026.6.0}/CODE_OF_CONDUCT.md +0 -0
  28. {uxarray-2026.4.0 → uxarray-2026.6.0}/CONTRIBUTING.md +0 -0
  29. {uxarray-2026.4.0 → uxarray-2026.6.0}/INSTALLATION.md +0 -0
  30. {uxarray-2026.4.0 → uxarray-2026.6.0}/LICENSE +0 -0
  31. {uxarray-2026.4.0 → uxarray-2026.6.0}/MANIFEST.in +0 -0
  32. {uxarray-2026.4.0 → uxarray-2026.6.0}/README.md +0 -0
  33. {uxarray-2026.4.0 → uxarray-2026.6.0}/build.sh +0 -0
  34. {uxarray-2026.4.0 → uxarray-2026.6.0}/pyproject.toml +0 -0
  35. {uxarray-2026.4.0 → uxarray-2026.6.0}/setup.cfg +0 -0
  36. {uxarray-2026.4.0 → uxarray-2026.6.0}/setup.py +0 -0
  37. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/constants.py +0 -0
  38. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/conventions/__init__.py +0 -0
  39. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/conventions/descriptors.py +0 -0
  40. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/conventions/ugrid.py +0 -0
  41. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/__init__.py +0 -0
  42. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/accessors.py +0 -0
  43. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/aggregation.py +0 -0
  44. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/api.py +0 -0
  45. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/core/utils.py +0 -0
  46. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/cross_sections/__init__.py +0 -0
  47. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/cross_sections/dataarray_accessor.py +0 -0
  48. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/cross_sections/grid_accessor.py +0 -0
  49. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/cross_sections/sample.py +0 -0
  50. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/formatting_html.py +0 -0
  51. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/__init__.py +0 -0
  52. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/arcs.py +0 -0
  53. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/area.py +0 -0
  54. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/bounds.py +0 -0
  55. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/connectivity.py +0 -0
  56. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/dual.py +0 -0
  57. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/integrate.py +0 -0
  58. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/intersections.py +0 -0
  59. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/neighbors.py +0 -0
  60. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/point_in_face.py +0 -0
  61. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/slice.py +0 -0
  62. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/utils.py +0 -0
  63. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/grid/validation.py +0 -0
  64. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/__init__.py +0 -0
  65. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_delaunay.py +0 -0
  66. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_esmf.py +0 -0
  67. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_exodus.py +0 -0
  68. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_fesom2.py +0 -0
  69. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_geopandas.py +0 -0
  70. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_geos.py +0 -0
  71. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_healpix.py +0 -0
  72. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_icon.py +0 -0
  73. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_mpas.py +0 -0
  74. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_scrip.py +0 -0
  75. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_structured.py +0 -0
  76. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_topology.py +0 -0
  77. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_ugrid.py +0 -0
  78. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_vertices.py +0 -0
  79. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/_voronoi.py +0 -0
  80. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/io/utils.py +0 -0
  81. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/plot/__init__.py +0 -0
  82. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/plot/constants.py +0 -0
  83. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/plot/matplotlib.py +0 -0
  84. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/plot/utils.py +0 -0
  85. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/remap/bilinear.py +0 -0
  86. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/remap/inverse_distance_weighted.py +0 -0
  87. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/remap/nearest_neighbor.py +0 -0
  88. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/remap/spatial_coords_remap.py +0 -0
  89. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/remap/utils.py +0 -0
  90. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/subset/__init__.py +0 -0
  91. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/subset/dataarray_accessor.py +0 -0
  92. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/subset/grid_accessor.py +0 -0
  93. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/utils/__init__.py +0 -0
  94. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray/utils/computing.py +0 -0
  95. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray.egg-info/dependency_links.txt +0 -0
  96. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray.egg-info/requires.txt +0 -0
  97. {uxarray-2026.4.0 → uxarray-2026.6.0}/uxarray.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uxarray
3
- Version: 2026.4.0
3
+ Version: 2026.6.0
4
4
  Summary: Xarray extension for unstructured climate and global weather data analysis and visualization.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -1,3 +1,4 @@
1
+ from . import tutorial
1
2
  from .constants import INT_DTYPE, INT_FILL_VALUE
2
3
  from .core.api import (
3
4
  concat,
@@ -30,6 +31,7 @@ __all__ = (
30
31
  "open_multigrid",
31
32
  "list_grid_names",
32
33
  "concat",
34
+ "tutorial",
33
35
  "UxDataset",
34
36
  "UxDataArray",
35
37
  "INT_DTYPE",
@@ -24,6 +24,7 @@ from uxarray.core.utils import _map_dims_to_ugrid
24
24
  from uxarray.core.zonal import (
25
25
  _compute_conservative_zonal_mean_bands,
26
26
  _compute_non_conservative_zonal_mean,
27
+ _compute_zonal_anomaly,
27
28
  )
28
29
  from uxarray.cross_sections import UxDataArrayCrossSectionAccessor
29
30
  from uxarray.formatting_html import array_repr
@@ -465,12 +466,7 @@ class UxDataArray(xr.DataArray):
465
466
  else:
466
467
 
467
468
  def _is_default_extent() -> bool:
468
- # Default extents are indicated by xlim/ylim being (0, 1)
469
- # when autoscale is still on (no extent has been explicitly set)
470
- if not ax.get_autoscale_on():
471
- return False
472
- xlim, ylim = ax.get_xlim(), ax.get_ylim()
473
- return np.allclose(xlim, (0.0, 1.0)) and np.allclose(ylim, (0.0, 1.0))
469
+ return ax.get_autoscale_on()
474
470
 
475
471
  if _is_default_extent():
476
472
  try:
@@ -772,6 +768,70 @@ class UxDataArray(xr.DataArray):
772
768
  """Alias of zonal_mean; prefer `zonal_mean` for primary API."""
773
769
  return self.zonal_mean(lat=lat, conservative=conservative, **kwargs)
774
770
 
771
+ def zonal_anomaly(self, lat=(-90, 90, 10), conservative: bool = False):
772
+ """Compute the zonal anomaly: each face value minus the mean of its latitude band.
773
+
774
+ Returns a new ``UxDataArray`` with the same dimensions as the input,
775
+ where each face holds its original value minus the zonal mean of the
776
+ latitude band it belongs to.
777
+
778
+ Parameters
779
+ ----------
780
+ lat : tuple or array-like, default=(-90, 90, 10)
781
+ Latitude band specification:
782
+ - tuple (start, end, step): band edges via np.linspace(start, end, n)
783
+ - array-like: explicit band edges in degrees
784
+ conservative : bool, default=False
785
+ If True, uses area-weighted band means and blends across bands for
786
+ faces that straddle a band boundary, reusing the face-band weight
787
+ matrix computed for zonal_mean so no geometry is duplicated.
788
+ If False, assigns each face to a band by its centroid latitude.
789
+
790
+ Returns
791
+ -------
792
+ UxDataArray
793
+ Same dimensions as input with per-face band mean subtracted.
794
+
795
+ Examples
796
+ --------
797
+ >>> uxds["var"].zonal_anomaly()
798
+ >>> uxds["var"].zonal_anomaly(lat=(-60, 60, 5), conservative=True)
799
+ """
800
+ if not self._face_centered():
801
+ raise ValueError(
802
+ "Zonal anomaly is only supported for face-centered data variables."
803
+ )
804
+
805
+ if isinstance(lat, tuple):
806
+ start, end, step = lat
807
+ if step <= 0:
808
+ raise ValueError("Step size must be positive.")
809
+ num_points = int(round((end - start) / step)) + 1
810
+ edges = np.linspace(start, end, num_points)
811
+ edges = np.clip(edges, -90, 90)
812
+ elif isinstance(lat, (list, np.ndarray)):
813
+ edges = np.asarray(lat, dtype=float)
814
+ else:
815
+ raise ValueError(
816
+ "Invalid value for 'lat'. Must be a tuple (start, end, step) or array-like band edges."
817
+ )
818
+
819
+ if edges.ndim != 1 or edges.size < 2:
820
+ raise ValueError("Band edges must be 1D with at least two values.")
821
+
822
+ res = _compute_zonal_anomaly(self, edges, conservative=conservative)
823
+
824
+ return UxDataArray(
825
+ res,
826
+ dims=self.dims,
827
+ coords=self.coords,
828
+ name=self.name + "_zonal_anomaly"
829
+ if self.name is not None
830
+ else "zonal_anomaly",
831
+ attrs={"zonal_anomaly": True, "conservative": conservative},
832
+ uxgrid=self.uxgrid,
833
+ )
834
+
775
835
  def azimuthal_mean(
776
836
  self,
777
837
  center_coord,
@@ -1416,10 +1476,15 @@ class UxDataArray(xr.DataArray):
1416
1476
  """
1417
1477
  return _uxda_grid_aggregate(self, destination, "any", **kwargs)
1418
1478
 
1419
- def gradient(self, **kwargs) -> UxDataset:
1479
+ def gradient(self, scale_by_radius: bool = True, **kwargs) -> UxDataset:
1420
1480
  """
1421
1481
  Computes the gradient of a data variable.
1422
1482
 
1483
+ Parameters
1484
+ ----------
1485
+ scale_by_radius : bool, default=True
1486
+ Divide unit-sphere derivatives by ``uxgrid.sphere_radius``.
1487
+
1423
1488
  Returns
1424
1489
  -------
1425
1490
  gradient: UxDataset
@@ -1446,7 +1511,9 @@ class UxDataArray(xr.DataArray):
1446
1511
  )
1447
1512
 
1448
1513
  # Compute the zonal and meridional gradient components of the stored data variable
1449
- grad_zonal_da, grad_meridional_da = _compute_gradient(self)
1514
+ grad_zonal_da, grad_meridional_da = _compute_gradient(
1515
+ self, scale_by_radius=scale_by_radius
1516
+ )
1450
1517
 
1451
1518
  # Create a dataset containing both gradient components
1452
1519
  return UxDataset(
@@ -1459,7 +1526,9 @@ class UxDataArray(xr.DataArray):
1459
1526
  coords=self.coords,
1460
1527
  )
1461
1528
 
1462
- def curl(self, other: "UxDataArray", **kwargs) -> "UxDataArray":
1529
+ def curl(
1530
+ self, other: "UxDataArray", scale_by_radius: bool = True, **kwargs
1531
+ ) -> "UxDataArray":
1463
1532
  """
1464
1533
  Computes the curl of a vector field.
1465
1534
 
@@ -1469,6 +1538,8 @@ class UxDataArray(xr.DataArray):
1469
1538
  The second component of the vector field. This UxDataArray should
1470
1539
  represent the meridional (v) component, while self represents the
1471
1540
  zonal (u) component.
1541
+ scale_by_radius : bool, default=True
1542
+ Divide unit-sphere derivatives by ``uxgrid.sphere_radius``.
1472
1543
  **kwargs : dict
1473
1544
  Additional keyword arguments (currently unused, reserved for future extensions).
1474
1545
 
@@ -1519,21 +1590,30 @@ class UxDataArray(xr.DataArray):
1519
1590
  )
1520
1591
 
1521
1592
  # Compute gradients of both components
1522
- grad_u_zonal, grad_u_meridional = _compute_gradient(self)
1523
- grad_v_zonal, grad_v_meridional = _compute_gradient(other)
1593
+ grad_u_zonal, grad_u_meridional = _compute_gradient(
1594
+ self, scale_by_radius=scale_by_radius
1595
+ )
1596
+ grad_v_zonal, grad_v_meridional = _compute_gradient(
1597
+ other, scale_by_radius=scale_by_radius
1598
+ )
1524
1599
 
1525
1600
  # Compute curl = ∂v/∂x - ∂u/∂y
1526
1601
  curl_values = grad_v_zonal.values - grad_u_meridional.values
1527
1602
 
1603
+ u_units = self.attrs.get("units", "")
1604
+ has_sphere_radius = "sphere_radius" in self.uxgrid._ds.attrs
1605
+ if scale_by_radius and has_sphere_radius:
1606
+ curl_units = f"({u_units})/m" if u_units else "1/s"
1607
+ else:
1608
+ curl_units = f"({u_units})/rad" if u_units else "1/rad"
1609
+
1528
1610
  # Create the result UxDataArray
1529
1611
  curl_da = UxDataArray(
1530
1612
  curl_values,
1531
1613
  dims=self.dims,
1532
1614
  attrs={
1533
1615
  "long_name": f"Curl of ({self.name}, {other.name})",
1534
- "units": "1/s"
1535
- if "units" not in self.attrs
1536
- else f"({self.attrs.get('units', '1')})/m",
1616
+ "units": curl_units,
1537
1617
  "description": "Curl of vector field computed as ∂v/∂x - ∂u/∂y",
1538
1618
  },
1539
1619
  uxgrid=self.uxgrid,
@@ -1619,6 +1699,65 @@ class UxDataArray(xr.DataArray):
1619
1699
 
1620
1700
  return divergence_da
1621
1701
 
1702
+ def scalardotgradient(self, v: "UxDataArray", q: "UxDataArray") -> "UxDataArray":
1703
+ """
1704
+ Compute the dot product between a vector field and the gradient of a scalar field.
1705
+
1706
+ Parameters
1707
+ ----------
1708
+ v : UxDataArray
1709
+ The meridional component of the vector field. ``self`` is treated as
1710
+ the zonal component.
1711
+ q : UxDataArray
1712
+ Scalar field whose gradient is dotted with the vector field.
1713
+
1714
+ Returns
1715
+ -------
1716
+ scalar_dot_gradient : UxDataArray
1717
+ Dot product ``self * dq/dx + v * dq/dy``.
1718
+ """
1719
+ if not isinstance(v, UxDataArray):
1720
+ raise TypeError("v must be a UxDataArray")
1721
+
1722
+ if not isinstance(q, UxDataArray):
1723
+ raise TypeError("q must be a UxDataArray")
1724
+
1725
+ if self.uxgrid != v.uxgrid or self.uxgrid != q.uxgrid:
1726
+ raise ValueError("All UxDataArrays must have the same grid")
1727
+
1728
+ if self.dims != v.dims or self.dims != q.dims:
1729
+ raise ValueError("All UxDataArrays must have the same dimensions")
1730
+
1731
+ if self.ndim > 1:
1732
+ raise ValueError(
1733
+ "Scalar dot gradient currently requires 1D face-centered data. "
1734
+ "Consider selecting a single slice before computing."
1735
+ )
1736
+
1737
+ if not (self._face_centered() and v._face_centered() and q._face_centered()):
1738
+ raise ValueError(
1739
+ "Computing the scalar dot gradient is only supported for face-centered data variables."
1740
+ )
1741
+
1742
+ # Validate coordinate alignment up-front so a misaligned input fails
1743
+ # before the (potentially expensive) gradient call.
1744
+ u_aligned, v_aligned, q_aligned = xr.align(self, v, q, join="exact", copy=False)
1745
+
1746
+ q_gradient = q_aligned.gradient()
1747
+ q_zonal = q_gradient["zonal_gradient"]
1748
+ q_meridional = q_gradient["meridional_gradient"]
1749
+
1750
+ scalar_dot_gradient = (u_aligned * q_zonal) + (v_aligned * q_meridional)
1751
+ scalar_dot_gradient.name = "scalar_dot_gradient"
1752
+ scalar_dot_gradient.attrs.update(
1753
+ {
1754
+ "long_name": "scalar dot gradient",
1755
+ "description": "Dot product u * (dq/dx) + v * (dq/dy).",
1756
+ }
1757
+ )
1758
+
1759
+ return UxDataArray(scalar_dot_gradient, uxgrid=self.uxgrid)
1760
+
1622
1761
  def difference(self, destination: str | None = "edge"):
1623
1762
  """Computes the absolute difference of a data variable.
1624
1763
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import sys
5
5
  from html import escape
6
- from typing import IO, Any, Mapping
6
+ from typing import IO, Any, Hashable, Mapping
7
7
  from warnings import warn
8
8
 
9
9
  import numpy as np
@@ -621,11 +621,32 @@ class UxDataset(xr.Dataset):
621
621
 
622
622
  return integral
623
623
 
624
- def to_array(self) -> UxDataArray:
625
- """Override to make the result an instance of
626
- ``uxarray.UxDataArray``."""
624
+ def to_array(
625
+ self,
626
+ dim: Hashable = "variable",
627
+ name: Hashable = None,
628
+ ) -> UxDataArray:
629
+ """Convert this ``uxarray.UxDataset`` into a ``uxarray.UxDataArray``,
630
+ attaching this UxDataset's uxgrid to the result.
631
+
632
+ Similarly to xarray.Dataset.to_array(), the data variables will be
633
+ broadcast against each other and stacked along the first axis of
634
+ the new array. All coordinates of this dataset will remain coordinates.
635
+
636
+ Parameters
637
+ ----------
638
+ dim : Hashable, optional
639
+ Name of the new dimension. Defaults to "variable"
640
+ name : Hashable or None, optional
641
+ Name of the new data array.
642
+
643
+ Returns
644
+ -------
645
+ UxDataArray
646
+ The ``uxarray.UxDataset`` represented as a ``uxarray.UxDataArray``
647
+ """
627
648
 
628
- xarr = super().to_array()
649
+ xarr = super().to_array(dim=dim, name=name)
629
650
  return UxDataArray(xarr, uxgrid=self.uxgrid)
630
651
 
631
652
  def to_xarray(self, grid_format: str = "UGRID") -> xr.Dataset:
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
  from numba import njit, prange
3
5
 
@@ -88,7 +90,7 @@ def _check_node_on_boundary_and_gather_node_neighbors(
88
90
  return bool_bdy, node_neighbors[0:num_node_neighbors]
89
91
 
90
92
 
91
- def _compute_gradient(data):
93
+ def _compute_gradient(data, scale_by_radius=True):
92
94
  from uxarray import UxDataArray
93
95
 
94
96
  uxgrid = data.uxgrid
@@ -186,15 +188,38 @@ def _compute_gradient(data):
186
188
  "Computing the gradient is only supported for face-centered data variables."
187
189
  )
188
190
 
191
+ has_sphere_radius = "sphere_radius" in uxgrid._ds.attrs
192
+ if scale_by_radius:
193
+ if has_sphere_radius:
194
+ radius = uxgrid.sphere_radius
195
+ grad_zonal = grad_zonal / radius
196
+ grad_meridional = grad_meridional / radius
197
+ else:
198
+ warnings.warn(
199
+ "scale_by_radius=True but the grid has no 'sphere_radius' "
200
+ "attribute; result is left on the unit sphere. Set "
201
+ "uxgrid.sphere_radius or pass scale_by_radius=False.",
202
+ UserWarning,
203
+ stacklevel=2,
204
+ )
205
+
206
+ base_units = data.attrs.get("units", "")
207
+ if scale_by_radius and has_sphere_radius:
208
+ grad_units = f"{base_units}/m" if base_units else "1/m"
209
+ else:
210
+ grad_units = f"{base_units}/rad" if base_units else "1/rad"
211
+
189
212
  # Zonal
190
213
  grad_zonal_da = UxDataArray(
191
214
  data=grad_zonal, name="zonal_gradient", dims=data.dims, uxgrid=uxgrid
192
215
  )
216
+ grad_zonal_da.attrs["units"] = grad_units
193
217
 
194
218
  # Meridional
195
219
  grad_meridional_da = UxDataArray(
196
220
  data=grad_meridional, name="meridional_gradient", dims=data.dims, uxgrid=uxgrid
197
221
  )
222
+ grad_meridional_da.attrs["units"] = grad_units
198
223
 
199
224
  return grad_zonal_da, grad_meridional_da
200
225