Skip to content

Commit 354f3e5

Browse files
committed
Dataless netcdf load+save; plus tests.
1 parent 7536010 commit 354f3e5

File tree

3 files changed

+121
-8
lines changed

3 files changed

+121
-8
lines changed

lib/iris/fileformats/netcdf/loader.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,17 @@ def _load_cube_inner(engine, cf, cf_var, filename):
392392
from iris.cube import Cube
393393

394394
"""Create the cube associated with the CF-netCDF data variable."""
395-
data = _get_cf_var_data(cf_var)
396-
cube = Cube(data)
395+
from iris.fileformats.netcdf.saver import Saver
396+
397+
if hasattr(cf_var, Saver._DATALESS_ATTRNAME):
398+
# This data-variable represents a dataless cube.
399+
# The variable array content was never written (to take up no space).
400+
data = None
401+
shape = cf_var.shape
402+
else:
403+
data = _get_cf_var_data(cf_var)
404+
shape = None
405+
cube = Cube(data=data, shape=shape)
397406

398407
# Reset the actions engine.
399408
engine.reset()

lib/iris/fileformats/netcdf/saver.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,6 +2275,10 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube):
22752275
if grid_mapping:
22762276
_setncattr(cf_var_cube, "grid_mapping", grid_mapping)
22772277

