Skip to content

Commit 309b921

Browse files
authored
Merge pull request #627 from dcs4cop/forman-xxx-add_spatial_ref
New function "add_spatial_ref"
2 parents ff02034 + 29d10b2 commit 309b921

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
### Enhancements
44

5+
* Introduced helper function `add_spatial_ref()`
6+
of package `xcube.core.gridmapping.cfconv` that allows
7+
adding a spatial coordinate reference system to an existing
8+
Zarr dataset. (#629)
9+
510
* Introduced parameter `base_dataset_id` for writing multi-level
611
datasets with the "file", "s3", and "memory" data stores.
712
If given, the base dataset will be linked only with the

test/core/gridmapping/test_cfconv.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
import unittest
33
from typing import Set
44

5+
import fsspec
56
import numpy as np
67
import pyproj
78
import xarray as xr
89

10+
from test.s3test import MOTO_SERVER_ENDPOINT_URL
11+
from test.s3test import S3Test
12+
from xcube.core.gridmapping import GridMapping
913
from xcube.core.gridmapping.cfconv import GridCoords
1014
from xcube.core.gridmapping.cfconv import GridMappingProxy
15+
from xcube.core.gridmapping.cfconv import add_spatial_ref
1116
from xcube.core.gridmapping.cfconv import get_dataset_grid_mapping_proxies
17+
from xcube.core.new import new_cube
18+
from xcube.core.store.fs.registry import new_fs_data_store
1219

1320
CRS_WGS84 = pyproj.crs.CRS(4326)
1421
CRS_CRS84 = pyproj.crs.CRS.from_string("urn:ogc:def:crs:OGC:1.3:CRS84")
@@ -192,3 +199,98 @@ def _gen_2d(self):
192199
self.assertEqual(('y', 'x'), lon.dims)
193200
self.assertEqual(('y', 'x'), lat.dims)
194201
return noise, crs, lon, lat
202+
203+
204+
class AddSpatialRefTest(S3Test):
205+
206+
def test_add_spatial_ref(self):
207+
self.assert_add_spatial_ref_ok(None, None)
208+
self.assert_add_spatial_ref_ok(None, ('cx', 'cy'))
209+
self.assert_add_spatial_ref_ok('crs', None)
210+
self.assert_add_spatial_ref_ok('crs', ('cx', 'cy'))
211+
212+
def assert_add_spatial_ref_ok(self, crs_var_name, xy_dim_names):
213+
214+
root = 'eurodatacube-test/xcube-eea'
215+
data_id = 'test.zarr'
216+
crs = pyproj.CRS.from_string("EPSG:3035")
217+
218+
if xy_dim_names:
219+
x_name, y_name = xy_dim_names
220+
else:
221+
x_name, y_name = 'x', 'y'
222+
223+
cube = new_cube(x_name=x_name,
224+
y_name=y_name,
225+
x_start=0,
226+
y_start=0,
227+
x_res=10,
228+
y_res=10,
229+
x_units='metres',
230+
y_units='metres',
231+
drop_bounds=True,
232+
width=100,
233+
height=100,
234+
variables=dict(A=1.3, B=8.3))
235+
236+
storage_options = dict(
237+
anon=False,
238+
client_kwargs=dict(
239+
endpoint_url=MOTO_SERVER_ENDPOINT_URL,
240+
)
241+
)
242+
243+
fs: fsspec.AbstractFileSystem = fsspec.filesystem('s3',
244+
**storage_options)
245+
if fs.isdir(root):
246+
fs.rm(root, recursive=True)
247+
fs.mkdirs(root, exist_ok=True)
248+
249+
data_store = new_fs_data_store('s3',
250+
root=root,
251+
storage_options=storage_options)
252+
253+
data_store.write_data(cube, data_id=data_id)
254+
cube = data_store.open_data(data_id)
255+
self.assertEqual({'A', 'B', 'time', x_name, y_name},
256+
set(cube.variables))
257+
258+
with self.assertRaises(ValueError) as cm:
259+
GridMapping.from_dataset(cube)
260+
self.assertEqual(
261+
('cannot find any grid mapping in dataset',),
262+
cm.exception.args
263+
)
264+
265+
path = f"{root}/{data_id}"
266+
group_store = fs.get_mapper(path, create=True)
267+
268+
expected_crs_var_name = crs_var_name or 'spatial_ref'
269+
270+
self.assertTrue(fs.exists(path))
271+
self.assertFalse(fs.exists(f"{path}/{expected_crs_var_name}"))
272+
self.assertFalse(fs.exists(f"{path}/{expected_crs_var_name}/.zarray"))
273+
self.assertFalse(fs.exists(f"{path}/{expected_crs_var_name}/.zattrs"))
274+
275+
kwargs = {}
276+
if crs_var_name is not None:
277+
kwargs.update(crs_var_name=crs_var_name)
278+
if xy_dim_names is not None:
279+
kwargs.update(xy_dim_names=xy_dim_names)
280+
add_spatial_ref(group_store, crs, **kwargs)
281+
282+
self.assertTrue(fs.exists(f"{path}/{expected_crs_var_name}"))
283+
self.assertTrue(fs.exists(f"{path}/{expected_crs_var_name}/.zarray"))
284+
self.assertTrue(fs.exists(f"{path}/{expected_crs_var_name}/.zattrs"))
285+
286+
cube = data_store.open_data(data_id)
287+
self.assertEqual({'A', 'B', 'time',
288+
x_name, y_name, expected_crs_var_name},
289+
set(cube.variables))
290+
self.assertEqual(expected_crs_var_name,
291+
cube.A.attrs.get('grid_mapping'))
292+
self.assertEqual(expected_crs_var_name,
293+
cube.B.attrs.get('grid_mapping'))
294+
295+
gm = GridMapping.from_dataset(cube)
296+
self.assertIn("LAEA Europe", gm.crs.srs)

xcube/core/gridmapping/cfconv.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
# SOFTWARE.
2121

2222
import warnings
23+
from collections.abc import MutableMapping
2324
from typing import Optional, Dict, Any, Hashable, Union, Set, List, Tuple
2425

26+
import numpy as np
2527
import pyproj
2628
import xarray as xr
29+
import zarr
30+
import zarr.convenience
2731

2832
from xcube.core.schema import get_dataset_chunks
33+
from xcube.util.assertions import assert_instance
2934

3035

3136
class GridCoords:
@@ -285,3 +290,43 @@ def _find_dataset_tile_size(dataset: xr.Dataset,
285290
if tile_width is not None and tile_height is not None:
286291
return tile_width, tile_height
287292
return None
293+
294+
295+
def add_spatial_ref(dataset_store: zarr.convenience.StoreLike,
296+
crs: pyproj.CRS,
297+
crs_var_name: Optional[str] = 'spatial_ref',
298+
xy_dim_names: Optional[Tuple[str, str]] = None):
299+
"""
300+
Helper function that allows adding a spatial reference to an
301+
existing Zarr dataset.
302+
303+
:param dataset_store: The dataset's existing Zarr store or path.
304+
:param crs: The spatial coordinate reference system.
305+
:param crs_var_name: The name of the variable that will hold the
306+
spatial reference. Defaults to "spatial_ref".
307+
:param xy_dim_names: The names of the x and y dimensions.
308+
Defaults to ("x", "y").
309+
"""
310+
assert_instance(dataset_store, (MutableMapping, str), name='group_store')
311+
assert_instance(crs_var_name, str, name='crs_var_name')
312+
x_dim_name, y_dim_name = xy_dim_names or ('x', 'y')
313+
314+
spatial_attrs = crs.to_cf()
315+
spatial_attrs['_ARRAY_DIMENSIONS'] = [] # Required by xarray
316+
group = zarr.open(dataset_store, mode='r+')
317+
spatial_ref = group.array(crs_var_name,
318+
0,
319+
shape=(),
320+
dtype=np.uint8,
321+
fill_value=0)
322+
spatial_ref.attrs.update(**spatial_attrs)
323+
324+
for item_name, item in group.items():
325+
if item_name != crs_var_name:
326+
dims = item.attrs.get('_ARRAY_DIMENSIONS')
327+
if dims and len(dims) >= 2 \
328+
and dims[-2] == y_dim_name \
329+
and dims[-1] == x_dim_name:
330+
item.attrs['grid_mapping'] = crs_var_name
331+
332+
zarr.convenience.consolidate_metadata(dataset_store)

0 commit comments

Comments
 (0)