cf-xarray 0.9.3__tar.gz → 0.9.4__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 (91) hide show
  1. cf_xarray-0.9.4/.github/release.yml +5 -0
  2. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/PKG-INFO +1 -1
  3. cf_xarray-0.9.4/cf_xarray/_version.py +1 -0
  4. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/geometry.py +354 -205
  5. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_geometry.py +7 -1
  6. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/PKG-INFO +1 -1
  7. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/SOURCES.txt +1 -0
  8. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/api.rst +1 -0
  9. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/geometry.md +2 -3
  10. cf_xarray-0.9.3/cf_xarray/_version.py +0 -1
  11. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.binder/environment.yml +0 -0
  12. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.deepsource.toml +0 -0
  13. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/dependabot.yml +0 -0
  14. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/ci.yaml +0 -0
  15. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/parse_logs.py +0 -0
  16. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/pypi.yaml +0 -0
  17. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/testpypi-release.yaml +0 -0
  18. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.github/workflows/upstream-dev-ci.yaml +0 -0
  19. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.gitignore +0 -0
  20. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.pre-commit-config.yaml +0 -0
  21. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.readthedocs.yml +0 -0
  22. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/.tributors +0 -0
  23. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/CITATION.cff +0 -0
  24. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/LICENSE +0 -0
  25. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/README.rst +0 -0
  26. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/__init__.py +0 -0
  27. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/accessor.py +0 -0
  28. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/coding.py +0 -0
  29. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/criteria.py +0 -0
  30. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/datasets.py +0 -0
  31. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/formatting.py +0 -0
  32. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/helpers.py +0 -0
  33. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/options.py +0 -0
  34. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/py.typed +0 -0
  35. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/scripts/make_doc.py +0 -0
  36. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/scripts/print_versions.py +0 -0
  37. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/sgrid.py +0 -0
  38. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/__init__.py +0 -0
  39. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/conftest.py +0 -0
  40. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_accessor.py +0 -0
  41. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_coding.py +0 -0
  42. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_helpers.py +0 -0
  43. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_options.py +0 -0
  44. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_scripts.py +0 -0
  45. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/tests/test_units.py +0 -0
  46. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/units.py +0 -0
  47. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray/utils.py +0 -0
  48. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/dependency_links.txt +0 -0
  49. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/requires.txt +0 -0
  50. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/cf_xarray.egg-info/top_level.txt +0 -0
  51. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/doc.yml +0 -0
  52. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/environment-no-optional-deps.yml +0 -0
  53. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/environment.yml +0 -0
  54. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/ci/upstream-dev-env.yml +0 -0
  55. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/codecov.yml +0 -0
  56. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_averaged.png +0 -0
  57. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_error.png +0 -0
  58. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/2D_bounds_nonunique.png +0 -0
  59. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/Makefile +0 -0
  60. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/dataset-diagram-logo.tex +0 -0
  61. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/full-logo.png +0 -0
  62. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/logo.png +0 -0
  63. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/logo.svg +0 -0
  64. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/rich-repr-example.png +0 -0
  65. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/_static/style.css +0 -0
  66. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/bounds.md +0 -0
  67. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/cartopy_rotated_pole.png +0 -0
  68. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/coding.md +0 -0
  69. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/conf.py +0 -0
  70. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/contributing.rst +0 -0
  71. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/coord_axes.md +0 -0
  72. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/custom-criteria.md +0 -0
  73. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/dsg.md +0 -0
  74. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/examples/introduction.ipynb +0 -0
  75. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/faq.md +0 -0
  76. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/flags.md +0 -0
  77. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/grid_mappings.md +0 -0
  78. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/howtouse.md +0 -0
  79. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/index.rst +0 -0
  80. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/make.bat +0 -0
  81. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/parametricz.md +0 -0
  82. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/plotting.md +0 -0
  83. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/provenance.md +0 -0
  84. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/quickstart.md +0 -0
  85. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/roadmap.rst +0 -0
  86. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/selecting.md +0 -0
  87. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/sgrid_ugrid.md +0 -0
  88. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/units.md +0 -0
  89. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/doc/whats-new.rst +0 -0
  90. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/pyproject.toml +0 -0
  91. {cf_xarray-0.9.3 → cf_xarray-0.9.4}/setup.cfg +0 -0
@@ -0,0 +1,5 @@
1
+ changelog:
2
+ exclude:
3
+ authors:
4
+ - dependabot
5
+ - pre-commit-ci
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cf_xarray
3
- Version: 0.9.3
3
+ Version: 0.9.4
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
@@ -0,0 +1 @@
1
+ __version__ = "0.9.4"
@@ -1,13 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
- from collections.abc import Sequence
4
+ from collections import ChainMap
5
+ from collections.abc import Hashable, Sequence
6
+ from dataclasses import dataclass
5
7
 
6
8
  import numpy as np
7
9
  import pandas as pd
8
10
  import xarray as xr
11
+ from numpy.typing import ArrayLike
9
12
 
10
13
  GEOMETRY_CONTAINER_NAME = "geometry_container"
14
+ FEATURES_DIM_NAME = "features"
11
15
 
