cf-xarray 0.10.5__tar.gz → 0.10.7__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 (96) hide show
  1. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/workflows/ci.yaml +5 -5
  2. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.pre-commit-config.yaml +5 -5
  3. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/PKG-INFO +2 -3
  4. cf_xarray-0.10.7/cf_xarray/_version.py +1 -0
  5. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/accessor.py +5 -5
  6. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/geometry.py +5 -3
  7. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/helpers.py +228 -3
  8. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_geometry.py +3 -1
  9. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_helpers.py +55 -1
  10. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray.egg-info/PKG-INFO +2 -3
  11. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/ci/doc.yml +1 -1
  12. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/conf.py +2 -2
  13. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/pyproject.toml +2 -3
  14. cf_xarray-0.10.5/cf_xarray/_version.py +0 -1
  15. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.binder/environment.yml +0 -0
  16. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.deepsource.toml +0 -0
  17. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/dependabot.yml +0 -0
  18. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/release.yml +0 -0
  19. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/workflows/parse_logs.py +0 -0
  20. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/workflows/pypi.yaml +0 -0
  21. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/workflows/testpypi-release.yaml +0 -0
  22. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.github/workflows/upstream-dev-ci.yaml +0 -0
  23. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.gitignore +0 -0
  24. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.readthedocs.yml +0 -0
  25. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/.tributors +0 -0
  26. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/CITATION.cff +0 -0
  27. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/LICENSE +0 -0
  28. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/README.rst +0 -0
  29. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/__init__.py +0 -0
  30. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/coding.py +0 -0
  31. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/criteria.py +0 -0
  32. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/datasets.py +0 -0
  33. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/formatting.py +0 -0
  34. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/groupers.py +0 -0
  35. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/options.py +0 -0
  36. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/parametric.py +0 -0
  37. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/py.typed +0 -0
  38. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/scripts/make_doc.py +0 -0
  39. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/scripts/print_versions.py +0 -0
  40. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/sgrid.py +0 -0
  41. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/__init__.py +0 -0
  42. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/conftest.py +0 -0
  43. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_accessor.py +0 -0
  44. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_coding.py +0 -0
  45. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_groupers.py +0 -0
  46. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_options.py +0 -0
  47. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_parametric.py +0 -0
  48. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_scripts.py +0 -0
  49. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/tests/test_units.py +0 -0
  50. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/units.py +0 -0
  51. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray/utils.py +0 -0
  52. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray.egg-info/SOURCES.txt +0 -0
  53. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray.egg-info/dependency_links.txt +0 -0
  54. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray.egg-info/requires.txt +0 -0
  55. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/cf_xarray.egg-info/top_level.txt +0 -0
  56. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/ci/environment-all-min-deps.yml +0 -0
  57. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/ci/environment-no-optional-deps.yml +0 -0
  58. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/ci/environment.yml +0 -0
  59. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/ci/upstream-dev-env.yml +0 -0
  60. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/codecov.yml +0 -0
  61. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/2D_bounds_averaged.png +0 -0
  62. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/2D_bounds_error.png +0 -0
  63. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/2D_bounds_nonunique.png +0 -0
  64. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/Makefile +0 -0
  65. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/dataset-diagram-logo.tex +0 -0
  66. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/full-logo.png +0 -0
  67. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/logo.png +0 -0
  68. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/logo.svg +0 -0
  69. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/rich-repr-example.png +0 -0
  70. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/_static/style.css +0 -0
  71. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/api.rst +0 -0
  72. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/bounds.md +0 -0
  73. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/cartopy_rotated_pole.png +0 -0
  74. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/coding.md +0 -0
  75. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/contributing.rst +0 -0
  76. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/coord_axes.md +0 -0
  77. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/custom-criteria.md +0 -0
  78. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/dsg.md +0 -0
  79. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/examples/introduction.ipynb +0 -0
  80. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/faq.md +0 -0
  81. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/flags.md +0 -0
  82. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/geometry.md +0 -0
  83. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/grid_mappings.md +0 -0
  84. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/howtouse.md +0 -0
  85. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/index.rst +0 -0
  86. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/make.bat +0 -0
  87. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/parametricz.md +0 -0
  88. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/plotting.md +0 -0
  89. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/provenance.md +0 -0
  90. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/quickstart.md +0 -0
  91. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/roadmap.rst +0 -0
  92. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/selecting.md +0 -0
  93. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/sgrid_ugrid.md +0 -0
  94. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/units.md +0 -0
  95. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/doc/whats-new.rst +0 -0
  96. {cf_xarray-0.10.5 → cf_xarray-0.10.7}/setup.cfg +0 -0
