uxarray 2026.9.0__tar.gz → 2026.9.1__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 (105) hide show
  1. {uxarray-2026.9.0 → uxarray-2026.9.1}/PKG-INFO +19 -16
  2. {uxarray-2026.9.0 → uxarray-2026.9.1}/pyproject.toml +31 -16
  3. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/dataarray.py +55 -41
  4. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/dataset.py +65 -36
  5. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/gradient.py +1 -1
  6. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/utils.py +20 -5
  7. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/cross_sections/sample.py +3 -0
  8. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/errors.py +7 -0
  9. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/angles.py +1 -1
  10. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/arcs.py +5 -3
  11. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/area.py +6 -2
  12. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/bounds.py +6 -5
  13. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/connectivity.py +5 -5
  14. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/coordinates.py +7 -5
  15. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/dual.py +1 -1
  16. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/geometry.py +42 -9
  17. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/grid.py +108 -49
  18. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/integrate.py +8 -4
  19. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/neighbors.py +61 -67
  20. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/point_in_face.py +1 -1
  21. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/slice.py +6 -2
  22. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/utils.py +5 -2
  23. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_esmf.py +10 -1
  24. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_geopandas.py +2 -1
  25. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_healpix.py +3 -0
  26. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_scrip.py +323 -28
  27. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_ugrid.py +61 -2
  28. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/plot/accessor.py +10 -2
  29. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/plot/matplotlib.py +2 -0
  30. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/plot/utils.py +6 -0
  31. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/bilinear.py +1 -1
  32. uxarray-2026.9.1/uxarray/utils/coords.py +212 -0
  33. uxarray-2026.9.1/uxarray/utils/imports.py +366 -0
  34. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray.egg-info/PKG-INFO +19 -16
  35. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray.egg-info/SOURCES.txt +1 -0
  36. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray.egg-info/requires.txt +21 -15
  37. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray.egg-info/scm_file_list.json +10 -0
  38. uxarray-2026.9.1/uxarray.egg-info/scm_version.json +8 -0
  39. uxarray-2026.9.0/uxarray/utils/coords.py +0 -86
  40. uxarray-2026.9.0/uxarray.egg-info/scm_version.json +0 -8
  41. {uxarray-2026.9.0 → uxarray-2026.9.1}/.codecov.yml +0 -0
  42. {uxarray-2026.9.0 → uxarray-2026.9.1}/.git_archival.txt +0 -0
  43. {uxarray-2026.9.0 → uxarray-2026.9.1}/.gitattributes +0 -0
  44. {uxarray-2026.9.0 → uxarray-2026.9.1}/CITATION.cff +0 -0
  45. {uxarray-2026.9.0 → uxarray-2026.9.1}/CODE_OF_CONDUCT.md +0 -0
  46. {uxarray-2026.9.0 → uxarray-2026.9.1}/CONTRIBUTING.md +0 -0
  47. {uxarray-2026.9.0 → uxarray-2026.9.1}/INSTALLATION.md +0 -0
  48. {uxarray-2026.9.0 → uxarray-2026.9.1}/LICENSE +0 -0
  49. {uxarray-2026.9.0 → uxarray-2026.9.1}/MANIFEST.in +0 -0
  50. {uxarray-2026.9.0 → uxarray-2026.9.1}/README.md +0 -0
  51. {uxarray-2026.9.0 → uxarray-2026.9.1}/build.sh +0 -0
  52. {uxarray-2026.9.0 → uxarray-2026.9.1}/setup.cfg +0 -0
  53. {uxarray-2026.9.0 → uxarray-2026.9.1}/setup.py +0 -0
  54. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/__init__.py +0 -0
  55. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/constants.py +0 -0
  56. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/conventions/__init__.py +0 -0
  57. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/conventions/descriptors.py +0 -0
  58. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/conventions/ugrid.py +0 -0
  59. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/__init__.py +0 -0
  60. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/accessors.py +0 -0
  61. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/aggregation.py +0 -0
  62. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/api.py +0 -0
  63. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/core/zonal.py +0 -0
  64. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/cross_sections/__init__.py +0 -0
  65. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/cross_sections/dataarray_accessor.py +0 -0
  66. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/cross_sections/grid_accessor.py +0 -0
  67. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/formatting_html.py +0 -0
  68. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/__init__.py +0 -0
  69. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/intersections.py +0 -0
  70. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/grid/validation.py +0 -0
  71. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/__init__.py +0 -0
  72. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_delaunay.py +0 -0
  73. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_exodus.py +0 -0
  74. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_fesom2.py +0 -0
  75. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_geos.py +0 -0
  76. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_icon.py +0 -0
  77. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_mpas.py +0 -0
  78. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_structured.py +0 -0
  79. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_topology.py +0 -0
  80. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_vertices.py +0 -0
  81. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/_voronoi.py +0 -0
  82. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/io/utils.py +0 -0
  83. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/plot/__init__.py +0 -0
  84. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/plot/constants.py +0 -0
  85. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/__init__.py +0 -0
  86. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/accessor.py +0 -0
  87. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/apply_weights.py +0 -0
  88. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/inverse_distance_weighted.py +0 -0
  89. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/nearest_neighbor.py +0 -0
  90. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/spatial_coords_remap.py +0 -0
  91. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/structured.py +0 -0
  92. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/utils.py +0 -0
  93. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/weights.py +0 -0
  94. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/remap/yac.py +0 -0
  95. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/subset/__init__.py +0 -0
  96. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/subset/dataarray_accessor.py +0 -0
  97. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/subset/grid_accessor.py +0 -0
  98. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/tutorial/__init__.py +0 -0
  99. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/tutorial/registry.py +0 -0
  100. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/utils/__init__.py +0 -0
  101. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/utils/computing.py +0 -0
  102. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/utils/numba_math.py +0 -0
  103. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray/utils/warnings.py +0 -0
  104. {uxarray-2026.9.0 → uxarray-2026.9.1}/uxarray.egg-info/dependency_links.txt +0 -0
  105. {uxarray-2026.9.0 → uxarray-2026.9.1}/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.9.0