2278+
_DATALESS_ATTRNAME = "iris_dataless_cube"
2279+
_DATALESS_DTYPE = np.dtype("u1")
2280+
_DATALESS_FILLVALUE = 127
2281+
22782282
def _create_cf_data_variable(
22792283
self,
22802284
cube,
@@ -2315,9 +2319,19 @@ def _create_cf_data_variable(
23152319
# TODO: when iris.FUTURE.save_split_attrs is removed, the 'local_keys' arg can
23162320
# be removed.
23172321
# Get the values in a form which is valid for the file format.
2318-
data = self._ensure_valid_dtype(cube.core_data(), "cube", cube)
2322+
is_dataless = cube.is_dataless()
2323+
if is_dataless:
2324+
data = None
2325+
else:
2326+
data = self._ensure_valid_dtype(cube.core_data(), "cube", cube)
23192327

2320-
if packing:
2328+
if is_dataless:
2329+
# The variable must have *some* dtype, and it must be maskable
2330+
dtype = self._DATALESS_DTYPE
2331+
fill_value = self._DATALESS_FILLVALUE
2332+
elif not packing:
2333+
dtype = data.dtype.newbyteorder("=")
2334+
else:
23212335
if isinstance(packing, dict):
23222336
if "dtype" not in packing:
23232337
msg = "The dtype attribute is required for packing."
@@ -2355,8 +2369,6 @@ def _create_cf_data_variable(
23552369
add_offset = (cmax + cmin) / 2
23562370
else:
23572371
add_offset = cmin + 2 ** (n - 1) * scale_factor
2358-
else:
2359-
dtype = data.dtype.newbyteorder("=")
23602372

23612373
def set_packing_ncattrs(cfvar):
23622374
"""Set netCDF packing attributes.
@@ -2380,8 +2392,9 @@ def set_packing_ncattrs(cfvar):
23802392
cf_name, dtype, dimension_names, fill_value=fill_value, **kwargs
23812393
)
23822394

2383-
set_packing_ncattrs(cf_var)
2384-
self._lazy_stream_data(data=data, cf_var=cf_var)
2395+
if not is_dataless:
2396+
set_packing_ncattrs(cf_var)
2397+
self._lazy_stream_data(data=data, cf_var=cf_var)
23852398

23862399
if cube.standard_name:
23872400
_setncattr(cf_var, "standard_name", cube.standard_name)
@@ -2446,6 +2459,10 @@ def set_packing_ncattrs(cfvar):
24462459

24472460
_setncattr(cf_var, attr_name, value)
24482461

2462+
# Add the 'dataless' marker if needed
2463+
if is_dataless:
2464+
_setncattr(cf_var, self._DATALESS_ATTRNAME, "true")
2465+
24492466
# Create the CF-netCDF data variable cell method attribute.
24502467
cell_methods = self._create_cf_cell_methods(cube, dimension_names)
24512468

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright Iris contributors
2+
#
3+
# This file is part of Iris and is released under the BSD license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""Integration tests for save+load of datales cubes."""
6+
7+
import numpy as np
8+
import pytest
9+
10+
import iris
11+
from iris.coords import DimCoord
12+
from iris.cube import Cube
13+
from iris.fileformats.netcdf._thread_safe_nc import netCDF4 as nc
14+
from iris.fileformats.netcdf.saver import Saver
15+
16+
17+
class TestDataless:
18+
@pytest.fixture(autouse=True)
19+
def setup(self, tmp_path_factory):
20+
ny, nx = 3, 4
21+
self.testcube = Cube(
22+
shape=(ny, nx),
23+
long_name="testdata",
24+
dim_coords_and_dims=[
25+
(DimCoord(np.arange(ny), long_name="y"), 0),
26+
(DimCoord(np.arange(nx), long_name="x"), 1),
27+
],
28+
)
29+
self.testdir = tmp_path_factory.mktemp("dataless")
30+
self.test_path = self.testdir / "test.nc"
31+
32+
@staticmethod
33+
def _strip_saveload_additions(reloaded_cube):
34+
reloaded_cube.attributes.pop("Conventions", None)
35+
reloaded_cube.var_name = None
36+
for co in reloaded_cube.coords():
37+
co.var_name = None
38+
39+
def test_dataless_save(self):
40+
# Check that we can save a dataless cube, and what that looks like in the file.
41+
iris.save(self.testcube, self.test_path)
42+
assert Saver._DATALESS_ATTRNAME not in self.testcube.attributes
43+
# Check the content as seen in the file
44+
ncds = nc.Dataset(self.test_path)
45+
var = ncds.variables["testdata"]
46+
assert Saver._DATALESS_ATTRNAME in var.ncattrs()
47+
assert var.dtype == Saver._DATALESS_DTYPE
48+
assert "_FillValue" in var.ncattrs()
49+
assert var._FillValue == Saver._DATALESS_FILLVALUE
50+
assert np.all(np.ma.getmaskarray(var[:]) == True) # noqa: E712
51+
52+
def test_dataless_load(self):
53+
# Check that we can load a saved dataless cube, and it matches the original.
54+
iris.save(self.testcube, self.test_path)
55+
56+
# NB Load with load_raw, since we haven't finished supporting dataless merge.
57+
(result_cube,) = iris.load_raw(self.test_path)
58+
assert result_cube.is_dataless()
59+
assert "iris_dataless_cube" not in result_cube.attributes
60+
61+
# strip off extra things added by netcdf save+load
62+
self._strip_saveload_additions(result_cube)
63+
64+
# Result now == original
65+
assert result_cube == self.testcube
66+
67+
def test_mixture_saveload(self):
68+
# Check that a mixture of dataless and "normal" cubes can be saved + loaded back
69+
dataless = self.testcube
70+
ny = dataless.shape[0]
71+
dataful = Cube(
72+
np.ones((ny, 3)),
73+
long_name="other",
74+
dim_coords_and_dims=[(dataless.coord("y"), 0)],
75+
)
76+
iris.save([dataless, dataful], self.test_path)
77+
# NB Load with load_raw, since we haven't finished supporting dataless merge.
78+
cubes = iris.load_raw(self.test_path)
79+
assert len(cubes) == 2
80+
read_dataless = cubes.extract_cube("testdata")
81+
read_dataful = cubes.extract_cube("other")
82+
assert read_dataless.is_dataless()
83+
assert not read_dataful.is_dataless()
84+
for cube in (read_dataless, read_dataful):
85+
self._strip_saveload_additions(cube)
86+
assert read_dataless == dataless
87+
assert read_dataful == dataful

0 commit comments

Comments
 (0)