12
16
  __all__ = [
13
17
  "decode_geometries",
@@ -30,9 +34,149 @@ __all__ = [
30
34
  # 1. node coordinates are exact; the 'normal' coordinates are a reasonable value to use, if you do not know how to interpret the nodes.
31
35
 
32
36
 
37
+ @dataclass
38
+ class GeometryNames:
39
+ """Helper class to ease handling of all the variable names needed for CF geometries."""
40
+
41
+ def __init__(
42
+ self,
43
+ suffix: str = "",
44
+ grid_mapping_name: str | None = None,
45
+ grid_mapping: str | None = None,
46
+ ):
47
+ self.container_name: str = GEOMETRY_CONTAINER_NAME + suffix
48
+ self.node_dim: str = "node" + suffix
49
+ self.node_count: str = "node_count" + suffix
50
+ self.node_coordinates_x: str = "x" + suffix
51
+ self.node_coordinates_y: str = "y" + suffix
52
+ self.coordinates_x: str = "crd_x" + suffix
53
+ self.coordinates_y: str = "crd_y" + suffix
54
+ self.part_node_count: str = "part_node_count" + suffix
55
+ self.part_dim: str = "part" + suffix
56
+ self.interior_ring: str = "interior_ring" + suffix
57
+ self.attrs_x: dict[str, str] = {}
58
+ self.attrs_y: dict[str, str] = {}
59
+ self.grid_mapping_attr = {"grid_mapping": grid_mapping} if grid_mapping else {}
60
+
61
+ # Special treatment of selected grid mappings
62
+ if grid_mapping_name in ["latitude_longitude", "rotated_latitude_longitude"]:
63
+ # Special case for longitude_latitude type grid mappings
64
+ self.coordinates_x = "lon"
65
+ self.coordinates_y = "lat"
66
+ if grid_mapping_name == "latitude_longitude":
67
+ self.attrs_x = dict(units="degrees_east", standard_name="longitude")
68
+ self.attrs_y = dict(units="degrees_north", standard_name="latitude")
69
+ elif grid_mapping_name == "rotated_latitude_longitude":
70
+ self.attrs_x = dict(
71
+ units="degrees_east", standard_name="grid_longitude"
72
+ )
73
+ self.attrs_y = dict(
74
+ units="degrees_north", standard_name="grid_latitude"
75
+ )
76
+ elif grid_mapping_name is not None:
77
+ self.attrs_x = dict(standard_name="projection_x_coordinate")
78
+ self.attrs_y = dict(standard_name="projection_y_coordinate")
79
+ self.attrs_x.update(self.grid_mapping_attr)
80
+ self.attrs_y.update(self.grid_mapping_attr)
81
+
82
+ @property
83
+ def geometry_container_attrs(self) -> dict[str, str]:
84
+ return {
85
+ "node_count": self.node_count,
86
+ "node_coordinates": f"{self.node_coordinates_x} {self.node_coordinates_y}",
87
+ "coordinates": f"{self.coordinates_x} {self.coordinates_y}",
88
+ **self.grid_mapping_attr,
89
+ }
90
+
91
+ def coords(
92
+ self,
93
+ *,
94
+ dim: Hashable,
95
+ x: ArrayLike,
96
+ y: ArrayLike,
97
+ crdX: ArrayLike | None = None,
98
+ crdY: ArrayLike | None = None,
99
+ ) -> dict[str, xr.DataArray]:
100
+ """
101
+ Construct coordinate DataArrays for the numpy data (x, y, crdX, crdY)
102
+
103
+ Parameters
104
+ ----------
105
+ x: array
106
+ Node coordinates for X coordinate
107
+ y: array
108
+ Node coordinates for Y coordinate
109
+ crdX: array, optional
110
+ Nominal X coordinate
111
+ crdY: array, optional
112
+ Nominal X coordinate
113
+ """
114
+ mapping = {
115
+ self.node_coordinates_x: xr.DataArray(
116
+ x, dims=self.node_dim, attrs={"axis": "X", **self.attrs_x}
117
+ ),
118
+ self.node_coordinates_y: xr.DataArray(
119
+ y, dims=self.node_dim, attrs={"axis": "Y", **self.attrs_y}
120
+ ),
121
+ }
122
+ if crdX is not None:
123
+ mapping[self.coordinates_x] = xr.DataArray(
124
+ crdX,
125
+ dims=(dim,),
126
+ attrs={"nodes": self.node_coordinates_x, **self.attrs_x},
127
+ )
128
+ if crdY is not None:
129
+ mapping[self.coordinates_y] = xr.DataArray(
130
+ crdY,
131
+ dims=(dim,),
132
+ attrs={"nodes": self.node_coordinates_y, **self.attrs_y},
133
+ )
134
+ return mapping
135
+
136
+
137
+ def _assert_single_geometry_container(ds: xr.Dataset) -> Hashable:
138
+ container_names = _get_geometry_containers(ds)
139
+ if len(container_names) > 1:
140
+ raise ValueError(
141
+ "Only one geometry container is supported by cf_to_points. "
142
+ "To handle multiple geometries use `decode_geometries` instead."
143
+ )
144
+ (container_name,) = container_names
145
+ return container_name
146
+
147
+
148
+ def _get_geometry_containers(obj: xr.DataArray | xr.Dataset) -> list[Hashable]:
149
+ """
150
+ Translate from key (either CF key or variable name) to its bounds' variable names.
151
+
152
+ This function interprets the ``geometry`` attribute on DataArrays.
153
+
154
+ Parameters
155
+ ----------
156
+ obj : DataArray, Dataset
157
+ DataArray belonging to the coordinate to be checked
158
+
159
+ Returns
160
+ -------
161
+ List[str]
162
+ Variable name(s) in parent xarray object that are bounds of `key`
163
+ """
164
+
165
+ if isinstance(obj, xr.DataArray):
166
+ obj = obj._to_temp_dataset()
167
+ variables = obj._variables
168
+
169
+ results = set()
170
+ for name, var in variables.items():
171
+ attrs_or_encoding = ChainMap(var.attrs, var.encoding)
172
+ if "geometry_type" in attrs_or_encoding:
173
+ results.update([name])
174
+ return list(results)
175
+
176
+
33
177
  def decode_geometries(encoded: xr.Dataset) -> xr.Dataset:
34
178
  """
35
- Decode CF encoded geometries to a numpy object array containing shapely geometries.
179
+ Decode CF encoded geometries to numpy object arrays containing shapely geometries.
36
180
 
37
181
  Parameters
38
182
  ----------
@@ -50,46 +194,57 @@ def decode_geometries(encoded: xr.Dataset) -> xr.Dataset:
50
194
  cf_to_shapely
51
195
  encode_geometries
52
196
  """
53
- if GEOMETRY_CONTAINER_NAME not in encoded._variables:
197
+
198
+ containers = _get_geometry_containers(encoded)
199
+ if not containers:
54
200
  raise NotImplementedError(
55
- f"Currently only a single geometry variable named {GEOMETRY_CONTAINER_NAME!r} is supported."
56
- "A variable by this name is not present in the provided dataset."
201
+ "No geometry container variables detected, none of the provided variables "
202
+ "have a `geometry_type` attribute."
57
203
  )
58
204
 
59
- enc_geom_var = encoded[GEOMETRY_CONTAINER_NAME]
60
- geom_attrs = enc_geom_var.attrs
61
- # Grab the coordinates attribute
62
- geom_attrs.update(enc_geom_var.encoding)
63
-
64
- geom_var = cf_to_shapely(encoded).variable
65
-
66
- todrop = (GEOMETRY_CONTAINER_NAME,) + tuple(
67
- s
68
- for s in " ".join(
69
- geom_attrs.get(attr, "")
70
- for attr in [
71
- "interior_ring",
72
- "node_coordinates",
73
- "node_count",
74
- "part_node_count",
75
- "coordinates",
76
- ]
77
- ).split(" ")
78
- if s
79
- )
80
- decoded = encoded.drop_vars(todrop)
81
-
82
- name = geom_attrs.get("variable_name", None)
83
- if name in decoded.dims:
84
- decoded = decoded.assign_coords(
85
- xr.Coordinates(coords={name: geom_var}, indexes={})
205
+ todrop: list[Hashable] = []
206
+ decoded = xr.Dataset()
207
+ for container_name in containers:
208
+ enc_geom_var = encoded[container_name]
209
+ geom_attrs = enc_geom_var.attrs
210
+
211
+ # Grab the coordinates attribute
212
+ geom_attrs.update(enc_geom_var.encoding)
213
+
214
+ geom_var = cf_to_shapely(encoded, container=container_name).variable
215
+
216
+ todrop.extend(
217
+ (container_name,)
218
+ + tuple(
219
+ s
220
+ for s in " ".join(
221
+ geom_attrs.get(attr, "")
222
+ for attr in [
223
+ "interior_ring",
224
+ "node_coordinates",
225
+ "node_count",
226
+ "part_node_count",
227
+ "coordinates",
228
+ ]
229
+ ).split(" ")
230
+ if s
231
+ )
86
232
  )
87
- else:
88
- decoded[name] = geom_var
233
+
234
+ name = geom_attrs.get("variable_name", None)
235
+ if name in encoded.dims:
236
+ decoded = decoded.assign_coords(
237
+ xr.Coordinates(coords={name: geom_var}, indexes={})
238
+ )
239
+ else:
240
+ decoded[name] = geom_var
241
+
242
+ decoded.update(encoded.drop_vars(todrop))
89
243
 
90
244
  # Is this a good idea? We are deleting information.
245
+ # OTOH we have decoded it to a useful in-memory representation
91
246
  for var in decoded._variables.values():
92
- if var.attrs.get("geometry") == GEOMETRY_CONTAINER_NAME:
247
+ if var.attrs.get("geometry") in containers:
93
248
  var.attrs.pop("geometry")
94
249
  return decoded
95
250
 
@@ -101,11 +256,6 @@ def encode_geometries(ds: xr.Dataset):
101
256
  Practically speaking, geometry variables are numpy object arrays where the first
102
257
  element is a shapely geometry.
103
258
 
104
- .. warning::
105
-
106
- Only a single geometry variable is supported at present. Contributions to fix this
107
- are welcome.
108
-
109
259
  Parameters
110
260
  ----------
111
261
  ds : Dataset
@@ -114,7 +264,9 @@ def encode_geometries(ds: xr.Dataset):
114
264
  Returns
115
265
  -------
116
266
  Dataset
117
- Where all geometry variables are encoded.
267
+ Where all geometry variables are encoded. The information in a single geometry
268
+ variable in the input is split across multiple variables in the returned Dataset
269
+ following the CF conventions.
118
270
 
119
271
  See Also
120
272
  --------
@@ -151,58 +303,57 @@ def encode_geometries(ds: xr.Dataset):
151
303
  # e.g. xvec GeometryIndex
152
304
  ds = ds.drop_indexes(to_drop)
153
305
 
154
- if len(geom_var_names) > 1:
155
- raise NotImplementedError(
156
- "Multiple geometry variables are not supported at this time. "
157
- "Contributions to fix this are welcome. "
158
- f"Detected geometry variables are {geom_var_names!r}"
306
+ variables = {}
307
+ for name in geom_var_names:
308
+ # TODO: do we prefer this choice be invariant to number of geometry variables
309
+ suffix = "_" + str(name) if len(geom_var_names) > 1 else ""
310
+ container_name = GEOMETRY_CONTAINER_NAME + suffix
311
+ # If `name` is a dimension name, then we need to drop it. Otherwise we don't
312
+ # So set errors="ignore"
313
+ variables.update(
314
+ shapely_to_cf(ds[name], suffix=suffix)
315
+ .drop_vars(name, errors="ignore")
316
+ ._variables
159
317
  )
160
318
 
161
- (name,) = geom_var_names
162
- variables = {}
163
- # If `name` is a dimension name, then we need to drop it. Otherwise we don't
164
- # So set errors="ignore"
165
- variables.update(
166
- shapely_to_cf(ds[name]).drop_vars(name, errors="ignore")._variables
319
+ geom_var = ds[name]
320
+ more_updates = {}
321
+ for varname, var in ds._variables.items():
322
+ if varname == name:
323
+ continue
324
+ # TODO: this is incomplete. It works for vector data cubes where one of the geometry vars
325
+ # is a dimension coordinate.
326
+ if name in var.dims:
327
+ var = var.copy()
328
+ var._attrs = copy.deepcopy(var._attrs)
329
+ var.attrs["geometry"] = container_name
330
+ # The grid_mapping and coordinates attributes can be carried by the geometry container
331
+ # variable provided they are also carried by the data variables associated with the container.
332
+ if to_add := geom_var.attrs.get("coordinates", ""):
333
+ var.attrs["coordinates"] = var.attrs.get("coordinates", "") + to_add
334
+ more_updates[varname] = var
335
+ variables.update(more_updates)
336
+
337
+ # WARNING: cf-xarray specific convention.
338
+ # For vector data cubes, `name` is a dimension name.
339
+ # By encoding to CF, we have
340
+ # encoded the information in that variable across many different
341
+ # variables (e.g. node_count) with `name` as a dimension.
342
+ # We have to record `name` somewhere so that we reconstruct
343
+ # a geometry variable of the right name at decode-time.
344
+ variables[container_name].attrs["variable_name"] = name
345
+
346
+ encoded = xr.Dataset(variables).set_coords(
347
+ set(ds._coord_names) - set(geom_var_names)
167
348
  )
168
349
 
169
- geom_var = ds[name]
170
-
171
- more_updates = {}
172
- for varname, var in ds._variables.items():
173
- if varname == name:
174
- continue
175
- # TODO: this is incomplete. It works for vector data cubes where one of the geometry vars
176
- # is a dimension coordinate.
177
- if name in var.dims:
178
- var = var.copy()
179
- var._attrs = copy.deepcopy(var._attrs)
180
- var.attrs["geometry"] = GEOMETRY_CONTAINER_NAME
181
- # The grid_mapping and coordinates attributes can be carried by the geometry container
182
- # variable provided they are also carried by the data variables associated with the container.
183
- if to_add := geom_var.attrs.get("coordinates", ""):
184
- var.attrs["coordinates"] = var.attrs.get("coordinates", "") + to_add
185
- more_updates[varname] = var
186
- variables.update(more_updates)
187
-
188
- # WARNING: cf-xarray specific convention.
189
- # For vector data cubes, `name` is a dimension name.
190
- # By encoding to CF, we have
191
- # encoded the information in that variable across many different
192
- # variables (e.g. node_count) with `name` as a dimension.
193
- # We have to record `name` somewhere so that we reconstruct
194
- # a geometry variable of the right name at decode-time.
195
- variables[GEOMETRY_CONTAINER_NAME].attrs["variable_name"] = name
196
-
197
- encoded = xr.Dataset(variables)
198
-
199
350
  return encoded
200
351
 
201
352
 
202
353
  def reshape_unique_geometries(
203
354
  ds: xr.Dataset,
204
355
  geom_var: str = "geometry",
205
- new_dim: str = "features",
356
+ new_dim: str = FEATURES_DIM_NAME,
206
357
  ) -> xr.Dataset:
207
358
  """Reshape a dataset containing a geometry variable so that all unique features are
208
359
  identified along a new dimension.
@@ -263,7 +414,12 @@ def reshape_unique_geometries(
263
414
  return out
264
415
 
265
416
 
266
- def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None = None):
417
+ def shapely_to_cf(
418
+ geometries: xr.DataArray | Sequence,
419
+ grid_mapping: str | None = None,
420
+ *,
421
+ suffix: str = "",
422
+ ):
267
423
  """
268
424
  Convert a DataArray with shapely geometry objects into a CF-compliant dataset.
269
425
 
@@ -277,8 +433,8 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
277
433
  A CF grid mapping name. When given, coordinates and attributes are named and set accordingly.
278
434
  Defaults to None, in which case the coordinates are simply names "crd_x" and "crd_y".
279
435
 
280
- .. warning::
281
- Only the `longitude_latitude` grid mapping is currently implemented.
436
+ container_name: str, optional
437
+ Name for the "geometry container" scalar variable in the encoded Dataset
282
438
 
283
439
  Returns
284
440
  -------
@@ -289,7 +445,7 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
289
445
  - 'node_count' : The number of nodes per feature. Always present for Lines and Polygons. For Points: only present if there are multipart geometries.
290
446
  - 'part_node_count' : The number of nodes per individual geometry. Only for Lines with multipart geometries and for Polygons with multipart geometries or holes.
291
447
  - 'interior_ring' : Integer boolean indicating whether rings are interior or exterior. Only for Polygons with holes.
292
- - 'geometry_container' : Empty variable with attributes describing the geometry type.
448
+ - container_name : Empty variable with attributes describing the geometry type.
293
449
 
294
450
  References
295
451
  ----------
@@ -308,56 +464,39 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
308
464
  geom.item().geom_type if isinstance(geom, xr.DataArray) else geom.geom_type
309
465
  for geom in geometries
310
466
  }
311
- if types.issubset({"Point", "MultiPoint"}):
312
- ds = points_to_cf(geometries)
313
- elif types.issubset({"LineString", "MultiLineString"}):
314
- ds = lines_to_cf(geometries)
315
- elif types.issubset({"Polygon", "MultiPolygon"}):
316
- ds = polygons_to_cf(geometries)
317
- else:
318
- raise ValueError(
319
- f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
320
- )
321
-
322
- ds[GEOMETRY_CONTAINER_NAME].attrs.update(coordinates="crd_x crd_y")
323
467
 
468
+ grid_mapping_varname = None
324
469
  if (
325
470
  grid_mapping is None
326
471
  and isinstance(geometries, xr.DataArray)
327
472
  and (grid_mapping_varname := geometries.attrs.get("grid_mapping"))
328
473
  ):
329
474
  if grid_mapping_varname in geometries.coords:
330
- grid_mapping = geometries.coords[grid_mapping_varname].attrs[
331
- "grid_mapping_name"
332
- ]
333
- for name_ in ["x", "y", "crd_x", "crd_y"]:
334
- ds[name_].attrs["grid_mapping"] = grid_mapping_varname
335
-
336
- # Special treatment of selected grid mappings
337
- if grid_mapping in ["latitude_longitude", "rotated_latitude_longitude"]:
338
- # Special case for longitude_latitude type grid mappings
339
- ds = ds.rename(crd_x="lon", crd_y="lat")
340
- if grid_mapping == "latitude_longitude":
341
- ds.lon.attrs.update(units="degrees_east", standard_name="longitude")
342
- ds.x.attrs.update(units="degrees_east", standard_name="longitude")
343
- ds.lat.attrs.update(units="degrees_north", standard_name="latitude")
344
- ds.y.attrs.update(units="degrees_north", standard_name="latitude")
345
- elif grid_mapping == "rotated_latitude_longitude":
346
- ds.lon.attrs.update(units="degrees", standard_name="grid_longitude")
347
- ds.x.attrs.update(units="degrees", standard_name="grid_longitude")
348
- ds.lat.attrs.update(units="degrees", standard_name="grid_latitude")
349
- ds.y.attrs.update(units="degrees", standard_name="grid_latitude")
350
- ds[GEOMETRY_CONTAINER_NAME].attrs.update(coordinates="lon lat")
351
- elif grid_mapping is not None:
352
- ds.crd_x.attrs.update(standard_name="projection_x_coordinate")
353
- ds.x.attrs.update(standard_name="projection_x_coordinate")
354
- ds.crd_y.attrs.update(standard_name="projection_y_coordinate")
355
- ds.y.attrs.update(standard_name="projection_y_coordinate")
475
+ # Not all CRS can be encoded in CF
476
+ grid_mapping = geometries.coords[grid_mapping_varname].attrs.get(
477
+ "grid_mapping_name", None
478
+ )
479
+
480
+ # TODO: consider accepting a GeometryNames instance from the user instead
481
+ names = GeometryNames(
482
+ suffix=suffix, grid_mapping_name=grid_mapping, grid_mapping=grid_mapping_varname
483
+ )
484
+
485
+ if types.issubset({"Point", "MultiPoint"}):
486
+ ds = points_to_cf(geometries, names=names)
487
+ elif types.issubset({"LineString", "MultiLineString"}):
488
+ ds = lines_to_cf(geometries, names=names)
489
+ elif types.issubset({"Polygon", "MultiPolygon"}):
490
+ ds = polygons_to_cf(geometries, names=names)
491
+ else:
492
+ raise ValueError(
493
+ f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
494
+ )
356
495
 
357
496
  return ds
358
497
 
359
498
 
360
- def cf_to_shapely(ds: xr.Dataset):
499
+ def cf_to_shapely(ds: xr.Dataset, *, container: Hashable = GEOMETRY_CONTAINER_NAME):
361
500
  """
362
501
  Convert geometries stored in a CF-compliant way to shapely objects stored in a single variable.
363
502
 
@@ -378,22 +517,35 @@ def cf_to_shapely(ds: xr.Dataset):
378
517
  ----------
379
518
  Please refer to the CF conventions document: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#geometries
380
519
  """
381
- geom_type = ds[GEOMETRY_CONTAINER_NAME].attrs["geometry_type"]
520
+ if container not in ds._variables:
521
+ raise ValueError(
522
+ f"{container!r} is not the name of a variable in the provided Dataset."
523
+ )
524
+ if not (geom_type := ds[container].attrs.get("geometry_type", None)):
525
+ raise ValueError(
526
+ f"{container!r} is not the name of a valid geometry variable. "
527
+ "It does not have a `geometry_type` attribute."
528
+ )
529
+
530
+ # Extract all necessary geometry variables
531
+ subds = ds.cf[[container]]
382
532
  if geom_type == "point":
383
- geometries = cf_to_points(ds)
533
+ geometries = cf_to_points(subds)
384
534
  elif geom_type == "line":
385
- geometries = cf_to_lines(ds)
535
+ geometries = cf_to_lines(subds)
386
536
  elif geom_type == "polygon":
387
- geometries = cf_to_polygons(ds)
537
+ geometries = cf_to_polygons(subds)
388
538
  else:
389
539
  raise ValueError(
390
540
  f"Valid CF geometry types are 'point', 'line' and 'polygon'. Got {geom_type}"
391
541
  )
542
+ if gm := ds[container].attrs.get("grid_mapping"):
543
+ geometries.attrs["grid_mapping"] = gm
392
544
 
393
545
  return geometries.rename("geometry")
394
546
 
395
547
 
396
- def points_to_cf(pts: xr.DataArray | Sequence):
548
+ def points_to_cf(pts: xr.DataArray | Sequence, *, names: GeometryNames | None = None):
397
549
  """Get a list of points (shapely.geometry.[Multi]Point) and return a CF-compliant geometry dataset.
398
550
 
399
551
  Parameters
@@ -410,11 +562,14 @@ def points_to_cf(pts: xr.DataArray | Sequence):
410
562
  from shapely.geometry import MultiPoint
411
563
 
412
564
  if isinstance(pts, xr.DataArray):
565
+ # TODO: Fix this hardcoding
566
+ if pts.ndim != 1:
567
+ raise ValueError("Only 1D DataArrays are supported.")
413
568
  dim = pts.dims[0]
414
569
  coord = pts[dim] if dim in pts.coords else None
415
570
  pts_ = pts.values.tolist()
416
571
  else:
417
- dim = "features"
572
+ dim = FEATURES_DIM_NAME
418
573
  coord = None
419
574
  pts_ = pts
420
575
 
@@ -430,33 +585,27 @@ def points_to_cf(pts: xr.DataArray | Sequence):
430
585
  crdX.append(xy[0, 0])
431
586
  crdY.append(xy[0, 1])
432
587
 
588
+ if names is None:
589
+ names = GeometryNames()
590
+
433
591
  ds = xr.Dataset(
434
592
  data_vars={
435
- "node_count": xr.DataArray(node_count, dims=(dim,)),
436
- "geometry_container": xr.DataArray(
437
- attrs={
438
- "geometry_type": "point",
439
- "node_count": "node_count",
440
- "node_coordinates": "x y",
441
- "coordinates": "crd_x crd_y",
442
- }
593
+ names.node_count: xr.DataArray(node_count, dims=(dim,)),
594
+ names.container_name: xr.DataArray(
595
+ data=np.nan,
596
+ attrs={"geometry_type": "point", **names.geometry_container_attrs},
443
597
  ),
444
598
  },
445
- coords={
446
- "x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
447
- "y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
448
- "crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
449
- "crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
450
- },
599
+ coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
451
600
  )
452
601
 
453
602
  if coord is not None:
454
603
  ds = ds.assign_coords({dim: coord})
455
604
 
456
605
  # Special case when we have no MultiPoints
457
- if (ds.node_count == 1).all():
458
- ds = ds.drop_vars("node_count")
459
- del ds[GEOMETRY_CONTAINER_NAME].attrs["node_count"]
606
+ if (ds[names.node_count] == 1).data.all():
607
+ ds = ds.drop_vars(names.node_count)
608
+ del ds[names.container_name].attrs["node_count"]
460
609
  return ds
461
610
 
462
611
 
@@ -467,7 +616,7 @@ def cf_to_points(ds: xr.Dataset):
467
616
  ----------
468
617
  ds : xr.Dataset
469
618
  A dataset with CF-compliant point geometries.
470
- Must have a "geometry_container" variable with at least a 'node_coordinates' attribute.
619
+ Must have a *single* "geometry container" variable with at least a 'node_coordinates' attribute.
471
620
  Must also have the two 1D variables listed by this attribute.
472
621
 
473
622
  Returns
@@ -479,8 +628,9 @@ def cf_to_points(ds: xr.Dataset):
479
628
  """
480
629
  from shapely.geometry import MultiPoint, Point
481
630
 
631
+ container_name = _assert_single_geometry_container(ds)
482
632
  # Shorthand for convenience
483
- geo = ds[GEOMETRY_CONTAINER_NAME].attrs
633
+ geo = ds[container_name].attrs
484
634
 
485
635
  # The features dimension name, defaults to the one of 'node_count' or the dimension of the coordinates, if present.
486
636
  feat_dim = None
@@ -488,14 +638,14 @@ def cf_to_points(ds: xr.Dataset):
488
638
  xcoord_name, _ = geo["coordinates"].split(" ")
489
639
  (feat_dim,) = ds[xcoord_name].dims
490
640
 
491
- x_name, y_name = ds[GEOMETRY_CONTAINER_NAME].attrs["node_coordinates"].split(" ")
641
+ x_name, y_name = ds[container_name].attrs["node_coordinates"].split(" ")
492
642
  xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
493
643
 
494
- node_count_name = ds[GEOMETRY_CONTAINER_NAME].attrs.get("node_count")
644
+ node_count_name = ds[container_name].attrs.get("node_count")
495
645
  if node_count_name is None:
496
646
  # No node_count means all geometries are single points (node_count = 1)
497
- # And if we had no coordinates, then the dimension defaults to "features"
498
- feat_dim = feat_dim or "features"
647
+ # And if we had no coordinates, then the dimension defaults to FEATURES_DIM_NAME
648
+ feat_dim = feat_dim or FEATURES_DIM_NAME
499
649
  node_count = xr.DataArray([1] * xy.shape[0], dims=(feat_dim,))
500
650
  if feat_dim in ds.coords:
501
651
  node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
@@ -512,10 +662,13 @@ def cf_to_points(ds: xr.Dataset):
512
662
  geoms[i] = MultiPoint(xy[j : j + n, :])
513
663
  j += n
514
664
 
515
- return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
665
+ da = xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
666
+ if node_count_name:
667
+ del da[node_count_name]
668
+ return da
516
669
 
517
670
 
518
- def lines_to_cf(lines: xr.DataArray | Sequence):
671
+ def lines_to_cf(lines: xr.DataArray | Sequence, *, names: GeometryNames | None = None):
519
672
  """Convert an iterable of lines (shapely.geometry.[Multi]Line) into a CF-compliant geometry dataset.
520
673
 
521
674
  Parameters
@@ -540,6 +693,9 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
540
693
  coord = None
541
694
  lines_ = np.array(lines)
542
695
 
696
+ if names is None:
697
+ names = GeometryNames()
698
+
543
699
  _, arr, offsets = to_ragged_array(lines_)
544
700
  x = arr[:, 0]
545
701
  y = arr[:, 1]
@@ -558,33 +714,23 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
558
714
 
559
715
  ds = xr.Dataset(
560
716
  data_vars={
561
- "node_count": xr.DataArray(node_count, dims=(dim,)),
562
- "part_node_count": xr.DataArray(part_node_count, dims=("part",)),
563
- "geometry_container": xr.DataArray(
564
- attrs={
565
- "geometry_type": "line",
566
- "node_count": "node_count",
567
- "part_node_count": "part_node_count",
568
- "node_coordinates": "x y",
569
- "coordinates": "crd_x crd_y",
570
- }
717
+ names.node_count: xr.DataArray(node_count, dims=(dim,)),
718
+ names.container_name: xr.DataArray(
719
+ data=np.nan,
720
+ attrs={"geometry_type": "line", **names.geometry_container_attrs},
571
721
  ),
572
722
  },
573
- coords={
574
- "x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
575
- "y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
576
- "crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
577
- "crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
578
- },
723
+ coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
579
724
  )
580
725
 
581
726
  if coord is not None:
582
727
  ds = ds.assign_coords({dim: coord})
583
728
 
584
729
  # Special case when we have no MultiLines
585
- if len(ds.part_node_count) == len(ds.node_count):
586
- ds = ds.drop_vars("part_node_count")
587
- del ds[GEOMETRY_CONTAINER_NAME].attrs["part_node_count"]
730
+ if len(part_node_count) != len(node_count):
731
+ ds[names.part_node_count] = xr.DataArray(part_node_count, dims=names.part_dim)
732
+ ds[names.container_name].attrs["part_node_count"] = names.part_node_count
733
+
588
734
  return ds
589
735
 
590
736
 
@@ -607,8 +753,10 @@ def cf_to_lines(ds: xr.Dataset):
607
753
  """
608
754
  from shapely import GeometryType, from_ragged_array
609
755
 
756
+ container_name = _assert_single_geometry_container(ds)
757
+
610
758
  # Shorthand for convenience
611
- geo = ds[GEOMETRY_CONTAINER_NAME].attrs
759
+ geo = ds[container_name].attrs
612
760
 
613
761
  # The features dimension name, defaults to the one of 'node_count'
614
762
  # or the dimension of the coordinates, if present.
@@ -645,10 +793,14 @@ def cf_to_lines(ds: xr.Dataset):
645
793
  # get items from lines or multilines depending on number of parts
646
794
  geoms = np.where(np.diff(offset2) == 1, lines[offset2[:-1]], multilines)
647
795
 
648
- return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
796
+ return xr.DataArray(
797
+ geoms, dims=node_count.dims, coords=node_count.coords
798
+ ).drop_vars(node_count_name)
649
799
 
650
800
 
651
- def polygons_to_cf(polygons: xr.DataArray | Sequence):
801
+ def polygons_to_cf(
802
+ polygons: xr.DataArray | Sequence, *, names: GeometryNames | None = None
803
+ ):
652
804
  """Convert an iterable of polygons (shapely.geometry.[Multi]Polygon) into a CF-compliant geometry dataset.
653
805
 
654
806
  Parameters
@@ -656,6 +808,9 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
656
808
  polygons : sequence of shapely.geometry.Polygon or MultiPolygon
657
809
  The sequence of [multi]polygons to translate to a CF dataset.
658
810
 
811
+ names: GeometryNames, optional
812
+ Structure that helps manipulate geometry attrs.
813
+
659
814
  Returns
660
815
  -------
661
816
  xr.Dataset
@@ -673,6 +828,9 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
673
828
  coord = None
674
829
  polygons_ = np.array(polygons)
675
830
 
831
+ if names is None:
832
+ names = GeometryNames()
833
+
676
834
  _, arr, offsets = to_ragged_array(polygons_)
677
835
  x = arr[:, 0]
678
836
  y = arr[:, 1]
@@ -696,40 +854,27 @@ def polygons_to_cf(polygons: xr.DataArray | Sequence):
696
854
 
697
855
  ds = xr.Dataset(
698
856
  data_vars={
699
- "node_count": xr.DataArray(node_count, dims=(dim,)),
700
- "interior_ring": xr.DataArray(interior_ring, dims=("part",)),
701
- "part_node_count": xr.DataArray(part_node_count, dims=("part",)),
702
- "geometry_container": xr.DataArray(
703
- attrs={
704
- "geometry_type": "polygon",
705
- "node_count": "node_count",
706
- "part_node_count": "part_node_count",
707
- "interior_ring": "interior_ring",
708
- "node_coordinates": "x y",
709
- "coordinates": "crd_x crd_y",
710
- }
857
+ names.node_count: xr.DataArray(node_count, dims=(dim,)),
858
+ names.container_name: xr.DataArray(
859
+ data=np.nan,
860
+ attrs={"geometry_type": "polygon", **names.geometry_container_attrs},
711
861
  ),
712
862
  },
713
- coords={
714
- "x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
715
- "y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
716
- "crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
717
- "crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
718
- },
863
+ coords=names.coords(x=x, y=y, crdX=crdX, crdY=crdY, dim=dim),
719
864
  )
720
865
 
721
866
  if coord is not None:
722
867
  ds = ds.assign_coords({dim: coord})
723
868
 
724
869
  # Special case when we have no MultiPolygons and no holes
725
- if len(ds.part_node_count) == len(ds.node_count):
726
- ds = ds.drop_vars("part_node_count")
727
- del ds[GEOMETRY_CONTAINER_NAME].attrs["part_node_count"]
870
+ if len(part_node_count) != len(node_count):
871
+ ds[names.part_node_count] = xr.DataArray(part_node_count, dims=names.part_dim)
872
+ ds[names.container_name].attrs["part_node_count"] = names.part_node_count
728
873
 
729
874
  # Special case when we have no holes
730
- if (ds.interior_ring == 0).all():
731
- ds = ds.drop_vars("interior_ring")
732
- del ds[GEOMETRY_CONTAINER_NAME].attrs["interior_ring"]
875
+ if (interior_ring != 0).any():
876
+ ds[names.interior_ring] = xr.DataArray(interior_ring, dims=names.part_dim)
877
+ ds[names.container_name].attrs["interior_ring"] = names.interior_ring
733
878
  return ds
734
879
 
735
880
 
@@ -752,8 +897,10 @@ def cf_to_polygons(ds: xr.Dataset):
752
897
  """
753
898
  from shapely import GeometryType, from_ragged_array
754
899
 
900
+ container_name = _assert_single_geometry_container(ds)
901
+
755
902
  # Shorthand for convenience
756
- geo = ds[GEOMETRY_CONTAINER_NAME].attrs
903
+ geo = ds[container_name].attrs
757
904
 
758
905
  # The features dimension name, defaults to the one of 'part_node_count'
759
906
  # or the dimension of the coordinates, if present.
@@ -805,4 +952,6 @@ def cf_to_polygons(ds: xr.Dataset):
805
952
  # get items from polygons or multipolygons depending on number of parts
806
953
  geoms = np.where(np.diff(offset3) == 1, polygons[offset3[:-1]], multipolygons)
807
954
 
808
- return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
955
+ return xr.DataArray(
956
+ geoms, dims=node_count.dims, coords=node_count.coords
957
+ ).drop_vars(node_count_name)
@@ -360,6 +360,8 @@ def test_shapely_to_cf_errors():
360
360
  )
361
361
  assert encoded["x"].attrs["standard_name"] == "projection_x_coordinate"
362
362
  assert encoded["y"].attrs["standard_name"] == "projection_y_coordinate"
363
+ for name in ["x", "y", "crd_x", "crd_y"]:
364
+ assert "grid_mapping" not in encoded[name].attrs
363
365
 
364
366
 
365
367
  @requires_shapely
@@ -500,6 +502,10 @@ def test_encode_decode(geometry_ds, polygon_geometry):
500
502
  )
501
503
  ).assign({"foo": ("geoms", [1, 2])})
502
504
 
503
- for ds in (geometry_ds[1], polygon_geometry.to_dataset(), geom_dim_ds):
505
+ polyds = (
506
+ polygon_geometry.rename("polygons").rename({"index": "index2"}).to_dataset()
507
+ )
508
+ multi_ds = xr.merge([polyds, geometry_ds[1]])
509
+ for ds in (geometry_ds[1], polygon_geometry.to_dataset(), geom_dim_ds, multi_ds):
504
510
  roundtripped = decode_geometries(encode_geometries(ds))
505
511
  xr.testing.assert_identical(ds, roundtripped)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cf_xarray
3
- Version: 0.9.3
3
+ Version: 0.9.4
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
@@ -10,6 +10,7 @@ codecov.yml
10
10
  pyproject.toml
11
11
  .binder/environment.yml
12
12
  .github/dependabot.yml
13
+ .github/release.yml
13
14
  .github/workflows/ci.yaml
14
15
  .github/workflows/parse_logs.py
15
16
  .github/workflows/pypi.yaml
@@ -26,6 +26,7 @@ Geometries
26
26
  geometry.encode_geometries
27
27
  geometry.shapely_to_cf
28
28
  geometry.cf_to_shapely
29
+ geometry.GeometryNames
29
30
 
30
31
  .. currentmodule:: xarray
31
32
 
@@ -97,9 +97,8 @@ ds.identical(decoded)
97
97
 
98
98
  The following limitations can be relaxed in the future. PRs welcome!
99
99
 
100
- 1. cf-xarray uses `"geometry_container"` as the name for the geometry variable always
101
- 1. cf-xarray only supports decoding a single geometry in a Dataset.
102
- 1. CF xarray will not set the `"geometry"` attribute that links a variable to a geometry by default unless the geometry variable is a dimension coordiante for that variable. This heuristic works OK for vector data cubes (e.g. [xvec](https://xvec.readthedocs.io/en/stable/)). You should set the `"geometry"` attribute manually otherwise. Suggestions for better behaviour here are very welcome.
100
+ 1. cf-xarray uses `"geometry_container"` as the name for the geometry variable always. If there are multiple geometry variables then `"geometry_N"`is used where N is an integer >= 0. cf-xarray behaves similarly for all associated geometry variables names: i.e. `"node"`, `"node_count"`, `"part_node_count"`, `"part"`, `"interior_ring"`. `"x"`, `"y"` (with suffixes if needed) are always the node coordinate variable names, and `"crd_x"`, `"crd_y"` are the nominal X, Y coordinate locations. None of this is configurable at the moment.
101
+ 1. CF xarray will not set the `"geometry"` attribute that links a variable to a geometry by default unless the geometry variable is a dimension coordinate for that variable. This heuristic works OK for vector data cubes (e.g. [xvec](https://xvec.readthedocs.io/en/stable/)). You should set the `"geometry"` attribute manually otherwise. Suggestions for better behaviour here are very welcome.
103
102
 
104
103
  ## Lower-level conversions
105
104
 
@@ -1 +0,0 @@
1
- __version__ = "0.9.3"
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
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