3
+ Version: 2026.9.1
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
@@ -221,37 +221,40 @@ Requires-Python: >=3.11
221
221
  Description-Content-Type: text/markdown
222
222
  License-File: LICENSE
223
223
  Requires-Dist: antimeridian
224
- Requires-Dist: cartopy
225
224
  Requires-Dist: dask[dataframe]
226
- Requires-Dist: datashader
227
- Requires-Dist: geoviews
228
- Requires-Dist: holoviews
229
- Requires-Dist: matplotlib<3.11
230
- Requires-Dist: matplotlib-inline
231
225
  Requires-Dist: netcdf4
232
226
  Requires-Dist: numba>=0.63
233
227
  Requires-Dist: numpy
234
228
  Requires-Dist: pandas
235
- Requires-Dist: pyarrow
236
- Requires-Dist: requests
237
229
  Requires-Dist: scikit-learn
238
230
  Requires-Dist: scipy
239
231
  Requires-Dist: shapely
240
- Requires-Dist: spatialpandas
241
- Requires-Dist: geopandas>=1.0
242
232
  Requires-Dist: xarray
243
- Requires-Dist: hvplot
244
- Requires-Dist: healpix
245
233
  Requires-Dist: polars
246
- Requires-Dist: pyproj
234
+ Provides-Extra: all
235
+ Requires-Dist: uxarray[dev,geo,viz]; extra == "all"
247
236
  Provides-Extra: complete
248
- Requires-Dist: uxarray[dev]; extra == "complete"
237
+ Requires-Dist: uxarray[dev,geo,viz]; extra == "complete"
249
238
  Provides-Extra: dev
239
+ Requires-Dist: asv; extra == "dev"
240
+ Requires-Dist: pooch; extra == "dev"
250
241
  Requires-Dist: pre_commit; extra == "dev"
251
242
  Requires-Dist: pytest; extra == "dev"
252
243
  Requires-Dist: pytest-cov; extra == "dev"
253
244
  Requires-Dist: ruff; extra == "dev"
254
- Requires-Dist: asv; extra == "dev"
245
+ Provides-Extra: geo
246
+ Requires-Dist: cartopy; extra == "geo"
247
+ Requires-Dist: geopandas>=1.0; extra == "geo"
248
+ Requires-Dist: healpix; extra == "geo"
249
+ Requires-Dist: pyproj; extra == "geo"
250
+ Requires-Dist: spatialpandas; extra == "geo"
251
+ Provides-Extra: viz
252
+ Requires-Dist: cartopy<0.26; extra == "viz"
253
+ Requires-Dist: datashader; extra == "viz"
254
+ Requires-Dist: matplotlib<3.11; extra == "viz"
255
+ Requires-Dist: geoviews; extra == "viz"
256
+ Requires-Dist: holoviews; extra == "viz"
257
+ Requires-Dist: hvplot; extra == "viz"
255
258
  Dynamic: license-file
256
259
 
257
260
 
@@ -22,41 +22,53 @@ requires-python = ">=3.11"
22
22
  # minimal dependencies start
23
23
  dependencies = [
24
24
  "antimeridian",
25
- "cartopy",
26
25
  "dask[dataframe]",
27
- "datashader",
28
- "geoviews",
29
- "holoviews",
30
- "matplotlib<3.11", # matplotlib 3.11.0 breaks some plots but <3.11 is fine; see issue #1542.
31
- "matplotlib-inline",
32
26
  "netcdf4",
33
27
  "numba>=0.63", # 0.63 is the first release supporting Python 3.14 (3.10 <= py < 3.15). See #1561.
34
28
  "numpy",
35
29
  "pandas",
36
- "pyarrow",
37
- "requests",
38
30
  "scikit-learn",
39
31
  "scipy",
40
32
  "shapely",
41
- "spatialpandas",
42
- "geopandas>=1.0", # 1.0 is the first release requiring pyogrio, the file-IO backend read_file needs. See #1693.
43
33
  "xarray",
44
- "hvplot",
45
- "healpix",
46
34
  "polars",
47
- "pyproj"
48
35
  ]
