cf-xarray 0.8.7__tar.gz → 0.8.8__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.
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/pypi.yaml +5 -5
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/testpypi-release.yaml +4 -4
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.pre-commit-config.yaml +4 -4
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/PKG-INFO +1 -1
- cf_xarray-0.8.8/cf_xarray/_version.py +1 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/datasets.py +10 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/formatting.py +55 -9
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/geometry.py +179 -33
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_accessor.py +27 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_geometry.py +239 -15
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/PKG-INFO +2 -2
- cf_xarray-0.8.8/codecov.yml +3 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/whats-new.rst +6 -0
- cf_xarray-0.8.7/cf_xarray/_version.py +0 -1
- cf_xarray-0.8.7/codecov.yml +0 -2
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.binder/environment.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.deepsource.toml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/dependabot.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/ci.yaml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/parse_logs.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/upstream-dev-ci.yaml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.gitignore +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.readthedocs.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.tributors +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/CITATION.cff +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/LICENSE +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/README.rst +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/__init__.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/accessor.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/coding.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/criteria.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/helpers.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/options.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/py.typed +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/scripts/make_doc.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/scripts/print_versions.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/sgrid.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/__init__.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_coding.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_helpers.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_options.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_scripts.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_units.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/units.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/utils.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/SOURCES.txt +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/dependency_links.txt +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/requires.txt +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/top_level.txt +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/doc.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/environment-no-optional-deps.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/environment.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/upstream-dev-env.yml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_averaged.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_error.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_nonunique.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/Makefile +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/dataset-diagram-logo.tex +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/full-logo.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/logo.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/logo.svg +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/rich-repr-example.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/style.css +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/api.rst +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/bounds.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/cartopy_rotated_pole.png +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/coding.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/conf.py +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/contributing.rst +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/coord_axes.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/custom-criteria.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/dsg.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/examples/introduction.ipynb +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/faq.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/flags.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/geometry.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/grid_mappings.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/howtouse.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/index.rst +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/make.bat +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/parametricz.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/plotting.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/provenance.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/quickstart.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/roadmap.rst +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/selecting.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/sgrid_ugrid.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/units.md +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/pyproject.toml +0 -0
- {cf_xarray-0.8.7 → cf_xarray-0.8.8}/setup.cfg +0 -0
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
- uses: actions/checkout@v4
|
|
16
16
|
with:
|
|
17
17
|
fetch-depth: 0
|
|
18
|
-
- uses: actions/setup-python@
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
19
|
name: Install Python
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.10"
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
else
|
|
42
42
|
echo "✅ Looks good"
|
|
43
43
|
fi
|
|
44
|
-
- uses: actions/upload-artifact@
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
45
|
with:
|
|
46
46
|
name: releases
|
|
47
47
|
path: dist
|
|
@@ -50,11 +50,11 @@ jobs:
|
|
|
50
50
|
needs: build-artifacts
|
|
51
51
|
runs-on: ubuntu-latest
|
|
52
52
|
steps:
|
|
53
|
-
- uses: actions/setup-python@
|
|
53
|
+
- uses: actions/setup-python@v5
|
|
54
54
|
name: Install Python
|
|
55
55
|
with:
|
|
56
56
|
python-version: "3.10"
|
|
57
|
-
- uses: actions/download-artifact@
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
58
|
with:
|
|
59
59
|
name: releases
|
|
60
60
|
path: dist
|
|
@@ -91,7 +91,7 @@ jobs:
|
|
|
91
91
|
id-token: write
|
|
92
92
|
|
|
93
93
|
steps:
|
|
94
|
-
- uses: actions/download-artifact@
|
|
94
|
+
- uses: actions/download-artifact@v4
|
|
95
95
|
with:
|
|
96
96
|
name: releases
|
|
97
97
|
path: dist
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
with:
|
|
22
22
|
fetch-depth: 0
|
|
23
23
|
|
|
24
|
-
- uses: actions/setup-python@
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
25
|
name: Install Python
|
|
26
26
|
with:
|
|
27
27
|
python-version: "3.10"
|
|
@@ -53,7 +53,7 @@ jobs:
|
|
|
53
53
|
echo "✅ Looks good"
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
|
-
- uses: actions/upload-artifact@
|
|
56
|
+
- uses: actions/upload-artifact@v4
|
|
57
57
|
with:
|
|
58
58
|
name: releases
|
|
59
59
|
path: dist
|
|
@@ -62,11 +62,11 @@ jobs:
|
|
|
62
62
|
needs: build-artifacts
|
|
63
63
|
runs-on: ubuntu-latest
|
|
64
64
|
steps:
|
|
65
|
-
- uses: actions/setup-python@
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
66
|
name: Install Python
|
|
67
67
|
with:
|
|
68
68
|
python-version: "3.10"
|
|
69
|
-
- uses: actions/download-artifact@
|
|
69
|
+
- uses: actions/download-artifact@v4
|
|
70
70
|
with:
|
|
71
71
|
name: releases
|
|
72
72
|
path: dist
|
|
@@ -10,13 +10,13 @@ repos:
|
|
|
10
10
|
|
|
11
11
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
12
12
|
# Ruff version.
|
|
13
|
-
rev: 'v0.1.
|
|
13
|
+
rev: 'v0.1.9'
|
|
14
14
|
hooks:
|
|
15
15
|
- id: ruff
|
|
16
16
|
args: ["--show-fixes", "--fix"]
|
|
17
17
|
|
|
18
18
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
|
19
|
-
rev: 23.
|
|
19
|
+
rev: 23.12.1
|
|
20
20
|
hooks:
|
|
21
21
|
- id: black
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ repos:
|
|
|
36
36
|
- mdformat-myst
|
|
37
37
|
|
|
38
38
|
- repo: https://github.com/nbQA-dev/nbQA
|
|
39
|
-
rev: 1.7.
|
|
39
|
+
rev: 1.7.1
|
|
40
40
|
hooks:
|
|
41
41
|
- id: nbqa-black
|
|
42
42
|
- id: nbqa-ruff
|
|
@@ -56,7 +56,7 @@ repos:
|
|
|
56
56
|
- id: debug-statements
|
|
57
57
|
|
|
58
58
|
- repo: https://github.com/keewis/blackdoc
|
|
59
|
-
rev: v0.3.
|
|
59
|
+
rev: v0.3.9
|
|
60
60
|
hooks:
|
|
61
61
|
- id: blackdoc
|
|
62
62
|
files: .+\.py$
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.8"
|
|
@@ -503,6 +503,16 @@ flag_indep = xr.DataArray(
|
|
|
503
503
|
name="flag_var",
|
|
504
504
|
)
|
|
505
505
|
|
|
506
|
+
flag_indep_uint16 = xr.DataArray(
|
|
507
|
+
np.array([1, 10, 100, 1000, 10000, 65535], dtype=np.uint16),
|
|
508
|
+
dims=("time",),
|
|
509
|
+
attrs={
|
|
510
|
+
"flag_masks": [2**i for i in range(16)],
|
|
511
|
+
"flag_meanings": " ".join([f"flag_{2**i}" for i in range(16)]),
|
|
512
|
+
"standard_name": "flag_independent",
|
|
513
|
+
},
|
|
514
|
+
name="flag_var",
|
|
515
|
+
)
|
|
506
516
|
|
|
507
517
|
flag_mix = xr.DataArray(
|
|
508
518
|
np.array([4, 8, 13, 5, 10, 14, 7, 3], np.uint8),
|
|
@@ -151,8 +151,47 @@ def _maybe_panel(textgen, title: str, rich: bool):
|
|
|
151
151
|
return title + ":\n" + text
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
def
|
|
155
|
-
|
|
154
|
+
def _get_bit_length(dtype):
|
|
155
|
+
# Check if dtype is a numpy dtype, if not, convert it
|
|
156
|
+
if not isinstance(dtype, np.dtype):
|
|
157
|
+
dtype = np.dtype(dtype)
|
|
158
|
+
|
|
159
|
+
# Calculate the bit length
|
|
160
|
+
bit_length = 8 * dtype.itemsize
|
|
161
|
+
|
|
162
|
+
return bit_length
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _unpackbits(mask, bit_length):
|
|
166
|
+
# Ensure the array is a numpy array
|
|
167
|
+
arr = np.asarray(mask)
|
|
168
|
+
|
|
169
|
+
# Create an output array of the appropriate shape
|
|
170
|
+
output_shape = arr.shape + (bit_length,)
|
|
171
|
+
output = np.zeros(output_shape, dtype=np.uint8)
|
|
172
|
+
|
|
173
|
+
# Unpack bits
|
|
174
|
+
for i in range(bit_length):
|
|
175
|
+
output[..., i] = (arr >> i) & 1
|
|
176
|
+
|
|
177
|
+
return output[..., ::-1]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _max_chars_for_bit_length(bit_length):
|
|
181
|
+
"""
|
|
182
|
+
Find the maximum characters needed for a fixed-width display
|
|
183
|
+
for integer values of a certain bit_length. Use calculation
|
|
184
|
+
for signed integers, since it conservatively will always have
|
|
185
|
+
enough characters for signed or unsigned.
|
|
186
|
+
"""
|
|
187
|
+
# Maximum value for signed integers of this bit length
|
|
188
|
+
max_val = 2 ** (bit_length - 1) - 1
|
|
189
|
+
# Add 1 for the negative sign
|
|
190
|
+
return len(str(max_val)) + 1
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def find_set_bits(mask, value, repeated_masks, bit_length):
|
|
194
|
+
bitpos = np.arange(bit_length)[::-1]
|
|
156
195
|
if mask not in repeated_masks:
|
|
157
196
|
if value == 0:
|
|
158
197
|
return [-1]
|
|
@@ -161,8 +200,8 @@ def find_set_bits(mask, value, repeated_masks):
|
|
|
161
200
|
else:
|
|
162
201
|
return [int(np.log2(mask))]
|
|
163
202
|
else:
|
|
164
|
-
allset = bitpos[
|
|
165
|
-
setbits = bitpos[
|
|
203
|
+
allset = bitpos[_unpackbits(mask, bit_length) == 1]
|
|
204
|
+
setbits = bitpos[_unpackbits(mask & value, bit_length) == 1]
|
|
166
205
|
return [b if abs(b) in setbits else -b for b in allset]
|
|
167
206
|
|
|
168
207
|
|
|
@@ -184,6 +223,11 @@ def _format_flags(accessor, rich):
|
|
|
184
223
|
# for f, (m, _) in flag_dict.items()
|
|
185
224
|
# if m is not None and m not in repeated_masks
|
|
186
225
|
# ]
|
|
226
|
+
|
|
227
|
+
bit_length = _get_bit_length(accessor._obj.dtype)
|
|
228
|
+
mask_width = _max_chars_for_bit_length(bit_length)
|
|
229
|
+
key_width = max(len(key) for key in flag_dict)
|
|
230
|
+
|
|
187
231
|
bit_text = []
|
|
188
232
|
value_text = []
|
|
189
233
|
for key, (mask, value) in flag_dict.items():
|
|
@@ -191,8 +235,8 @@ def _format_flags(accessor, rich):
|
|
|
191
235
|
bit_text.append("✗" if rich else "")
|
|
192
236
|
value_text.append(str(value))
|
|
193
237
|
continue
|
|
194
|
-
bits = find_set_bits(mask, value, repeated_masks)
|
|
195
|
-
bitstring = ["."] *
|
|
238
|
+
bits = find_set_bits(mask, value, repeated_masks, bit_length)
|
|
239
|
+
bitstring = ["."] * bit_length
|
|
196
240
|
if bits == [-1]:
|
|
197
241
|
continue
|
|
198
242
|
else:
|
|
@@ -200,9 +244,9 @@ def _format_flags(accessor, rich):
|
|
|
200
244
|
bitstring[abs(b)] = _format_cf_name("1" if b >= 0 else "0", rich)
|
|
201
245
|
text = "".join(bitstring[::-1])
|
|
202
246
|
value_text.append(
|
|
203
|
-
f"{mask} & {value}"
|
|
247
|
+
f"{mask:{mask_width}} & {value}"
|
|
204
248
|
if key in excl_flags and value is not None
|
|
205
|
-
else
|
|
249
|
+
else f"{mask:{mask_width}}"
|
|
206
250
|
)
|
|
207
251
|
bit_text.append(text if rich else f" / Bit: {text}")
|
|
208
252
|
|
|
@@ -230,7 +274,9 @@ def _format_flags(accessor, rich):
|
|
|
230
274
|
else:
|
|
231
275
|
rows = []
|
|
232
276
|
for val, bit, key in zip(value_text, bit_text, flag_dict):
|
|
233
|
-
rows.append(
|
|
277
|
+
rows.append(
|
|
278
|
+
f"{TAB}{_format_cf_name(key, rich):>{key_width}}: {TAB} {val} {bit}"
|
|
279
|
+
)
|
|
234
280
|
return _print_rows("Flag Meanings", rows, rich)
|
|
235
281
|
|
|
236
282
|
|
|
@@ -75,9 +75,6 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
|
|
|
75
75
|
"""
|
|
76
76
|
Convert a DataArray with shapely geometry objects into a CF-compliant dataset.
|
|
77
77
|
|
|
78
|
-
.. warning::
|
|
79
|
-
Only point geometries are currently implemented.
|
|
80
|
-
|
|
81
78
|
Parameters
|
|
82
79
|
----------
|
|
83
80
|
geometries : sequence of shapely geometries or xarray.DataArray
|
|
@@ -115,7 +112,7 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
|
|
|
115
112
|
elif types.issubset({"LineString", "MultiLineString"}):
|
|
116
113
|
ds = lines_to_cf(geometries)
|
|
117
114
|
elif types.issubset({"Polygon", "MultiPolygon"}):
|
|
118
|
-
|
|
115
|
+
ds = polygons_to_cf(geometries)
|
|
119
116
|
else:
|
|
120
117
|
raise ValueError(
|
|
121
118
|
f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
|
|
@@ -142,9 +139,6 @@ def cf_to_shapely(ds: xr.Dataset):
|
|
|
142
139
|
"""
|
|
143
140
|
Convert geometries stored in a CF-compliant way to shapely objects stored in a single variable.
|
|
144
141
|
|
|
145
|
-
.. warning::
|
|
146
|
-
Only point geometries are currently implemented.
|
|
147
|
-
|
|
148
142
|
Parameters
|
|
149
143
|
----------
|
|
150
144
|
ds : xr.Dataset
|
|
@@ -168,7 +162,7 @@ def cf_to_shapely(ds: xr.Dataset):
|
|
|
168
162
|
elif geom_type == "line":
|
|
169
163
|
geometries = cf_to_lines(ds)
|
|
170
164
|
elif geom_type == "polygon":
|
|
171
|
-
|
|
165
|
+
geometries = cf_to_polygons(ds)
|
|
172
166
|
else:
|
|
173
167
|
raise ValueError(
|
|
174
168
|
f"Valid CF geometry types are 'point', 'line' and 'polygon'. Got {geom_type}"
|
|
@@ -328,13 +322,13 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
|
|
|
328
322
|
x = arr[:, 0]
|
|
329
323
|
y = arr[:, 1]
|
|
330
324
|
|
|
331
|
-
|
|
325
|
+
part_node_count = np.diff(offsets[0])
|
|
332
326
|
if len(offsets) == 1:
|
|
333
327
|
indices = offsets[0]
|
|
334
|
-
|
|
328
|
+
node_count = part_node_count
|
|
335
329
|
else:
|
|
336
330
|
indices = np.take(offsets[0], offsets[1])
|
|
337
|
-
|
|
331
|
+
node_count = np.diff(indices)
|
|
338
332
|
|
|
339
333
|
geom_coords = arr.take(indices[:-1], 0)
|
|
340
334
|
crdX = geom_coords[:, 0]
|
|
@@ -342,8 +336,8 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
|
|
|
342
336
|
|
|
343
337
|
ds = xr.Dataset(
|
|
344
338
|
data_vars={
|
|
345
|
-
"node_count": xr.DataArray(node_count, dims=(
|
|
346
|
-
"part_node_count": xr.DataArray(part_node_count, dims=(
|
|
339
|
+
"node_count": xr.DataArray(node_count, dims=(dim,)),
|
|
340
|
+
"part_node_count": xr.DataArray(part_node_count, dims=("part",)),
|
|
347
341
|
"geometry_container": xr.DataArray(
|
|
348
342
|
attrs={
|
|
349
343
|
"geometry_type": "line",
|
|
@@ -394,7 +388,7 @@ def cf_to_lines(ds: xr.Dataset):
|
|
|
394
388
|
# Shorthand for convenience
|
|
395
389
|
geo = ds.geometry_container.attrs
|
|
396
390
|
|
|
397
|
-
# The features dimension name, defaults to the one of '
|
|
391
|
+
# The features dimension name, defaults to the one of 'node_count'
|
|
398
392
|
# or the dimension of the coordinates, if present.
|
|
399
393
|
feat_dim = None
|
|
400
394
|
if "coordinates" in geo:
|
|
@@ -405,36 +399,188 @@ def cf_to_lines(ds: xr.Dataset):
|
|
|
405
399
|
xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
|
|
406
400
|
|
|
407
401
|
node_count_name = geo.get("node_count")
|
|
408
|
-
part_node_count_name = geo.get("part_node_count")
|
|
402
|
+
part_node_count_name = geo.get("part_node_count", node_count_name)
|
|
409
403
|
if node_count_name is None:
|
|
410
404
|
raise ValueError("'node_count' must be provided for line geometries")
|
|
411
405
|
else:
|
|
412
406
|
node_count = ds[node_count_name]
|
|
407
|
+
feat_dim = feat_dim or "index"
|
|
408
|
+
if feat_dim in ds.coords:
|
|
409
|
+
node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
|
|
413
410
|
|
|
414
|
-
|
|
411
|
+
# first get geometries for all the parts
|
|
412
|
+
part_node_count = ds[part_node_count_name]
|
|
413
|
+
offset1 = np.insert(np.cumsum(part_node_count.values), 0, 0)
|
|
415
414
|
lines = from_ragged_array(GeometryType.LINESTRING, xy, offsets=(offset1,))
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
# get index of offset2 values that are edges for part_node_count
|
|
417
|
+
offset2 = np.nonzero(np.isin(offset1, np.insert(np.cumsum(node_count), 0, 0)))[0]
|
|
418
|
+
|
|
419
|
+
multilines = from_ragged_array(
|
|
420
|
+
GeometryType.MULTILINESTRING, xy, offsets=(offset1, offset2)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# get items from lines or multilines depending on number of parts
|
|
424
|
+
geoms = np.where(np.diff(offset2) == 1, lines[offset2[:-1]], multilines)
|
|
425
|
+
|
|
426
|
+
return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def polygons_to_cf(polygons: xr.DataArray | Sequence):
|
|
430
|
+
"""Convert an iterable of polygons (shapely.geometry.[Multi]Polygon) into a CF-compliant geometry dataset.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
polygons : sequence of shapely.geometry.Polygon or MultiPolygon
|
|
435
|
+
The sequence of [multi]polygons to translate to a CF dataset.
|
|
436
|
+
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
xr.Dataset
|
|
440
|
+
A Dataset with variables 'x', 'y', 'crd_x', 'crd_y', 'node_count' and 'geometry_container'
|
|
441
|
+
and optionally 'part_node_count'.
|
|
442
|
+
"""
|
|
443
|
+
from shapely import to_ragged_array
|
|
444
|
+
|
|
445
|
+
if isinstance(polygons, xr.DataArray):
|
|
446
|
+
dim = polygons.dims[0]
|
|
447
|
+
coord = polygons[dim] if dim in polygons.coords else None
|
|
448
|
+
polygons_ = polygons.values
|
|
449
|
+
else:
|
|
450
|
+
dim = "index"
|
|
451
|
+
coord = None
|
|
452
|
+
polygons_ = np.array(polygons)
|
|
453
|
+
|
|
454
|
+
_, arr, offsets = to_ragged_array(polygons_)
|
|
455
|
+
x = arr[:, 0]
|
|
456
|
+
y = arr[:, 1]
|
|
457
|
+
|
|
458
|
+
part_node_count = np.diff(offsets[0])
|
|
459
|
+
if len(offsets) == 1:
|
|
460
|
+
indices = offsets[0]
|
|
461
|
+
node_count = part_node_count
|
|
462
|
+
elif len(offsets) >= 2:
|
|
463
|
+
indices = np.take(offsets[0], offsets[1])
|
|
464
|
+
interior_ring = np.isin(offsets[0], indices, invert=True)[:-1].astype(int)
|
|
465
|
+
|
|
466
|
+
if len(offsets) == 3:
|
|
467
|
+
indices = np.take(indices, offsets[2])
|
|
468
|
+
|
|
469
|
+
node_count = np.diff(indices)
|
|
470
|
+
|
|
471
|
+
geom_coords = arr.take(indices[:-1], 0)
|
|
472
|
+
crdX = geom_coords[:, 0]
|
|
473
|
+
crdY = geom_coords[:, 1]
|
|
474
|
+
|
|
475
|
+
ds = xr.Dataset(
|
|
476
|
+
data_vars={
|
|
477
|
+
"node_count": xr.DataArray(node_count, dims=(dim,)),
|
|
478
|
+
"interior_ring": xr.DataArray(interior_ring, dims=("part",)),
|
|
479
|
+
"part_node_count": xr.DataArray(part_node_count, dims=("part",)),
|
|
480
|
+
"geometry_container": xr.DataArray(
|
|
481
|
+
attrs={
|
|
482
|
+
"geometry_type": "polygon",
|
|
483
|
+
"node_count": "node_count",
|
|
484
|
+
"part_node_count": "part_node_count",
|
|
485
|
+
"interior_ring": "interior_ring",
|
|
486
|
+
"node_coordinates": "x y",
|
|
487
|
+
"coordinates": "crd_x crd_y",
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
},
|
|
491
|
+
coords={
|
|
492
|
+
"x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
|
|
493
|
+
"y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
|
|
494
|
+
"crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
|
|
495
|
+
"crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
|
|
496
|
+
},
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if coord is not None:
|
|
500
|
+
ds = ds.assign_coords({dim: coord})
|
|
501
|
+
|
|
502
|
+
# Special case when we have no MultiPolygons and no holes
|
|
503
|
+
if len(ds.part_node_count) == len(ds.node_count):
|
|
504
|
+
ds = ds.drop_vars("part_node_count")
|
|
505
|
+
del ds.geometry_container.attrs["part_node_count"]
|
|
506
|
+
|
|
507
|
+
# Special case when we have no holes
|
|
508
|
+
if (ds.interior_ring == 0).all():
|
|
509
|
+
ds = ds.drop_vars("interior_ring")
|
|
510
|
+
del ds.geometry_container.attrs["interior_ring"]
|
|
511
|
+
return ds
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def cf_to_polygons(ds: xr.Dataset):
|
|
515
|
+
"""Convert polygon geometries stored in a CF-compliant way to shapely polygons stored in a single variable.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
ds : xr.Dataset
|
|
520
|
+
A dataset with CF-compliant polygon geometries.
|
|
521
|
+
Must have a "geometry_container" variable with at least a 'node_coordinates' attribute.
|
|
522
|
+
Must also have the two 1D variables listed by this attribute.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
geometry : xr.DataArray
|
|
527
|
+
A 1D array of shapely.geometry.[Multi]Polygon objects.
|
|
528
|
+
It has the same dimension as the ``part_node_count`` or the coordinates variables, or
|
|
529
|
+
``'features'`` if those were not present in ``ds``.
|
|
530
|
+
"""
|
|
531
|
+
from shapely import GeometryType, from_ragged_array
|
|
532
|
+
|
|
533
|
+
# Shorthand for convenience
|
|
534
|
+
geo = ds.geometry_container.attrs
|
|
535
|
+
|
|
536
|
+
# The features dimension name, defaults to the one of 'part_node_count'
|
|
537
|
+
# or the dimension of the coordinates, if present.
|
|
538
|
+
feat_dim = None
|
|
539
|
+
if "coordinates" in geo:
|
|
540
|
+
xcoord_name, _ = geo["coordinates"].split(" ")
|
|
541
|
+
(feat_dim,) = ds[xcoord_name].dims
|
|
542
|
+
|
|
543
|
+
x_name, y_name = geo["node_coordinates"].split(" ")
|
|
544
|
+
xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
|
|
545
|
+
|
|
546
|
+
node_count_name = geo.get("node_count")
|
|
547
|
+
part_node_count_name = geo.get("part_node_count", node_count_name)
|
|
548
|
+
interior_ring_name = geo.get("interior_ring")
|
|
549
|
+
|
|
550
|
+
if node_count_name is None:
|
|
551
|
+
raise ValueError("'node_count' must be provided for polygon geometries")
|
|
552
|
+
else:
|
|
553
|
+
node_count = ds[node_count_name]
|
|
420
554
|
feat_dim = feat_dim or "index"
|
|
421
|
-
part_node_count = xr.DataArray(node_count.values, dims=(feat_dim,))
|
|
422
555
|
if feat_dim in ds.coords:
|
|
423
|
-
|
|
556
|
+
node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
|
|
424
557
|
|
|
425
|
-
|
|
558
|
+
# first get geometries for all the rings
|
|
559
|
+
part_node_count = ds[part_node_count_name]
|
|
560
|
+
offset1 = np.insert(np.cumsum(part_node_count.values), 0, 0)
|
|
561
|
+
|
|
562
|
+
if interior_ring_name is None:
|
|
563
|
+
offset2 = np.array(list(range(len(offset1))))
|
|
426
564
|
else:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
offset2 = np.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
565
|
+
interior_ring = ds[interior_ring_name]
|
|
566
|
+
if not interior_ring[0] == 0:
|
|
567
|
+
raise ValueError("coordinate array must start with an exterior ring")
|
|
568
|
+
offset2 = np.append(np.where(interior_ring == 0)[0], [len(part_node_count)])
|
|
569
|
+
|
|
570
|
+
polygons = from_ragged_array(GeometryType.POLYGON, xy, offsets=(offset1, offset2))
|
|
571
|
+
|
|
572
|
+
# get index of offset2 values that are edges for node_count
|
|
573
|
+
offset3 = np.nonzero(
|
|
574
|
+
np.isin(
|
|
575
|
+
offset2,
|
|
576
|
+
np.nonzero(np.isin(offset1, np.insert(np.cumsum(node_count), 0, 0)))[0],
|
|
435
577
|
)
|
|
578
|
+
)[0]
|
|
579
|
+
multipolygons = from_ragged_array(
|
|
580
|
+
GeometryType.MULTIPOLYGON, xy, offsets=(offset1, offset2, offset3)
|
|
581
|
+
)
|
|
436
582
|
|
|
437
|
-
|
|
438
|
-
|
|
583
|
+
# get items from polygons or multipolygons depending on number of parts
|
|
584
|
+
geoms = np.where(np.diff(offset3) == 1, polygons[offset3[:-1]], multipolygons)
|
|
439
585
|
|
|
440
|
-
return xr.DataArray(geoms, dims=
|
|
586
|
+
return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
|
|
@@ -26,6 +26,7 @@ from ..datasets import (
|
|
|
26
26
|
dsg,
|
|
27
27
|
flag_excl,
|
|
28
28
|
flag_indep,
|
|
29
|
+
flag_indep_uint16,
|
|
29
30
|
flag_mix,
|
|
30
31
|
forecast,
|
|
31
32
|
mollwds,
|
|
@@ -164,6 +165,7 @@ def test_repr() -> None:
|
|
|
164
165
|
# Flag DataArray
|
|
165
166
|
assert "Flag Variable" in repr(flag_excl.cf)
|
|
166
167
|
assert "Flag Variable" in repr(flag_indep.cf)
|
|
168
|
+
assert "Flag Variable" in repr(flag_indep_uint16.cf)
|
|
167
169
|
assert "Flag Variable" in repr(flag_mix.cf)
|
|
168
170
|
assert "Flag Variable" in repr(basin.cf)
|
|
169
171
|
|
|
@@ -1837,6 +1839,30 @@ class TestFlags:
|
|
|
1837
1839
|
res = flag_indep.cf.flags[name]
|
|
1838
1840
|
np.testing.assert_equal(res.to_numpy(), expected[i])
|
|
1839
1841
|
|
|
1842
|
+
def test_flag_indep_uint16(self) -> None:
|
|
1843
|
+
expected = [
|
|
1844
|
+
[True, False, False, False, False, True], # bit 1
|
|
1845
|
+
[False, True, False, False, False, True], # bit 2
|
|
1846
|
+
[False, False, True, False, False, True], # bit 4
|
|
1847
|
+
[False, True, False, True, False, True], # bit 8
|
|
1848
|
+
[False, False, False, False, True, True], # bit 16
|
|
1849
|
+
[False, False, True, True, False, True], # bit 32
|
|
1850
|
+
[False, False, True, True, False, True], # bit 64
|
|
1851
|
+
[False, False, False, True, False, True], # bit 128
|
|
1852
|
+
[False, False, False, True, True, True], # bit 256
|
|
1853
|
+
[False, False, False, True, True, True], # bit 512
|
|
1854
|
+
[False, False, False, False, True, True], # bit 1024
|
|
1855
|
+
[False, False, False, False, False, True], # bit 2048
|
|
1856
|
+
[False, False, False, False, False, True], # bit 4096
|
|
1857
|
+
[False, False, False, False, True, True], # bit 8192
|
|
1858
|
+
[False, False, False, False, False, True], # bit 16384
|
|
1859
|
+
[False, False, False, False, False, True], # bit 32768
|
|
1860
|
+
]
|
|
1861
|
+
for i in range(16):
|
|
1862
|
+
name = f"flag_{2**i}"
|
|
1863
|
+
res = flag_indep_uint16.cf.flags[name]
|
|
1864
|
+
np.testing.assert_equal(res.to_numpy(), expected[i])
|
|
1865
|
+
|
|
1840
1866
|
def test_flag_mix(self) -> None:
|
|
1841
1867
|
expected = [
|
|
1842
1868
|
[False, False, True, True, False, False, True, True], # flag 1
|
|
@@ -1983,6 +2009,7 @@ def test_curvefit() -> None:
|
|
|
1983
2009
|
[basin, "Flag Variable"],
|
|
1984
2010
|
[flag_mix, "Flag Variable"],
|
|
1985
2011
|
[flag_indep, "Flag Variable"],
|
|
2012
|
+
[flag_indep_uint16, "Flag Variable"],
|
|
1986
2013
|
[flag_excl, "Flag Variable"],
|
|
1987
2014
|
[dsg, "Discrete Sampling Geometry"],
|
|
1988
2015
|
),
|
|
@@ -71,8 +71,8 @@ def geometry_line_ds():
|
|
|
71
71
|
y=xr.DataArray(
|
|
72
72
|
[0, 2, 4, 6, 0, 0, 1, 1.0, 2.0, 9.5], dims=("node",), attrs={"axis": "Y"}
|
|
73
73
|
),
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
node_count=xr.DataArray([4, 3, 3], dims=("index",)),
|
|
75
|
+
part_node_count=xr.DataArray([2, 2, 3, 3], dims=("part",)),
|
|
76
76
|
crd_x=xr.DataArray([0.0, 0.0, 1.0], dims=("index",), attrs={"nodes": "x"}),
|
|
77
77
|
crd_y=xr.DataArray([0.0, 0.0, 1.0], dims=("index",), attrs={"nodes": "y"}),
|
|
78
78
|
geometry_container=xr.DataArray(
|
|
@@ -108,7 +108,7 @@ def geometry_line_without_multilines_ds():
|
|
|
108
108
|
cf_ds = ds.assign(
|
|
109
109
|
x=xr.DataArray([0, 1, 1, 1.0, 2.0, 1.7], dims=("node",), attrs={"axis": "X"}),
|
|
110
110
|
y=xr.DataArray([0, 0, 1, 1.0, 2.0, 9.5], dims=("node",), attrs={"axis": "Y"}),
|
|
111
|
-
node_count=xr.DataArray([3, 3], dims=("
|
|
111
|
+
node_count=xr.DataArray([3, 3], dims=("index",)),
|
|
112
112
|
crd_x=xr.DataArray([0.0, 1.0], dims=("index",), attrs={"nodes": "x"}),
|
|
113
113
|
crd_y=xr.DataArray([0.0, 1.0], dims=("index",), attrs={"nodes": "y"}),
|
|
114
114
|
geometry_container=xr.DataArray(
|
|
@@ -126,6 +126,154 @@ def geometry_line_without_multilines_ds():
|
|
|
126
126
|
return cf_ds, shp_da
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
@pytest.fixture
|
|
130
|
+
def geometry_polygon_without_holes_ds():
|
|
131
|
+
from shapely.geometry import Polygon
|
|
132
|
+
|
|
133
|
+
# empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
|
|
134
|
+
geoms = np.empty(2, dtype=object)
|
|
135
|
+
geoms[:] = [
|
|
136
|
+
Polygon(([50, 0], [40, 15], [30, 0])),
|
|
137
|
+
Polygon(([70, 50], [60, 65], [50, 50])),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
ds = xr.Dataset()
|
|
141
|
+
shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
|
|
142
|
+
|
|
143
|
+
cf_ds = ds.assign(
|
|
144
|
+
x=xr.DataArray(
|
|
145
|
+
[50, 40, 30, 50, 70, 60, 50, 70], dims=("node",), attrs={"axis": "X"}
|
|
146
|
+
),
|
|
147
|
+
y=xr.DataArray(
|
|
148
|
+
[0, 15, 0, 0, 50, 65, 50, 50], dims=("node",), attrs={"axis": "Y"}
|
|
149
|
+
),
|
|
150
|
+
node_count=xr.DataArray([4, 4], dims=("index",)),
|
|
151
|
+
crd_x=xr.DataArray([50, 70], dims=("index",), attrs={"nodes": "x"}),
|
|
152
|
+
crd_y=xr.DataArray([0, 50], dims=("index",), attrs={"nodes": "y"}),
|
|
153
|
+
geometry_container=xr.DataArray(
|
|
154
|
+
attrs={
|
|
155
|
+
"geometry_type": "polygon",
|
|
156
|
+
"node_count": "node_count",
|
|
157
|
+
"node_coordinates": "x y",
|
|
158
|
+
"coordinates": "crd_x crd_y",
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
|
|
164
|
+
|
|
165
|
+
return cf_ds, shp_da
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.fixture
|
|
169
|
+
def geometry_polygon_without_multipolygons_ds():
|
|
170
|
+
from shapely.geometry import Polygon
|
|
171
|
+
|
|
172
|
+
# empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
|
|
173
|
+
geoms = np.empty(2, dtype=object)
|
|
174
|
+
geoms[:] = [
|
|
175
|
+
Polygon(([50, 0], [40, 15], [30, 0])),
|
|
176
|
+
Polygon(
|
|
177
|
+
([70, 50], [60, 65], [50, 50]),
|
|
178
|
+
[
|
|
179
|
+
([55, 55], [60, 60], [65, 55]),
|
|
180
|
+
],
|
|
181
|
+
),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
ds = xr.Dataset()
|
|
185
|
+
shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
|
|
186
|
+
|
|
187
|
+
cf_ds = ds.assign(
|
|
188
|
+
x=xr.DataArray(
|
|
189
|
+
[50, 40, 30, 50, 70, 60, 50, 70, 55, 60, 65, 55],
|
|
190
|
+
dims=("node",),
|
|
191
|
+
attrs={"axis": "X"},
|
|
192
|
+
),
|
|
193
|
+
y=xr.DataArray(
|
|
194
|
+
[0, 15, 0, 0, 50, 65, 50, 50, 55, 60, 55, 55],
|
|
195
|
+
dims=("node",),
|
|
196
|
+
attrs={"axis": "Y"},
|
|
197
|
+
),
|
|
198
|
+
node_count=xr.DataArray([4, 8], dims=("index",)),
|
|
199
|
+
part_node_count=xr.DataArray([4, 4, 4], dims=("part",)),
|
|
200
|
+
interior_ring=xr.DataArray([0, 0, 1], dims=("part",)),
|
|
201
|
+
crd_x=xr.DataArray([50, 70], dims=("index",), attrs={"nodes": "x"}),
|
|
202
|
+
crd_y=xr.DataArray([0, 50], dims=("index",), attrs={"nodes": "y"}),
|
|
203
|
+
geometry_container=xr.DataArray(
|
|
204
|
+
attrs={
|
|
205
|
+
"geometry_type": "polygon",
|
|
206
|
+
"node_count": "node_count",
|
|
207
|
+
"part_node_count": "part_node_count",
|
|
208
|
+
"interior_ring": "interior_ring",
|
|
209
|
+
"node_coordinates": "x y",
|
|
210
|
+
"coordinates": "crd_x crd_y",
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
|
|
216
|
+
|
|
217
|
+
return cf_ds, shp_da
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.fixture
|
|
221
|
+
def geometry_polygon_ds():
|
|
222
|
+
from shapely.geometry import MultiPolygon, Polygon
|
|
223
|
+
|
|
224
|
+
# empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
|
|
225
|
+
geoms = np.empty(2, dtype=object)
|
|
226
|
+
geoms[:] = [
|
|
227
|
+
MultiPolygon(
|
|
228
|
+
[
|
|
229
|
+
(
|
|
230
|
+
([20, 0], [10, 15], [0, 0]),
|
|
231
|
+
[
|
|
232
|
+
([5, 5], [10, 10], [15, 5]),
|
|
233
|
+
],
|
|
234
|
+
),
|
|
235
|
+
(([20, 20], [10, 35], [0, 20]),),
|
|
236
|
+
]
|
|
237
|
+
),
|
|
238
|
+
Polygon(([50, 0], [40, 15], [30, 0])),
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
ds = xr.Dataset()
|
|
242
|
+
shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
|
|
243
|
+
|
|
244
|
+
cf_ds = ds.assign(
|
|
245
|
+
x=xr.DataArray(
|
|
246
|
+
[20, 10, 0, 20, 5, 10, 15, 5, 20, 10, 0, 20, 50, 40, 30, 50],
|
|
247
|
+
dims=("node",),
|
|
248
|
+
attrs={"axis": "X"},
|
|
249
|
+
),
|
|
250
|
+
y=xr.DataArray(
|
|
251
|
+
[0, 15, 0, 0, 5, 10, 5, 5, 20, 35, 20, 20, 0, 15, 0, 0],
|
|
252
|
+
dims=("node",),
|
|
253
|
+
attrs={"axis": "Y"},
|
|
254
|
+
),
|
|
255
|
+
node_count=xr.DataArray([12, 4], dims=("index",)),
|
|
256
|
+
part_node_count=xr.DataArray([4, 4, 4, 4], dims=("part",)),
|
|
257
|
+
interior_ring=xr.DataArray([0, 1, 0, 0], dims=("part",)),
|
|
258
|
+
crd_x=xr.DataArray([20, 50], dims=("index",), attrs={"nodes": "x"}),
|
|
259
|
+
crd_y=xr.DataArray([0, 0], dims=("index",), attrs={"nodes": "y"}),
|
|
260
|
+
geometry_container=xr.DataArray(
|
|
261
|
+
attrs={
|
|
262
|
+
"geometry_type": "polygon",
|
|
263
|
+
"node_count": "node_count",
|
|
264
|
+
"part_node_count": "part_node_count",
|
|
265
|
+
"interior_ring": "interior_ring",
|
|
266
|
+
"node_coordinates": "x y",
|
|
267
|
+
"coordinates": "crd_x crd_y",
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
|
|
273
|
+
|
|
274
|
+
return cf_ds, shp_da
|
|
275
|
+
|
|
276
|
+
|
|
129
277
|
@requires_shapely
|
|
130
278
|
def test_shapely_to_cf(geometry_ds):
|
|
131
279
|
from shapely.geometry import Point
|
|
@@ -192,6 +340,43 @@ def test_shapely_to_cf_for_lines_without_multilines(
|
|
|
192
340
|
xr.testing.assert_identical(actual, expected)
|
|
193
341
|
|
|
194
342
|
|
|
343
|
+
@requires_shapely
|
|
344
|
+
def test_shapely_to_cf_for_polygons_as_da(geometry_polygon_ds):
|
|
345
|
+
expected, in_da = geometry_polygon_ds
|
|
346
|
+
|
|
347
|
+
actual = cfxr.shapely_to_cf(in_da)
|
|
348
|
+
xr.testing.assert_identical(actual, expected)
|
|
349
|
+
|
|
350
|
+
in_da = in_da.assign_coords(index=["a", "b"])
|
|
351
|
+
actual = cfxr.shapely_to_cf(in_da)
|
|
352
|
+
xr.testing.assert_identical(actual, expected.assign_coords(index=["a", "b"]))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@requires_shapely
|
|
356
|
+
def test_shapely_to_cf_for_polygons_as_sequence(geometry_polygon_ds):
|
|
357
|
+
expected, in_da = geometry_polygon_ds
|
|
358
|
+
actual = cfxr.shapely_to_cf(in_da.values)
|
|
359
|
+
xr.testing.assert_identical(actual, expected)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@requires_shapely
|
|
363
|
+
def test_shapely_to_cf_for_polygons_without_multipolygons(
|
|
364
|
+
geometry_polygon_without_multipolygons_ds,
|
|
365
|
+
):
|
|
366
|
+
expected, in_da = geometry_polygon_without_multipolygons_ds
|
|
367
|
+
actual = cfxr.shapely_to_cf(in_da)
|
|
368
|
+
xr.testing.assert_identical(actual, expected)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@requires_shapely
|
|
372
|
+
def test_shapely_to_cf_for_polygons_without_holes(
|
|
373
|
+
geometry_polygon_without_holes_ds,
|
|
374
|
+
):
|
|
375
|
+
expected, in_da = geometry_polygon_without_holes_ds
|
|
376
|
+
actual = cfxr.shapely_to_cf(in_da)
|
|
377
|
+
xr.testing.assert_identical(actual, expected)
|
|
378
|
+
|
|
379
|
+
|
|
195
380
|
@requires_shapely
|
|
196
381
|
def test_shapely_to_cf_errors():
|
|
197
382
|
from shapely.geometry import Point, Polygon
|
|
@@ -199,13 +384,8 @@ def test_shapely_to_cf_errors():
|
|
|
199
384
|
geoms = [
|
|
200
385
|
Polygon([[1, 1], [1, 3], [3, 3], [1, 1]]),
|
|
201
386
|
Polygon([[1, 1, 4], [1, 3, 4], [3, 3, 3], [1, 1, 4]]),
|
|
387
|
+
Point(1, 2),
|
|
202
388
|
]
|
|
203
|
-
with pytest.raises(
|
|
204
|
-
NotImplementedError, match="Polygon geometry conversion is not implemented"
|
|
205
|
-
):
|
|
206
|
-
cfxr.shapely_to_cf(geoms)
|
|
207
|
-
|
|
208
|
-
geoms.append(Point(1, 2))
|
|
209
389
|
with pytest.raises(ValueError, match="Mixed geometry types are not supported"):
|
|
210
390
|
cfxr.shapely_to_cf(geoms)
|
|
211
391
|
|
|
@@ -247,7 +427,7 @@ def test_cf_to_shapely_for_lines_without_multilines(
|
|
|
247
427
|
in_ds, expected = geometry_line_without_multilines_ds
|
|
248
428
|
actual = cfxr.cf_to_shapely(in_ds)
|
|
249
429
|
assert actual.dims == ("index",)
|
|
250
|
-
xr.testing.assert_identical(actual, expected)
|
|
430
|
+
xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
|
|
251
431
|
|
|
252
432
|
in_ds = in_ds.assign_coords(index=["b", "c"])
|
|
253
433
|
actual = cfxr.cf_to_shapely(in_ds)
|
|
@@ -258,12 +438,51 @@ def test_cf_to_shapely_for_lines_without_multilines(
|
|
|
258
438
|
|
|
259
439
|
|
|
260
440
|
@requires_shapely
|
|
261
|
-
def
|
|
262
|
-
in_ds,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
441
|
+
def test_cf_to_shapely_for_polygons(geometry_polygon_ds):
|
|
442
|
+
in_ds, expected = geometry_polygon_ds
|
|
443
|
+
|
|
444
|
+
actual = cfxr.cf_to_shapely(in_ds)
|
|
445
|
+
assert actual.dims == ("index",)
|
|
446
|
+
xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@requires_shapely
|
|
450
|
+
def test_cf_to_shapely_for_polygons_without_multipolygons(
|
|
451
|
+
geometry_polygon_without_multipolygons_ds,
|
|
452
|
+
):
|
|
453
|
+
in_ds, expected = geometry_polygon_without_multipolygons_ds
|
|
454
|
+
actual = cfxr.cf_to_shapely(in_ds)
|
|
455
|
+
assert actual.dims == ("index",)
|
|
456
|
+
xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
|
|
457
|
+
|
|
458
|
+
in_ds = in_ds.assign_coords(index=["b", "c"])
|
|
459
|
+
actual = cfxr.cf_to_shapely(in_ds)
|
|
460
|
+
assert actual.dims == ("index",)
|
|
461
|
+
xr.testing.assert_identical(
|
|
462
|
+
actual.drop_vars(["crd_x", "crd_y"]), expected.assign_coords(index=["b", "c"])
|
|
463
|
+
)
|
|
464
|
+
|
|
266
465
|
|
|
466
|
+
@requires_shapely
|
|
467
|
+
def test_cf_to_shapely_for_polygons_without_holes(
|
|
468
|
+
geometry_polygon_without_holes_ds,
|
|
469
|
+
):
|
|
470
|
+
in_ds, expected = geometry_polygon_without_holes_ds
|
|
471
|
+
actual = cfxr.cf_to_shapely(in_ds)
|
|
472
|
+
assert actual.dims == ("index",)
|
|
473
|
+
xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
|
|
474
|
+
|
|
475
|
+
in_ds = in_ds.assign_coords(index=["b", "c"])
|
|
476
|
+
actual = cfxr.cf_to_shapely(in_ds)
|
|
477
|
+
assert actual.dims == ("index",)
|
|
478
|
+
xr.testing.assert_identical(
|
|
479
|
+
actual.drop_vars(["crd_x", "crd_y"]), expected.assign_coords(index=["b", "c"])
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@requires_shapely
|
|
484
|
+
def test_cf_to_shapely_errors(geometry_ds, geometry_line_ds, geometry_polygon_ds):
|
|
485
|
+
in_ds, _ = geometry_ds
|
|
267
486
|
in_ds.geometry_container.attrs["geometry_type"] = "punkt"
|
|
268
487
|
with pytest.raises(ValueError, match="Valid CF geometry types are "):
|
|
269
488
|
cfxr.cf_to_shapely(in_ds)
|
|
@@ -273,6 +492,11 @@ def test_cf_to_shapely_errors(geometry_ds, geometry_line_ds):
|
|
|
273
492
|
with pytest.raises(ValueError, match="'node_count' must be provided"):
|
|
274
493
|
cfxr.cf_to_shapely(in_ds)
|
|
275
494
|
|
|
495
|
+
in_ds, _ = geometry_polygon_ds
|
|
496
|
+
del in_ds.geometry_container.attrs["node_count"]
|
|
497
|
+
with pytest.raises(ValueError, match="'node_count' must be provided"):
|
|
498
|
+
cfxr.cf_to_shapely(in_ds)
|
|
499
|
+
|
|
276
500
|
|
|
277
501
|
@requires_shapely
|
|
278
502
|
def test_reshape_unique_geometries(geometry_ds):
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
What's New
|
|
4
4
|
----------
|
|
5
5
|
|
|
6
|
+
v0.8.8 (Jan 19, 2023)
|
|
7
|
+
=====================
|
|
8
|
+
- Add conversion between CF geometries and Shapely objects for polygons. By `Julia Signell`_.
|
|
9
|
+
- Support 32bit wide bit masks. By `Michael St Laurent`_.
|
|
10
|
+
|
|
6
11
|
v0.8.7 (Dec 19, 2023)
|
|
7
12
|
=====================
|
|
8
13
|
- Add conversion between CF geometries and Shapely objects for lines. By `Julia Signell`_.
|
|
@@ -253,4 +258,5 @@ v0.1.3
|
|
|
253
258
|
.. _`Aidan Heerdegen`: https://github.com/aidanheerdegen
|
|
254
259
|
.. _`Clément Haëck`: https://github.com/Descanonge
|
|
255
260
|
.. _`Julia Signell`: https://github.com/jsignell
|
|
261
|
+
.. _`Michael St Laurent`: https://github.com/mps010160
|
|
256
262
|
.. _`flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.7"
|
cf_xarray-0.8.7/codecov.yml
DELETED
|
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
|
|
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
|
|
File without changes
|