diff --git a/lib/iris/_lazy_data.py b/lib/iris/_lazy_data.py index e7ffccce32..0da1c4c785 100644 --- a/lib/iris/_lazy_data.py +++ b/lib/iris/_lazy_data.py @@ -131,14 +131,14 @@ def as_concrete_data(data): return data -def lazy_masked_fill_value(data): +def get_fill_value(data): """ - Return the fill value of a lazy masked array. + Return the fill value of a concrete or lazy masked array. Args: * data: - A dask array, NumPy `ndarray` or masked array + A dask array, NumPy `ndarray` or masked array. Returns: The fill value of `data` if `data` represents a masked array, or None. @@ -147,9 +147,14 @@ def lazy_masked_fill_value(data): # If lazy, get the smallest slice of the data from which we can retrieve # the fill_value. if is_lazy_data(data): - inds = tuple([0] * (data.ndim-1)) - smallest_slice = data[inds][:0] - data = as_concrete_data(smallest_slice) + inds = [0] * data.ndim + if inds: + inds[-1] = slice(0, 1) + data = data[tuple(inds)] + data = as_concrete_data(data) + else: + if isinstance(data, ma.core.MaskedConstant): + data = ma.array(data.data, mask=data.mask) # Now get the fill value. fill_value = None diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index a58a583c67..a2d87d2eed 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -56,7 +56,7 @@ import iris.fileformats._pyke_rules import iris.io import iris.util -from iris._lazy_data import as_lazy_data, lazy_masked_fill_value +from iris._lazy_data import as_lazy_data, get_fill_value # Show Pyke inference engine statistics. DEBUG = False @@ -1926,10 +1926,7 @@ def set_packing_ncattrs(cfvar): if packing is None: # Determine whether there is a cube MDI value. - if ma.isMaskedArray(cube.data): - fill_value = cube.data.fill_value - else: - fill_value = None + fill_value = get_fill_value(cube.core_data()) # Get the values in a form which is valid for the file format. data = self._ensure_valid_dtype(cube.data, 'cube', cube) @@ -1946,16 +1943,8 @@ def set_packing_ncattrs(cfvar): else: # Create the cube CF-netCDF data variable. - # Set `fill_value` if the data array is masked. If the data array - # is lazy masked, we realise the smallest possible slice of the - # array and retrieve the fill value from that. if packing is None: - if not cube.has_lazy_data() and ma.isMaskedArray(cube.data): - fill_value = cube.data.fill_value - elif cube.has_lazy_data(): - fill_value = lazy_masked_fill_value(cube.lazy_data()) - else: - fill_value = None + fill_value = get_fill_value(cube.core_data()) dtype = cube.dtype.newbyteorder('=') cf_var = self._dataset.createVariable( diff --git a/lib/iris/tests/unit/lazy_data/test_get_fill_value.py b/lib/iris/tests/unit/lazy_data/test_get_fill_value.py new file mode 100644 index 0000000000..e83739f331 --- /dev/null +++ b/lib/iris/tests/unit/lazy_data/test_get_fill_value.py @@ -0,0 +1,75 @@ +# (C) British Crown Copyright 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Test the function :func:`iris._lazy data.get_fill_value`.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import dask.array as da +import numpy as np +import numpy.ma as ma + +from iris._lazy_data import as_lazy_data, get_fill_value + + +class Test_get_fill_value(tests.IrisTest): + def setUp(self): + # Array shape and fill-value. + spec = [((2, 3, 4, 5), -1), # 4d array + ((2, 3, 4), -2), # 3d array + ((2, 3), -3), # 2d array + ((2,), -4), # 1d array + ((), -5)] # 0d array + self.arrays = [np.empty(shape) for (shape, _) in spec] + self.masked = [ma.empty(shape, fill_value=fv) for (shape, fv) in spec] + self.lazy_arrays = [as_lazy_data(array) for array in self.arrays] + self.lazy_masked = [as_lazy_data(array) for array in self.masked] + # Add the masked constant case. + mc = ma.array([0], mask=True)[0] + self.masked.append(mc) + self.lazy_masked.append(as_lazy_data(mc)) + # Collect the expected fill-values. + self.expected_fill_values = [fv for (_, fv) in spec] + mc_fill_value = ma.masked_array(0, dtype=mc.dtype).fill_value + self.expected_fill_values.append(mc_fill_value) + + def test_arrays(self): + for array in self.arrays: + self.assertIsNone(get_fill_value(array)) + + def test_masked(self): + for array, expected in zip(self.masked, self.expected_fill_values): + result = get_fill_value(array) + self.assertEqual(result, expected) + + def test_lazy_arrays(self): + for array in self.lazy_arrays: + self.assertIsNone(get_fill_value(array)) + + def test_lazy_masked(self): + for array, expected in zip(self.lazy_masked, + self.expected_fill_values): + result = get_fill_value(array) + self.assertEqual(result, expected) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/lazy_data/test_lazy_masked_fill_value.py b/lib/iris/tests/unit/lazy_data/test_lazy_masked_fill_value.py deleted file mode 100644 index aaaefea657..0000000000 --- a/lib/iris/tests/unit/lazy_data/test_lazy_masked_fill_value.py +++ /dev/null @@ -1,86 +0,0 @@ -# (C) British Crown Copyright 2017, Met Office -# -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . -"""Test the function :func:`iris._lazy data.lazy_masked_fill_value`.""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests - -import dask.array as da -import numpy as np -import numpy.ma as ma - -from iris._lazy_data import lazy_masked_fill_value, _MAX_CHUNK_SIZE - - -class Test_as_lazy_data(tests.IrisTest): - def setUp(self): - shape = (2, 3, 4) - data = np.arange(24).reshape(shape) - mask = np.zeros(shape) - mask[data % 5 == 1] = 1 - self.fill_value = 9999 - self.m = ma.masked_array(data, mask=mask, fill_value=self.fill_value) - self.dm = da.from_array(self.m, asarray=False, - chunks=_MAX_CHUNK_SIZE) - - def test_lazy_masked_ND(self): - fill_value = lazy_masked_fill_value(self.dm) - self.assertEqual(fill_value, self.fill_value) - - def test_lazy_masked_0D(self): - data = self.dm[0, 0, :1] - fill_value = lazy_masked_fill_value(data) - self.assertEqual(fill_value, self.fill_value) - - def test_lazy_masked_1D(self): - data = self.dm[0, 0, :] - fill_value = lazy_masked_fill_value(data) - self.assertEqual(fill_value, self.fill_value) - - def test_lazy_masked_2D(self): - data = self.dm[0, :] - fill_value = lazy_masked_fill_value(data) - self.assertEqual(fill_value, self.fill_value) - - def test_real_masked(self): - fill_value = lazy_masked_fill_value(self.m) - self.assertEqual(fill_value, self.fill_value) - - def test_lazy_unmasked(self): - data = da.from_array(self.m.filled(), - chunks=_MAX_CHUNK_SIZE) - fill_value = lazy_masked_fill_value(data) - self.assertIsNone(fill_value) - - def test_real_unmasked(self): - data = self.m.filled() - fill_value = lazy_masked_fill_value(data) - self.assertIsNone(fill_value) - - def test_data_not_realised(self): - # Check that only the zero-element slice is realised. - data = self.dm[0, :] - lazy_masked_fill_value(data) - self.assertIsInstance(data, da.core.Array) - - -if __name__ == '__main__': - tests.main()