49
36
  # minimal dependencies end
50
37
 
51
38
  [project.optional-dependencies]
52
- complete = ["uxarray[dev]"]
53
- dev = ['pre_commit', 'pytest', 'pytest-cov', 'ruff', 'asv']
39
+ all = ["uxarray[dev,geo,viz]"]
40
+ complete = ["uxarray[dev,geo,viz]"] # alias to "all"
41
+ dev = [
42
+ 'asv',
43
+ 'pooch',
44
+ 'pre_commit',
45
+ 'pytest',
46
+ 'pytest-cov',
47
+ 'ruff',
48
+ ]
49
+ geo = [
50
+ "cartopy", # in both geo and viz because it is needed by to_geodataframe() and by plotting routines.
51
+ "geopandas>=1.0", # 1.0 is the first release requiring pyogrio, the file-IO backend read_file needs. See #1693.
52
+ "healpix",
53
+ "pyproj",
54
+ "spatialpandas",
55
+ ]
56
+ viz = [
57
+ "cartopy<0.26", # cartopy 0.26.0 breaks some geoviews plots; see issue #1780.
58
+ # (cartopy pin intentionally here but not in "geo" extras because geoviews is only in "viz".)
59
+ "datashader",
60
+ "matplotlib<3.11", # matplotlib 3.11.0 breaks some plots but <3.11 is fine; see issue #1542.
61
+ "geoviews", # no version pin; newer should support any cartopy; see holoviz/geoviews#884.
62
+ "holoviews",
63
+ "hvplot",
64
+ ]
54
65
 
55
66
  [project.urls]
56
67
  Documentation = "https://uxarray.readthedocs.io/"
57
68
  Source = "https://github.com/UXARRAY/uxarray"
58
69
  Tracker = "https://github.com/UXARRAY/uxarray/issues"
59
-
70
+ # please keep [project.urls] immediately after [project.optional-dependencies],
71
+ # or, if rearranging sections, make sure to edit installation.rst accordingly.
60
72
 
61
73
  [build-system]
62
74
  build-backend = "setuptools.build_meta"
@@ -87,3 +99,6 @@ known-first-party = ["uxarray"]
87
99
 
88
100
  [tool.ruff.format]
89
101
  docstring-code-format = true
102
+
103
+ [tool.pytest.ini_options]
104
+ testpaths = ["test"] # (intentionally excludes test_optional_deps)
@@ -11,7 +11,6 @@ from xarray.core import dtypes
11
11
  from xarray.core.options import OPTIONS
12
12
  from xarray.core.utils import UncachedAccessor
13
13
 
14
- import uxarray
15
14
  from uxarray.constants import GRID_DIMS
16
15
  from uxarray.core.aggregation import _uxda_grid_aggregate