@@ -27,11 +27,11 @@ jobs:
27
27
  fail-fast: false
28
28
  matrix:
29
29
  os: ["ubuntu-latest"]
30
- python-version: ["3.10", "3.13"]
30
+ python-version: ["3.11", "3.13"]
31
31
  env: [""]
32
32
  include:
33
33
  - env: "all-min-deps"
34
- python-version: "3.10"
34
+ python-version: "3.11"
35
35
  os: ubuntu-latest
36
36
  - env: "no-optional-deps"
37
37
  python-version: "3.13"
@@ -70,7 +70,7 @@ jobs:
70
70
  run: |
71
71
  pytest -n auto --cov=./ --cov-report=xml
72
72
  - name: Upload code coverage to Codecov
73
- uses: codecov/codecov-action@v5.4.0
73
+ uses: codecov/codecov-action@v5.4.3
74
74
  with:
75
75
  file: ./coverage.xml
76
76
  flags: unittests
@@ -86,7 +86,7 @@ jobs:
86
86
  shell: bash -l {0}
87
87
  strategy:
88
88
  matrix:
89
- python-version: ["3.10", "3.13"]
89
+ python-version: ["3.11", "3.13"]
90
90
  steps:
91
91
  - uses: actions/checkout@v4
92
92
  with:
@@ -109,7 +109,7 @@ jobs:
109
109
  run: |
110
110
  python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
111
111
  - name: Upload mypy coverage to Codecov
112
- uses: codecov/codecov-action@v5.4.0
112
+ uses: codecov/codecov-action@v5.4.3
113
113
  with:
114
114
  file: mypy_report/cobertura.xml
115
115
  flags: mypy
@@ -3,28 +3,28 @@ ci:
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/asottile/pyupgrade
6
- rev: v3.19.1
6
+ rev: v3.20.0
7
7
  hooks:
8
8
  - id: pyupgrade
9
9
  args: ["--py310-plus"]
10
10
 
11
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
12
12
  # Ruff version.
13
- rev: 'v0.9.3'
13
+ rev: 'v0.12.2'
14
14
  hooks:
15
15
  - id: ruff
16
16
  args: ["--fix", "--show-fixes"]
17
17
  - id: ruff-format
18
18
 
19
19
  - repo: https://github.com/rstcheck/rstcheck
20
- rev: v6.2.4
20
+ rev: v6.2.5
21
21
  hooks:
22
22
  - id: rstcheck
23
23
  additional_dependencies: [sphinx, tomli]
24
24
  args: ['--config', 'pyproject.toml']
25
25
 
26
26
  - repo: https://github.com/executablebooks/mdformat
27
- rev: 0.7.21
27
+ rev: 0.7.22
28
28
  hooks:
29
29
  - id: mdformat
30
30
  additional_dependencies:
@@ -55,7 +55,7 @@ repos:
55
55
  - id: validate-cff
56
56
 
57
57
  - repo: https://github.com/abravalheri/validate-pyproject
58
- rev: v0.23
58
+ rev: v0.24.1
59
59
  hooks:
60
60
  - id: validate-pyproject
61
61
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.5
3
+ Version: 0.10.7
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -214,11 +214,10 @@ Classifier: License :: OSI Approved :: Apache Software License
214
214
  Classifier: Natural Language :: English
215
215
  Classifier: Operating System :: OS Independent
216
216
  Classifier: Programming Language :: Python
217
- Classifier: Programming Language :: Python :: 3.10
218
217
  Classifier: Programming Language :: Python :: 3.11
219
218
  Classifier: Programming Language :: Python :: 3.12
220
219
  Classifier: Programming Language :: Python :: 3.13
221
- Requires-Python: >=3.10
220
+ Requires-Python: >=3.11
222
221
  Description-Content-Type: text/x-rst
223
222
  License-File: LICENSE
224
223
  Requires-Dist: xarray>=2023.09.0