17
16
  from uxarray.core.gradient import (
@@ -45,7 +44,13 @@ from uxarray.io._healpix import get_zoom_from_cells
45
44
  from uxarray.plot.accessor import UxDataArrayPlotAccessor
46
45
  from uxarray.remap.accessor import RemapAccessor
47
46
  from uxarray.subset import DataArraySubsetAccessor
48
- from uxarray.utils.coords import _preserve_valid_coords
47
+ from uxarray.utils.coords import (
48
+ _assert_grid_dim_coord_consistent_if_in_both,
49
+ _assign_grid_dim_indexer_coords_if_appropriate,
50
+ _crash_if_1d_xarray_indexer_dim_in_uxarray_obj,
51
+ _preserve_valid_coords,
52
+ )
53
+ from uxarray.utils.imports import _raise_hint_if_optional_deps_missing
49
54
 
50
55
  if TYPE_CHECKING:
51
56
  import cartopy.crs as ccrs
@@ -183,26 +188,14 @@ class UxDataArray(xr.DataArray):
183
188
 
184
189
  @property
185
190
  def data_mapping(self):
186
- """Returns which unstructured grid a data variable is mapped to."""
187
- if self._face_centered():
188
- return "faces"
189
- elif self._edge_centered():
190
- return "edges"
191
- elif self._node_centered():
192
- return "nodes"
193
- else:
194
- return None
195
-
196
- @property
197
- def data_location(self):
198
- """Returns where on the grid the data variable is stored.
191
+ """Returns which grid element a data variable is mapped to.
199
192
 
200
- The location is inferred from the grid dimension present in the data
201
- variable, using UGRID-style names:
193
+ The mapping is inferred from the grid dimension present in the data
194
+ variable:
202
195
 
203
- - ``"face_centered"`` if the data contains the ``n_face`` dimension
204
- - ``"node_centered"`` if the data contains the ``n_node`` dimension
205
- - ``"edge_centered"`` if the data contains the ``n_edge`` dimension
196
+ - ``"faces"`` if the data contains the ``n_face`` dimension
197
+ - ``"edges"`` if the data contains the ``n_edge`` dimension
198
+ - ``"nodes"`` if the data contains the ``n_node`` dimension
206
199
  - ``None`` if the data is not mapped to the grid
207
200
 
208
201
  Notes
@@ -215,15 +208,14 @@ class UxDataArray(xr.DataArray):
215
208
  Returns
216
209
  -------
217
210
  str or None
218
- One of ``"face_centered"``, ``"node_centered"``,
219
- ``"edge_centered"``, or ``None``.
211
+ One of ``"faces"``, ``"edges"``, ``"nodes"``, or ``None``.
220
212
  """
221
213
  if self._face_centered():
222
- return "face_centered"
223
- elif self._node_centered():
224
- return "node_centered"
214
+ return "faces"
225
215
  elif self._edge_centered():
226
- return "edge_centered"
216
+ return "edges"
217
+ elif self._node_centered():
218
+ return "nodes"
227
219
  else:
228
220
  return None
229
221
 
@@ -320,7 +312,7 @@ class UxDataArray(xr.DataArray):
320
312
 
321
313
  else:
322
314
  raise DataCenteringError(
323
- f"to_geodataframe() expects face_centered data; got {self.data_location} data "
315
+ f"to_geodataframe() expects data mapped to faces; got data_mapping={self.data_mapping!r} "
324
316
  f"(with sizes={dict(**self.sizes)}). Consider running "
325
317
  "``UxDataArray.topological_mean(destination='face')`` to aggregate the data onto faces."
326
318
  )
@@ -480,6 +472,7 @@ class UxDataArray(xr.DataArray):
480
472
  >>> ax.imshow(raster, origin="lower", extent=ax.get_xlim() + ax.get_ylim())
481
473
 
482
474
  """
475
+ _raise_hint_if_optional_deps_missing("cartopy")
483
476
  from cartopy.mpl.geoaxes import GeoAxes
484
477
 
485
478
  from uxarray.constants import INT_DTYPE
@@ -524,6 +517,7 @@ class UxDataArray(xr.DataArray):
524
517
 
525
518
  if _is_default_extent():
526
519
  try:
520
+ _raise_hint_if_optional_deps_missing("cartopy")
527
521
  import cartopy.crs as ccrs
528
522
 
529
523
  lon_min = float(self.uxgrid.node_lon.min(skipna=True).values)
@@ -596,8 +590,10 @@ class UxDataArray(xr.DataArray):
596
590
  -------
597
591
  uxds: UxDataSet
598
592
  """
593
+ from uxarray.core.dataset import UxDataset
594
+
599
595
  xrds = super().to_dataset(dim=dim, name=name, promote_attrs=promote_attrs)
600
- uxds = uxarray.core.dataset.UxDataset(xrds, uxgrid=self._uxgrid)
596
+ uxds = UxDataset(xrds, uxgrid=self._uxgrid)
601
597
 
602
598
  return uxds
603
599
 
@@ -645,8 +641,8 @@ class UxDataArray(xr.DataArray):
645
641
 
646
642
  elif not self._face_centered():
647
643
  raise DataCenteringError(
648
- "Integration of non-face_centered data is not yet supported. "
649
- f"(Got {self.data_location} data with sizes={dict(**self.sizes)})"
644
+ "Integration of data not mapped to faces is not yet supported. "
645
+ f"(Got data_mapping={self.data_mapping!r} with sizes={dict(**self.sizes)})"
650
646
  )
651
647
 
652
648
  else:
@@ -1796,7 +1792,7 @@ class UxDataArray(xr.DataArray):
1796
1792
  # Spherical metric term, the companion of the one in curl(). Omitting
1797
1793
  # it is only valid on a plane.
1798
1794
  tan_lat = np.tan(np.deg2rad(self.uxgrid.face_lat.values))
1799
- metric = other.values * tan_lat
1795
+ metric = other.data * tan_lat
1800
1796
  if scale_by_radius and "sphere_radius" in self.uxgrid._ds.attrs:
1801
1797
  metric = metric / self.uxgrid._ds.attrs["sphere_radius"]
1802
1798
  divergence = divergence - metric
@@ -2006,7 +2002,10 @@ class UxDataArray(xr.DataArray):
2006
2002
  using n_edge=7 selects just the two faces touching edge 7. For data on 'n_face',
2007
2003
  the result would have 'n_face' with just those two faces. For data on 'n_edge',
2008
2004
  the result would have 'n_edge' with all edges located on either of those two faces.
2005
+
2009
2006
  Grid dimension indexers cannot have more than 1 dimension (such as a 2D DataArray).
2007
+ Grid dimensions are never renamed (even if indexed by 1D DataArray with different dim name).
2008
+ Grid dimension indexer cannot have a non-grid dimension which exists in the original UxDataArray.
2010
2009
 
2011
2010
  Parameters
2012
2011
  ----------
@@ -2042,6 +2041,10 @@ class UxDataArray(xr.DataArray):
2042
2041
  -------
2043
2042
  UxDataArray
2044
2043
  A new UxDataArray indexed according to `indexers` and updated grid if applicable.
2044
+ If indexer DataArrays have coordinates that do not conflict with
2045
+ this object, then these coordinates will be attached,
2046
+ except that 1D coordinates of indexers applied along a grid dimension will
2047
+ only be included if it is 'n_face' and the data also has 'n_face' dimension.
2045
2048
 
2046
2049
  Raises
2047
2050
  ------
@@ -2071,22 +2074,27 @@ class UxDataArray(xr.DataArray):
2071
2074
  indexers = indexers.copy() # don't modify the original dict
2072
2075
  grid_indexer = indexers.pop(grid_dim)
2073
2076
 
2077
+ _crash_if_1d_xarray_indexer_dim_in_uxarray_obj(self, grid_dim, grid_indexer)
2078
+
2074
2079
  sliced_grid = self.uxgrid.isel(
2075
2080
  **{grid_dim: grid_indexer}, inverse_indices=inverse_indices
2076
2081
  )
2077
2082
 
2078
- da = self._slice_from_grid(sliced_grid)
2083
+ result = self._slice_from_grid(sliced_grid)
2084
+
2085
+ result = _assign_grid_dim_indexer_coords_if_appropriate(
2086
+ result, grid_dim, grid_indexer
2087
+ )
2079
2088
 
2080
2089
  # if there are any remaining indexers, apply them
2081
2090
  if indexers:
2082
- xarr = super(UxDataArray, da).isel(
2091
+ result = super(UxDataArray, result).isel(
2083
2092
  indexers=indexers, drop=drop, missing_dims=missing_dims
2084
2093
  )
2085
2094
  # re‐wrap so the grid sticks around
2086
- return type(self)(xarr, uxgrid=sliced_grid)
2095
+ result = type(self)(result, uxgrid=sliced_grid)
2087
2096
 
2088
- # no other dims, return the grid‐sliced da
2089
- return da
2097
+ return result
2090
2098
  else: # len(grid_dims)>1; _validate_indexers should have crashed.
2091
2099
  raise AssertionError("internal implementation error if reached this line")
2092
2100
 
@@ -2106,7 +2114,10 @@ class UxDataArray(xr.DataArray):
2106
2114
  using n_edge=7 selects just the two faces touching edge 7. For data on 'n_face',
2107
2115
  the result would have 'n_face' with just those two faces. For data on 'n_edge',
2108
2116
  the result would have 'n_edge' with all edges located on either of those two faces.
2117
+
2109
2118
  Grid dimension indexers cannot have more than 1 dimension (such as a 2D DataArray).
2119
+ Grid dimensions are never renamed (even if indexed by 1D DataArray with different dim name).
2120
+ Grid dimension indexer cannot have a non-grid dimension which exists in the original UxDataArray.
2110
2121
 
2111
2122
  By default, grid dims do not have coordinates assigned. But, if they have
2112
2123
  been assigned, `.sel()` respects them in the intuitive way. For example,
@@ -2167,7 +2178,8 @@ class UxDataArray(xr.DataArray):
2167
2178
  and the uxgrid indexed appropriately as well, if indexing any grid dim.
2168
2179
  If indexer DataArrays have coordinates that do not conflict with
2169
2180
  this object, then these coordinates will be attached,
2170
- except for indexers along a grid dimension (see issue #1712).
2181
+ except that 1D coordinates of indexers applied along a grid dimension will
2182
+ only be included if it is 'n_face' and the data also has 'n_face' dimension.
2171
2183
  In general, the result's data will be a view of the data in this array,
2172
2184
  unless indexing along a grid dimension or otherwise
2173
2185
  triggering vectorized indexing by using an array indexer,
@@ -2213,6 +2225,10 @@ class UxDataArray(xr.DataArray):
2213
2225
  # offload the grid-indexing work to isel():
2214
2226
  result = self.isel({grid_dim: grid_indices}, drop=drop)
2215
2227
 
2228
+ # special case: if grid_dim in indexer and result.coords, ensure consistency.
2229
+ # (all other coords' consistency checks already occurred in isel().)
2230
+ _assert_grid_dim_coord_consistent_if_in_both(result, grid_dim, grid_indexer)
2231
+
2216
2232
  # index by other dims if any remain:
2217
2233
  ds = result.to_xarray().sel(
2218
2234
  indexers=indexers, # (grid_dim indexer was popped)
@@ -2321,7 +2337,7 @@ class UxDataArray(xr.DataArray):
2321
2337
  "Data variable must be either node, edge, or face centered."
2322
2338
  )
2323
2339
 
2324
- return UxDataArray(da_sliced, uxgrid=sliced_grid)
2340
+ return type(self)(da_sliced, uxgrid=sliced_grid)
2325
2341
 
2326
2342
  def get_dual(self):
2327
2343
  """Compute the dual mesh for a data array, returns a new data array
@@ -2359,9 +2375,7 @@ class UxDataArray(xr.DataArray):
2359
2375
  dims = [dim_map.get(dim, dim) for dim in self.dims]
2360
2376
 
2361
2377
  # Construct the new data array
2362
- uxda = uxarray.UxDataArray(
2363
- uxgrid=dual, data=self.data, dims=dims, name=self.name
2364
- )
2378
+ uxda = type(self)(uxgrid=dual, data=self.data, dims=dims, name=self.name)
2365
2379
 
2366
2380
  return uxda
2367
2381
 
@@ -11,7 +11,7 @@ from xarray.core import dtypes
11
11
  from xarray.core.options import OPTIONS
12
12
  from xarray.core.utils import UncachedAccessor
13
13
 
14
- import uxarray
14
+ from uxarray.constants import GRID_DIMS
15
15
  from uxarray.core.dataarray import UxDataArray
16
16
  from uxarray.core.utils import (
17
17
  _map_dims_to_ugrid,
@@ -28,6 +28,11 @@ from uxarray.grid.validation import _check_duplicate_nodes_indices
28
28
  from uxarray.io._healpix import get_zoom_from_cells
29
29
  from uxarray.plot.accessor import UxDatasetPlotAccessor
30
30
  from uxarray.remap.accessor import RemapAccessor
31
+ from uxarray.utils.coords import (
32
+ _assert_grid_dim_coord_consistent_if_in_both,
33
+ _assign_grid_dim_indexer_coords_if_appropriate,
34
+ _crash_if_1d_xarray_indexer_dim_in_uxarray_obj,
35
+ )
31
36
 
32
37
 
33
38
  class UxDataset(xr.Dataset):
@@ -387,33 +392,39 @@ class UxDataset(xr.Dataset):
387
392
 
388
393
  return cls.from_xarray(ds, uxgrid, {face_dim: "n_face"})
389
394
 
390
- def _slice_dataset_from_grid(self, sliced_grid, grid_dim: str, grid_indexer):
395
+ def _slice_from_grid(self, sliced_grid):
396
+ """returns UxDataset based on slicing self according to sliced_grid.
397
+ sliced_grid should be a ``Grid`` which came directly from self.uxgrid.isel(...)
398
+ (or from slicing something equal to self.uxgrid), else behavior is undefined.
399
+ """
391
400
  data_vars = {}
392
401
  for name, da in self.data_vars.items():
393
- if grid_dim in da.dims:
394
- if hasattr(da, "_slice_from_grid"):
395
- data_vars[name] = da._slice_from_grid(sliced_grid)
396
- else:
397
- data_vars[name] = da.isel({grid_dim: grid_indexer})
402
+ if hasattr(da, "_slice_from_grid") and any(
403
+ dim in da.dims for dim in GRID_DIMS
404
+ ):
405
+ data_vars[name] = da._slice_from_grid(sliced_grid)
398
406
  else:
399
407
  data_vars[name] = da
400
408
 
401
- coords = {}
402
- for cname, cda in self.coords.items():
403
- if grid_dim in cda.dims:
404
- # Prefer authoritative coords from the sliced grid if available
405
- replacement = getattr(sliced_grid, cname, None)
406
- coords[cname] = (
407
- replacement
408
- if replacement is not None
409
- else cda.isel({grid_dim: grid_indexer})
410
- )
411
- else:
412
- coords[cname] = cda
413
-
414
- ds = xr.Dataset(data_vars=data_vars, coords=coords, attrs=self.attrs)
409
+ # Also account for any coords which aren't attached to any data_var:
410
+ bonus_coords = {}
411
+ for coord in self.coords:
412
+ for data_var in data_vars.values():
413
+ if coord in data_var.coords:
414
+ break
415
+ else: # didn't break
416
+ da = self.coords[coord]
417
+ if hasattr(da, "_slice_from_grid") and any(
418
+ dim in da.dims for dim in GRID_DIMS
419
+ ):
420
+ bonus_coords[coord] = da._slice_from_grid(sliced_grid)
421
+ else:
422
+ bonus_coords[coord] = da
415
423
 
416
- return ds
424
+ ds_sliced = xr.Dataset(
425
+ data_vars=data_vars, coords=bonus_coords, attrs=self.attrs
426
+ )
427
+ return type(self)(ds_sliced, uxgrid=sliced_grid)
417
428
 
418
429
  def isel(
419
430
  self,
@@ -435,7 +446,10 @@ class UxDataset(xr.Dataset):
435
446
  using n_edge=7 selects just the two faces touching edge 7. For data on 'n_face',
436
447
  the result would have 'n_face' with just those two faces. For data on 'n_edge',
437
448
  the result would have 'n_edge' with all edges located on either of those two faces.
449
+
438
450
  Grid dimension indexers cannot have more than 1 dimension (such as a 2D DataArray).
451
+ Grid dimensions are never renamed (even if indexed by 1D DataArray with different dim name).
452
+ Grid dimension indexer cannot have a non-grid dimension which exists in the original UxDataset.
439
453
 
440
454
  Parameters
441
455
  ----------
@@ -471,6 +485,10 @@ class UxDataset(xr.Dataset):
471
485
  -------
472
486
  UxDataset
473
487
  A new UxDataset indexed according to `indexers` and updated grid if applicable.
488
+ If indexer DataArrays have coordinates that do not conflict with
489
+ this object, then these coordinates will be attached,
490
+ except that 1D coordinates of indexers applied along a grid dimension will
491
+ only be included if it is 'n_face' and the data also has 'n_face' dimension.
474
492
 
475
493
  Raises
476
494
  ------
@@ -500,23 +518,27 @@ class UxDataset(xr.Dataset):
500
518
  indexers = indexers.copy() # don't modify the original dict
501
519
  grid_indexer = indexers.pop(grid_dim)
502
520
 
503
- # slice the grid
521
+ _crash_if_1d_xarray_indexer_dim_in_uxarray_obj(self, grid_dim, grid_indexer)
522
+
504
523
  sliced_grid = self.uxgrid.isel(
505
524
  **{grid_dim: grid_indexer}, inverse_indices=inverse_indices
506
525
  )
507
526
 
508
- ds = self._slice_dataset_from_grid(
509
- sliced_grid=sliced_grid,
510
- grid_dim=grid_dim,
511
- grid_indexer=grid_indexer,
527
+ result = self._slice_from_grid(sliced_grid)
528
+
529
+ result = _assign_grid_dim_indexer_coords_if_appropriate(
530
+ result, grid_dim, grid_indexer
512
531
  )
513
532
 
533
+ # if there are any remaining indexers, apply them
514
534
  if indexers:
515
- ds = xr.Dataset.isel(
516
- ds, indexers=indexers, drop=drop, missing_dims=missing_dims
535
+ result = super(UxDataset, result).isel(
536
+ indexers=indexers, drop=drop, missing_dims=missing_dims
517
537
  )
538
+ # re‐wrap so the grid sticks around
539
+ result = type(self)(result, uxgrid=sliced_grid)
518
540
 
519
- return type(self)(ds, uxgrid=sliced_grid)
541
+ return result
520
542
  else: # len(grid_dims)>1; _validate_indexers should have crashed.
521
543
  raise AssertionError("internal implementation error if reached this line")
522
544
 
@@ -537,7 +559,10 @@ class UxDataset(xr.Dataset):
537
559
  using n_edge=7 selects just the two faces touching edge 7. For data on 'n_face',
538
560
  the result would have 'n_face' with just those two faces. For data on 'n_edge',
539
561
  the result would have 'n_edge' with all edges located on either of those two faces.
562
+
540
563
  Grid dimension indexers cannot have more than 1 dimension (such as a 2D DataArray).
564
+ Grid dimensions are never renamed (even if indexed by 1D DataArray with different dim name).
565
+ Grid dimension indexer cannot have a non-grid dimension which exists in the original UxDataset.
541
566
 
542
567
  By default, grid dims do not have coordinates assigned. But, if they have
543
568
  been assigned, `.sel()` respects them in the intuitive way. For example,
@@ -566,7 +591,8 @@ class UxDataset(xr.Dataset):
566
591
  multi-index, the indexer may also be a dict-like object with keys
567
592
  matching index level names.
568
593
  If DataArrays are passed as indexers, xarray-style indexing will be
569
- carried out. See :ref:`indexing` for the details.
594
+ carried out (see :ref:`indexing` for the details),
595
+ with one exception: grid dimensions will never be renamed.
570
596
  One of indexers or indexers_kwargs must be provided.
571
597
  method : {None, "nearest", "pad", "ffill", "backfill", "bfill"}, optional
572
598
  Method to use for inexact matches:
@@ -599,7 +625,8 @@ class UxDataset(xr.Dataset):
599
625
  and the uxgrid indexed appropriately as well, if indexing any grid dim.
600
626
  If indexer DataArrays have coordinates that do not conflict with
601
627
  this object, then these coordinates will be attached,
602
- except for indexers along a grid dimension (see issue #1712).
628
+ except that 1D coordinates of indexers applied along a grid dimension will
629
+ only be included if it is 'n_face' and the data also has 'n_face' dimension.
603
630
  In general, each array's data will be a view of the array's data
604
631
  in this dataset, unless indexing along a grid dimension or otherwise
605
632
  triggering vectorized indexing by using an array indexer,
@@ -646,6 +673,10 @@ class UxDataset(xr.Dataset):
646
673
  # offload the grid-indexing work to isel():
647
674
  result = self.isel({grid_dim: grid_indices}, drop=drop)
648
675
 
676
+ # special case: if grid_dim in indexer and result.coords, ensure consistency.
677
+ # (all other coords' consistency checks already occurred in isel().)
678
+ _assert_grid_dim_coord_consistent_if_in_both(result, grid_dim, grid_indexer)
679
+
649
680
  # index by other dims if any remain:
650
681
  ds = result.to_xarray().sel(
651
682
  indexers=indexers, # (grid_dim indexer was popped)
@@ -921,7 +952,7 @@ class UxDataset(xr.Dataset):
921
952
  )
922
953
 
923
954
  # Initialize new dataset
924
- dataset = uxarray.UxDataset(uxgrid=dual)
955
+ dataset = type(self)(uxgrid=dual)
925
956
 
926
957
  # Dictionary to swap dimensions
927
958
  dim_map = {"n_face": "n_node", "n_node": "n_face"}
@@ -932,9 +963,7 @@ class UxDataset(xr.Dataset):
932
963
  dims = [dim_map.get(dim, dim) for dim in self[var].dims]
933
964
 
934
965
  # Construct the new data array
935
- uxda = uxarray.UxDataArray(
936
- uxgrid=dual, data=self[var].data, dims=dims, name=var
937
- )
966
+ uxda = UxDataArray(uxgrid=dual, data=self[var].data, dims=dims, name=var)
938
967
 
939
968
  # Add data array to dataset
940
969
  dataset[var] = uxda
@@ -350,7 +350,7 @@ def _dual_cell_area(sx, sy, sz, angles, n):
350
350
  return np.abs(area)
351
351
 
352
352
 
353
- @njit(cache=True, parallel=True)
353
+ @njit(cache=True, parallel=True, nogil=True)
354
354
  def _compute_gradients_on_faces(
355
355
  data,
356
356
  n_face,
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
  import xarray as xr
3
5
  from xarray.core.utils import either_dict_or_kwargs
@@ -186,10 +188,7 @@ def _resolve_coordinate_labels_to_indices(
186
188
  ):
187
189
  """returns indices which would be selected by coord_array.sel({dim: labels_to_sel}, ...)
188
190
  coord_array.isel({dim: result}) should be equivalent to coord_array.sel({dim: labels_to_sel}, ...).
189
-
190
- (Implementation here drops extra coordinates from any indexers,
191
- but if it is being applied to grid dims for sel() then it will produce behavior
192
- which is consistent with isel(), unless issue #1712 gets fixed.)
191
+ If labels_to_sel is an xr.DataArray, its coords/dims will also be attached to the result.
193
192
 
194
193
  dim: str
195
194
  dimension name to select along
@@ -203,10 +202,26 @@ def _resolve_coordinate_labels_to_indices(
203
202
  # (Maybe a more efficient implementation exists, but this is simple and gives correct results.)
204
203
  indices = xr.DataArray(np.arange(coord_array.sizes[dim]), dims=dim)
205
204
  _indices_coord_name = f"__{dim}_indices__" # just needs to be any unused name.
205
+ if _indices_coord_name in coord_array.coords:
206
+ warnings.warn(
207
+ f"Coordinate {_indices_coord_name!r} already exists in coord_array.coords "
208
+ "and will be overwritten, which may cause errors or subtly incorrect results..."
209
+ )
206
210
  if hasattr(coord_array, "to_xarray"): # convert to xarray to avoid recursive sel()
207
211
  coord_array = coord_array.to_xarray()
208
212
  coord_with_indices = coord_array.assign_coords({_indices_coord_name: indices})
209
213
  selected = coord_with_indices.sel(
210
214
  {dim: labels_to_sel}, method=method, tolerance=tolerance
211
215
  )
212
- return selected[_indices_coord_name].values # (return as np.ndarray, not DataArray)
216
+ result = selected[_indices_coord_name]
217
+ if isinstance(labels_to_sel, xr.DataArray):
218
+ # handle coords appropriately
219
+ result = result.drop_vars((dim, _indices_coord_name))
220
+ # (drop grid dim coords because the caller is expected to handle those directly;
221
+ # here the goal is just to properly propagate any other coords from labels_to_sel.)
222
+ result = result.rename(None) # no reason to keep the _indices_coord_name
223
+ # (and keeping it for longer could maybe cause confusing error later?)
224
+ else:
225
+ # drop all coords/name info which was added internally during this method.
226
+ result = result.values
227
+ return result
@@ -1,9 +1,12 @@
1
1
  import numpy as np
2
2
 
3
+ from uxarray.utils.imports import _raise_hint_if_optional_deps_missing
4
+
3
5
 
4
6
  def sample_geodesic(
5
7
  start: tuple[float, float], end: tuple[float, float], steps: int
6
8
  ) -> tuple[np.ndarray, np.ndarray]:
9
+ _raise_hint_if_optional_deps_missing("pyproj")
7
10
  from pyproj import Geod
8
11
 
9
12
  lon0, lat0 = start
@@ -38,3 +38,10 @@ class GridsMismatchError(ValueError):
38
38
 
39
39
  class YacNotAvailableError(RuntimeError):
40
40
  """Raised when the YAC backend is requested but unavailable."""
41
+
42
+
43
+ # # # ----- Miscellaneous Errors ----- # # #
44
+
45
+
46
+ class OptionalDependencyNotFoundError(ModuleNotFoundError):
47
+ """indicates functionality relies on a not-yet-installed optional dependency."""
@@ -8,7 +8,7 @@ from numba import njit, prange
8
8
  from uxarray.grid.utils import _numba_norm3, _small_angle_of_2_vectors
9
9
 
10
10
 
11
- @njit(cache=True, parallel=True)
11
+ @njit(cache=True, parallel=True, nogil=True)
12
12
  def _compute_face_node_angles_convex(
13
13
  node_x,
14
14
  node_y,