@@ -0,0 +1 @@
1
+ __version__ = "0.10.7"
@@ -39,8 +39,8 @@ except ImportError:
39
39
  )
40
40
 
41
41
  try:
42
- from xarray.core.weighted import (
43
- Weighted, # type:ignore[import-not-found,no-redef,unused-ignore]
42
+ from xarray.core.weighted import ( # type:ignore[import-not-found,no-redef,unused-ignore]
43
+ Weighted,
44
44
  )
45
45
  except ImportError:
46
46
  from xarray.computation.weighted import ( # type:ignore[import-not-found,no-redef,unused-ignore]
@@ -1376,7 +1376,7 @@ class CFAccessor:
1376
1376
 
1377
1377
  def curvefit(
1378
1378
  self,
1379
- coords: Hashable | DataArray | Iterable[Hashable | DataArray],
1379
+ coords: Hashable | Iterable[Hashable],
1380
1380
  func: Callable[..., Any],
1381
1381
  reduce_dims: Hashable | Iterable[Hashable] | None = None,
1382
1382
  skipna: bool = True,
@@ -1386,8 +1386,8 @@ class CFAccessor:
1386
1386
  kwargs: dict[str, Any] | None = None,
1387
1387
  ):
1388
1388
  if coords is not None:
1389
- if isinstance(coords, Hashable | DataArray):
1390
- coords_iter: Iterable[Hashable | DataArray] = [coords]
1389
+ if isinstance(coords, Hashable):
1390
+ coords_iter: Iterable[Hashable] = [coords]
1391
1391
  else:
1392
1392
  coords_iter = coords
1393
1393
  coords = [
@@ -298,8 +298,10 @@ def encode_geometries(ds: xr.Dataset):
298
298
  geom_var_names = [
299
299
  name
300
300
  for name, var in ds._variables.items()
301
- if var.dtype == "O" and isinstance(var.data.flat[0], SHAPELY_TYPES)
301
+ if var.dtype == "geometry"
302
+ or (var.dtype == "O" and isinstance(var.data.flat[0], SHAPELY_TYPES))
302
303
  ]
304
+
303
305
  if not geom_var_names:
304
306
  return ds
305
307
 
@@ -410,8 +412,8 @@ def reshape_unique_geometries(
410
412
  out = out.unstack(temp_name)
411
413
 
412
414
  # geom_var was reshaped also, reconstruct it from the unique values.
413
- unique_indexes = xr.DataArray(unique_indexes, dims=(new_dim,))
414
- out[geom_var] = ds[geom_var].isel({old_name: unique_indexes})
415
+ unique_indexes_da = xr.DataArray(unique_indexes, dims=(new_dim,))
416
+ out[geom_var] = ds[geom_var].isel({old_name: unique_indexes_da})
415
417
  if old_name not in ds.coords:
416
418
  # If there was no coord before, drop the dummy one we made.
417
419
  out = out.drop_vars(old_name) # type: ignore[arg-type,unused-ignore] # Hashable/str stuff
@@ -175,19 +175,79 @@ def bounds_to_vertices(
175
175
  f"Bounds format not understood. Got {bounds.dims} with shape {bounds.shape}."
176
176
  )
177
177
 
178
+ core_dim_coords = {
179
+ dim: bounds.coords[dim].values for dim in core_dims if dim in bounds.coords
180
+ }
181
+ core_dim_orders = _get_core_dim_orders(core_dim_coords)
182
+
178
183
  return xr.apply_ufunc(
179
184
  _bounds_helper,
180
185
  bounds,
181
186
  input_core_dims=[core_dims + [bounds_dim]],
182
187
  dask="parallelized",
183
- kwargs={"n_core_dims": n_core_dims, "nbounds": nbounds, "order": order},
188
+ kwargs={
189
+ "n_core_dims": n_core_dims,
190
+ "nbounds": nbounds,
191
+ "order": order,
192
+ "core_dim_orders": core_dim_orders,
193
+ },
184
194
  output_core_dims=[output_core_dims],
185
195
  dask_gufunc_kwargs=dict(output_sizes=output_sizes),
186
196
  output_dtypes=[bounds.dtype],
187
197
  )
188
198
 
189
199
 
190
- def _bounds_helper(values, n_core_dims, nbounds, order):
200
+ def _get_core_dim_orders(core_dim_coords: dict[str, np.ndarray]) -> dict[str, str]:
201
+ """
202
+ Determine the order (ascending, descending, or mixed) of each core dimension
203
+ based on its coordinates.
204
+
205
+ Repeated (equal) coordinates are ignored when determining the order. If all
206
+ coordinates are equal, the order is treated as "ascending".
207
+
208
+ Parameters
209
+ ----------
210
+ core_dim_coords : dict of str to np.ndarray
211
+ A dictionary mapping dimension names to their coordinate arrays.
212
+
213
+ Returns
214
+ -------
215
+ core_dim_orders : dict of str to str
216
+ A dictionary mapping each dimension name to a string indicating the order:
217
+ - "ascending": strictly increasing (ignoring repeated values)
218
+ - "descending": strictly decreasing (ignoring repeated values)
219
+ - "mixed": neither strictly increasing nor decreasing (ignoring repeated values)
220
+ """
221
+ core_dim_orders = {}
222
+
223
+ for dim, coords in core_dim_coords.items():
224
+ diffs = np.diff(coords)
225
+
226
+ # Handle datetime64 and timedelta64 safely for both numpy 1.26.4 and numpy 2
227
+ if np.issubdtype(coords.dtype, np.datetime64) or np.issubdtype(
228
+ coords.dtype, np.timedelta64
229
+ ):
230
+ # Cast to float64 for safe comparison
231
+ diffs_float = diffs.astype("float64")
232
+ nonzero_diffs = diffs_float[diffs_float != 0]
233
+ else:
234
+ zero = 0
235
+ nonzero_diffs = diffs[diffs != zero]
236
+
237
+ if nonzero_diffs.size == 0:
238
+ # All values are equal, treat as ascending
239
+ core_dim_orders[dim] = "ascending"
240
+ elif np.all(nonzero_diffs > 0):
241
+ core_dim_orders[dim] = "ascending"
242
+ elif np.all(nonzero_diffs < 0):
243
+ core_dim_orders[dim] = "descending"
244
+ else:
245
+ core_dim_orders[dim] = "mixed"
246
+
247
+ return core_dim_orders
248
+
249
+
250
+ def _bounds_helper(values, n_core_dims, nbounds, order, core_dim_orders):
191
251
  if n_core_dims == 2 and nbounds == 4:
192
252
  # Vertices case (2D lat/lon)
193
253
  if order in ["counterclockwise", None]:
@@ -211,11 +271,176 @@ def _bounds_helper(values, n_core_dims, nbounds, order):
211
271
  vertex_vals = np.block([[bot_left, bot_right], [top_left, top_right]])
212
272
  elif n_core_dims == 1 and nbounds == 2:
213
273
  # Middle points case (1D lat/lon)
214
- vertex_vals = np.concatenate((values[..., :, 0], values[..., -1:, 1]), axis=-1)
274
+ vertex_vals = _get_ordered_vertices(values, core_dim_orders)
215
275
 
216
276
  return vertex_vals
217
277
 
218
278
 
279
+ def _get_ordered_vertices(
280
+ bounds: np.ndarray, core_dim_orders: dict[str, str]
281
+ ) -> np.ndarray:
282
+ """
283
+ Convert a bounds array of shape (..., N, 2) or (N, 2) into a 1D array of vertices.
284
+
285
+ This function reconstructs the vertices from a bounds array, handling both
286
+ monotonic and non-monotonic cases.
287
+
288
+ Monotonic bounds (all values strictly increase or decrease when flattened):
289
+ - Concatenate the left endpoints (bounds[..., :, 0]) with the last right
290
+ endpoint (bounds[..., -1, 1]) to form the vertices.
291
+
292
+ Non-monotonic bounds:
293
+ - Determine the order of the core dimension(s) ('ascending' or 'descending').
294
+ - For ascending order:
295
+ - Use the minimum of each interval as the vertex.
296
+ - Use the maximum of the last interval as the final vertex.
297
+ - For descending order:
298
+ - Use the maximum of each interval as the vertex.
299
+ - Use the minimum of the last interval as the final vertex.
300
+ - Vertices are then sorted to match the coordinate direction.
301
+
302
+ Features:
303
+ - Handles both ascending and descending bounds.
304
+ - Preserves repeated coordinates if present.
305
+ - Output shape is (..., N+1) or (N+1,).
306
+
307
+ Parameters
308
+ ----------
309
+ bounds : np.ndarray
310
+ Array of bounds, typically with shape (N, 2) or (..., N, 2).
311
+ core_dim_orders : dict[str, str]
312
+ Dictionary mapping core dimension names to their order ('ascending' or
313
+ 'descending'). Used for sorting the vertices.
314
+
315
+ Returns
316
+ -------
317
+ np.ndarray
318
+ Array of vertices with shape (..., N+1) or (N+1,).
319
+ """
320
+ order = _get_order_of_core_dims(core_dim_orders)
321
+
322
+ if _is_bounds_monotonic(bounds):
323
+ vertices = np.concatenate((bounds[..., :, 0], bounds[..., -1:, 1]), axis=-1)
324
+ else:
325
+ if order == "ascending":
326
+ endpoints = np.minimum(bounds[..., :, 0], bounds[..., :, 1])
327
+ last_endpoint = np.maximum(bounds[..., -1, 0], bounds[..., -1, 1])
328
+ elif order == "descending":
329
+ endpoints = np.maximum(bounds[..., :, 0], bounds[..., :, 1])
330
+ last_endpoint = np.minimum(bounds[..., -1, 0], bounds[..., -1, 1])
331
+
332
+ vertices = np.concatenate(
333
+ [endpoints, np.expand_dims(last_endpoint, axis=-1)], axis=-1
334
+ )
335
+
336
+ vertices = _sort_vertices(vertices, order)
337
+
338
+ return vertices
339
+
340
+
341
+ def _is_bounds_monotonic(bounds: np.ndarray) -> bool:
342
+ """Check if the bounds are monotonic.
343
+
344
+ Arrays are monotonic if all values are increasing or decreasing. This
345
+ functions ignores an intervals where consecutive values are equal, which
346
+ represent repeated coordinates.
347
+
348
+ Parameters
349
+ ----------
350
+ arr : np.ndarray
351
+ Numpy array to check, typically with shape (..., N, 2).
352
+
353
+ Returns
354
+ -------
355
+ bool
356
+ True if the flattened array is increasing or decreasing, False otherwise.
357
+ """
358
+ # NOTE: Python 3.10 uses numpy 1.26.4. If the input is a datetime64 array,
359
+ # numpy 1.26.4 may raise: numpy.core._exceptions._UFuncInputCastingError:
360
+ # Cannot cast ufunc 'greater' input 0 from dtype('<m8[ns]') to dtype('<m8')
361
+ # with casting rule 'same_kind' To avoid this, always cast to float64 before
362
+ # np.diff.
363
+ arr_numeric = bounds.astype("float64").flatten()
364
+ diffs = np.diff(arr_numeric)
365
+ nonzero_diffs = diffs[diffs != 0]
366
+
367
+ # All values are equal, treat as monotonic
368
+ if nonzero_diffs.size == 0:
369
+ return True
370
+
371
+ return bool(np.all(nonzero_diffs > 0) or np.all(nonzero_diffs < 0))
372
+
373
+
374
+ def _get_order_of_core_dims(core_dim_orders: dict[str, str]) -> str:
375
+ """
376
+ Determines the common order of core dimensions from a dictionary of
377
+ dimension orders.
378
+
379
+ Parameters
380
+ ----------
381
+ core_dim_orders : dict of str
382
+ A dictionary mapping dimension names to their respective order strings.
383
+
384
+ Returns
385
+ -------
386
+ order : str
387
+ The common order string shared by all core dimensions.
388
+
389
+ Raises
390
+ ------
391
+ ValueError
392
+ If the core dimension orders are not all aligned (i.e., not all values
393
+ are the same).
394
+ """
395
+ orders = set(core_dim_orders.values())
396
+
397
+ if len(orders) != 1:
398
+ raise ValueError(
399
+ f"All core dimension orders must be aligned. Got orders: {core_dim_orders}"
400
+ )
401
+
402
+ order = next(iter(orders))
403
+
404
+ return order
405
+
406
+
407
+ def _sort_vertices(vertices: np.ndarray, order: str) -> np.ndarray:
408
+ """
409
+ Sorts the vertices array along the last axis in ascending or descending order.
410
+
411
+ Parameters
412
+ ----------
413
+ vertices : np.ndarray
414
+ An array of vertices to be sorted. Sorting is performed along the last
415
+ axis.
416
+ order : str
417
+ The order in which to sort the vertices. Must be either "ascending" or
418
+ any other value for descending order.
419
+
420
+ Returns
421
+ -------
422
+ np.ndarray
423
+ The sorted array of vertices, with the same shape as the input.
424
+
425
+ Examples
426
+ --------
427
+ >>> import numpy as np
428
+ >>> vertices = np.array([[3, 1, 2], [6, 5, 4]])
429
+ >>> _sort_vertices(vertices, "ascending")
430
+ array([[1, 2, 3],
431
+ [4, 5, 6]])
432
+ >>> _sort_vertices(vertices, "descending")
433
+ array([[3, 2, 1],
434
+ [6, 5, 4]])
435
+ """
436
+ if order == "ascending":
437
+ new_vertices = np.sort(vertices, axis=-1)
438
+ else:
439
+ new_vertices = np.sort(vertices, axis=-1)[..., ::-1]
440
+
441
+ return new_vertices
442
+
443
+
219
444
  def vertices_to_bounds(
220
445
  vertices: DataArray, out_dims: Sequence[str] = ("bounds", "x", "y")
221
446
  ) -> DataArray:
@@ -507,5 +507,7 @@ def test_encode_decode(geometry_ds, polygon_geometry):
507
507
  )
508
508
  multi_ds = xr.merge([polyds, geometry_ds[1]])
509
509
  for ds in (geometry_ds[1], polygon_geometry.to_dataset(), geom_dim_ds, multi_ds):
510
- roundtripped = decode_geometries(encode_geometries(ds))
510
+ encoded = encode_geometries(ds)
511
+ assert len(encoded.data_vars) > len(ds.data_vars)
512
+ roundtripped = decode_geometries(encoded)
511
513
  xr.testing.assert_identical(ds, roundtripped)
@@ -1,3 +1,4 @@
1
+ import xarray as xr
1
2
  from numpy.testing import assert_array_equal
2
3
  from xarray.testing import assert_equal
3
4
 
@@ -12,7 +13,7 @@ except ImportError:
12
13
 
13
14
 
14
15
  def test_bounds_to_vertices() -> None:
15
- # 1D case
16
+ # 1D case (stricly monotonic, descending bounds)
16
17
  ds = airds.cf.add_bounds(["lon", "lat", "time"])
17
18
  lat_c = cfxr.bounds_to_vertices(ds.lat_bounds, bounds_dim="bounds")
18
19
  assert_array_equal(ds.lat.values + 1.25, lat_c.values[:-1])
@@ -34,6 +35,59 @@ def test_bounds_to_vertices() -> None:
34
35
  lon_no = cfxr.bounds_to_vertices(rotds.lon_bounds, bounds_dim="bounds", order=None)
35
36
  assert_equal(lon_no, lon_ccw)
36
37
 
38
+ # 2D case (monotonicly increasing coords, non-monotonic bounds)
39
+ bounds_2d_desc = xr.DataArray(
40
+ [[50.5, 50.0], [51.0, 50.5], [51.0, 50.5], [52.0, 51.5], [52.5, 52.0]],
41
+ dims=("lat", "bounds"),
42
+ coords={"lat": [50.75, 50.75, 51.25, 51.75, 52.25]},
43
+ )
44
+ expected_vertices_2d_desc = xr.DataArray(
45
+ [50.0, 50.5, 50.5, 51.5, 52.0, 52.5],
46
+ dims=["lat_vertices"],
47
+ )
48
+ vertices_2d_desc = cfxr.bounds_to_vertices(bounds_2d_desc, bounds_dim="bounds")
49
+ assert_equal(expected_vertices_2d_desc, vertices_2d_desc)
50
+
51
+ # 3D case (non-monotonic bounds, monotonicly increasing coords)
52
+ bounds_3d = xr.DataArray(
53
+ [
54
+ [
55
+ [50.0, 50.5],
56
+ [50.5, 51.0],
57
+ [51.0, 51.5],
58
+ [51.5, 52.0],
59
+ [52.0, 52.5],
60
+ ],
61
+ [
62
+ [60.0, 60.5],
63
+ [60.5, 61.0],
64
+ [61.0, 61.5],
65
+ [61.5, 62.0],
66
+ [62.0, 62.5],
67
+ ],
68
+ ],
69
+ dims=("extra", "lat", "bounds"),
70
+ coords={
71
+ "extra": [0, 1],
72
+ "lat": [0, 1, 2, 3, 4],
73
+ "bounds": [0, 1],
74
+ },
75
+ )
76
+ expected_vertices_3d = xr.DataArray(
77
+ [
78
+ [50.0, 50.5, 51.0, 51.5, 52.0, 52.5],
79
+ [60.0, 60.5, 61.0, 61.5, 62.0, 62.5],
80
+ ],
81
+ dims=("extra", "lat_vertices"),
82
+ coords={
83
+ "extra": [0, 1],
84
+ },
85
+ )
86
+ vertices_3d = cfxr.bounds_to_vertices(
87
+ bounds_3d, bounds_dim="bounds", core_dims=["lat"]
88
+ )
89
+ assert_equal(vertices_3d, expected_vertices_3d)
90
+
37
91
  # Transposing the array changes the bounds direction
38
92
  ds = mollwds.transpose("x", "y", "x_vertices", "y_vertices", "bounds")
39
93
  lon_cw = cfxr.bounds_to_vertices(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.5
3
+ Version: 0.10.7
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -214,11 +214,10 @@ Classifier: License :: OSI Approved :: Apache Software License
214
214
  Classifier: Natural Language :: English
215
215
  Classifier: Operating System :: OS Independent
216
216
  Classifier: Programming Language :: Python
217
- Classifier: Programming Language :: Python :: 3.10
218
217
  Classifier: Programming Language :: Python :: 3.11
219
218
  Classifier: Programming Language :: Python :: 3.12
220
219
  Classifier: Programming Language :: Python :: 3.13
221
- Requires-Python: >=3.10
220
+ Requires-Python: >=3.11
222
221
  Description-Content-Type: text/x-rst
223
222
  License-File: LICENSE
224
223
  Requires-Dist: xarray>=2023.09.0
@@ -8,7 +8,7 @@ dependencies:
8
8
  - netcdf4
9
9
  - pooch
10
10
  - xarray
11
- - sphinx<8
11
+ - sphinx
12
12
  - sphinx-copybutton
13
13
  - numpydoc
14
14
  - sphinx-autosummary-accessors
@@ -265,8 +265,8 @@ napoleon_use_param = True
265
265
  napoleon_use_rtype = True
266
266
 
267
267
  numpydoc_show_class_members = False
268
- # Report warnings for all validation checks except the ones listed after "all"
269
- numpydoc_validation_checks = {"all", "ES01", "EX01", "SA01", "SA04"}
268
+ # Disable numpydoc validation to avoid compatibility issues with Sphinx 8
269
+ numpydoc_validation_checks = set()
270
270
  # don't report on objects that match any of these regex
271
271
  numpydoc_validation_exclude = {
272
272
  "cf_xarray.accessor.",
@@ -2,7 +2,7 @@
2
2
  name = "cf_xarray"
3
3
  description = "A convenience wrapper for using CF attributes on xarray objects"
4
4
  readme = "README.rst"
5
- requires-python = ">=3.10"
5
+ requires-python = ">=3.11"
6
6
  license = {file = "LICENSE"}
7
7
  keywords = ["xarray", "metadata", "CF conventions"]
8
8
  classifiers = [
@@ -11,7 +11,6 @@ classifiers = [
11
11
  "Natural Language :: English",
12
12
  "Operating System :: OS Independent",
13
13
  "Programming Language :: Python",
14
- "Programming Language :: Python :: 3.10",
15
14
  "Programming Language :: Python :: 3.11",
16
15
  "Programming Language :: Python :: 3.12",
17
16
  "Programming Language :: Python :: 3.13",
@@ -55,7 +54,7 @@ write_to_template= '__version__ = "{version}"'
55
54
  tag_regex= "^(?P<prefix>v)?(?P<version>[^\\+]+)(?P<suffix>.*)?$"
56
55
 
57
56
  [tool.ruff]
58
- target-version = "py310"
57
+ target-version = "py311"
59
58
  builtins = ["ellipsis"]
60
59
  exclude = [
61
60
  ".eggs",
@@ -1 +0,0 @@
1
- __version__ = "0.10.5"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes