Repository: dcs4cop/xcube
Pull request #155 (2019-08-19 11:24:43)

Forman add maskset to api
This PR lets `xcube.api` export `xcube.util.maskset.MaskSet` so we can use it in NBs.
Added tests for Sentinel-2 L2A SCL flags.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 1032aa9a..d6e3538d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,8 @@
 
 ### Enhancements
 
+* `xcube.api` now exports the `MaskSet` class which is useful for decoding flag values encoding following the
+  [CF conventions](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.7/cf-conventions.html#flags).
 * Added new CLI tool `xcube optimize` and API function `xcube.api.optimize_dataset` 
   optimizes data cubes for cloud object storage deployment. (#141)
 * Added two new spatial dataset operations to Python API `xcube.api` (#148):
diff --git a/xcube/api/__init__.py b/xcube/api/__init__.py
index 6fabdb73..55215556 100644
--- a/xcube/api/__init__.py
+++ b/xcube/api/__init__.py
@@ -1,4 +1,6 @@
-from .api import XCubeAPI
+# Force loading of xarray
+# noinspection PyUnresolvedReferences
+import xcube.api.api
 from .dump import dump_dataset
 from .extract import DEFAULT_INDEX_NAME_PATTERN, DEFAULT_INTERP_POINT_METHOD, DEFAULT_REF_NAME_PATTERN, \
     get_cube_point_indexes, get_cube_values_for_indexes, get_cube_values_for_points, get_dataset_indexes
@@ -16,6 +18,8 @@ from ..util.chunk import chunk_dataset
 # noinspection PyUnresolvedReferences
 from ..util.geom import clip_dataset_by_geometry, mask_dataset_by_geometry, convert_geometry
 # noinspection PyUnresolvedReferences
+from ..util.maskset import MaskSet
+# noinspection PyUnresolvedReferences
 from ..util.optimize import optimize_dataset
 # noinspection PyUnresolvedReferences
 from ..util.unchunk import unchunk_dataset
diff --git a/xcube/api/api.py b/xcube/api/api.py
index 16b83f66..4ee9d0a0 100644
--- a/xcube/api/api.py
+++ b/xcube/api/api.py
@@ -5,7 +5,6 @@ import numpy as np
 import pandas as pd
 import xarray as xr
 
-from xcube.util.chunk import chunk_dataset
 # noinspection PyUnresolvedReferences
 from .compute import compute_dataset
 from .dump import dump_dataset
@@ -18,10 +17,11 @@ from .readwrite import read_cube, open_cube, write_cube
 from .select import select_vars
 from .vars_to_dim import vars_to_dim
 from .verify import verify_cube
+from ..util.chunk import chunk_dataset
 
 
 @xr.register_dataset_accessor('xcube')
-class XCubeAPI:
+class XCubeDatasetAccessor:
     """
     The XCube API.
 
@@ -293,19 +293,3 @@ class XCubeAPI:
         :return: A list of dataset instances representing the multi-level pyramid.
         """
         return compute_levels(self._dataset, **kwargs)
-
-    def resample_in_time(self):
-        """
-        Resample a data cube in the time dimension.
-
-        :param frequency: Resampling frequency.
-        :param method: Resampling method or sequence of resampling methods.
-        :param offset: Offset used to adjust the resampled time labels. Some pandas date offset strings are supported.
-        :param base: Resampling method.
-        :param var_names: Variable names to include.
-        :param tolerance: Time tolerance for selective upsampling methods. Defaults to *frequency*.
-        :param interp_kind: Kind of interpolation if *method* is 'interpolation'.
-        :param metadata: Output metadata.
-        :return: A new data cube resampled in time.
-        """
-        return resample_in_time()
\ No newline at end of file
diff --git a/xcube/util/maskset.py b/xcube/util/maskset.py
index a0e4cbbe..590e4a9d 100644
--- a/xcube/util/maskset.py
+++ b/xcube/util/maskset.py
@@ -19,7 +19,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from typing import Dict, Any
+from typing import Dict, Any, Iterable
 
 import numpy as np
 import xarray as xr
@@ -92,6 +92,9 @@ class MaskSet:
     def __str__(self):
         return "%s(%s)" % (self._flag_var.name, ', '.join(["%s=%s" % (n, v) for n, v in self._flags.items()]))
 
+    def __dir__(self) -> Iterable[str]:
+        return self._flag_names
+
     def __getattr__(self, name: str) -> Any:
         if name not in self._flags:
             raise AttributeError(name)


Test patch:
diff --git a/test/api/test_api.py b/test/api/test_api.py
index d1cd0cc7..d82f3685 100644
--- a/test/api/test_api.py
+++ b/test/api/test_api.py
@@ -3,13 +3,13 @@ import unittest
 import xarray as xr
 
 from test.sampledata import new_test_dataset
-from xcube.api.api import XCubeAPI
+from xcube.api.api import XCubeDatasetAccessor
 
 
-class XCubeAPITest(unittest.TestCase):
+class XCubeDatasetAccessorTest(unittest.TestCase):
     # noinspection PyMethodMayBeStatic
     def test_init(self):
-        XCubeAPI(xr.Dataset())
+        XCubeDatasetAccessor(xr.Dataset())
 
     def test_installed(self):
         self.assertTrue(hasattr(xr.Dataset, "xcube"))
diff --git a/test/util/test_maskset.py b/test/util/test_maskset.py
index 0bffca0a..8f855ce1 100644
--- a/test/util/test_maskset.py
+++ b/test/util/test_maskset.py
@@ -1,6 +1,7 @@
 import unittest
 
 import numpy as np
+import xarray as xr
 from numpy.testing import assert_array_almost_equal
 
 from test.sampledata import create_highroc_dataset, create_c2rcc_flag_var, create_cmems_sst_flag_var
@@ -93,4 +94,61 @@ class MaskSetTest(unittest.TestCase):
         mask_set = mask_sets['c2rcc_flags']
         self.assertIsInstance(mask_set, MaskSet)
 
-# TODO: add tests according to http://cfconventions.org/Data/cf-conventions/cf-conventions-1.7/build/ch03s05.html
+    def test_mask_set_with_flag_values(self):
+        s2l2a_slc_meanings = ['no_data',
+                              'saturated_or_defective',
+                              'dark_area_pixels',
+                              'cloud_shadows',
+                              'vegetation',
+                              'bare_soils',
+                              'water',
+                              'clouds_low_probability_or_unclassified',
+                              'clouds_medium_probability',
+                              'clouds_high_probability',
+                              'cirrus',
+                              'snow_or_ice']
+
+        data = np.array([[1, 2, 8, 3],
+                         [7, 6, 0, 4],
+                         [9, 5, 11, 10]], dtype=np.uint8)
+        flag_var = xr.DataArray(data,
+                                dims=('y', 'x'),
+                                name='SLC',
+                                attrs=dict(
+                                    long_name="Scene classification flags",
+                                    flag_values=','.join(f'{i}' for i in range(len(s2l2a_slc_meanings))),
+                                    flag_meanings=' '.join(s2l2a_slc_meanings),
+                                ))
+
+        mask_set = MaskSet(flag_var)
+
+        self.assertEqual('SLC(no_data=(None, 0), saturated_or_defective=(None, 1), dark_area_pixels=(None, 2), '
+                         'cloud_shadows=(None, 3), vegetation=(None, 4), bare_soils=(None, 5), water=(None, 6), '
+                         'clouds_low_probability_or_unclassified=(None, 7), clouds_medium_probability=(None, 8), '
+                         'clouds_high_probability=(None, 9), cirrus=(None, 10), snow_or_ice=(None, 11))',
+                         str(mask_set))
+
+        validation_data = ((0, 'no_data', mask_set.no_data, np.array([[0, 0, 0, 0],
+                                                                      [0, 0, 1, 0],
+                                                                      [0, 0, 0, 0]],
+                                                                     dtype=np.uint8)),
+                           (4, 'vegetation', mask_set.vegetation, np.array([[0, 0, 0, 0],
+                                                                            [0, 0, 0, 1],
+                                                                            [0, 0, 0, 0]],
+                                                                           dtype=np.uint8)),
+                           (10, 'cirrus', mask_set.cirrus, np.array([[0, 0, 0, 0],
+                                                                     [0, 0, 0, 0],
+                                                                     [0, 0, 0, 1]],
+                                                                    dtype=np.uint8)),
+                           (6, 'water', mask_set.water, np.array([[0, 0, 0, 0],
+                                                                  [0, 1, 0, 0],
+                                                                  [0, 0, 0, 0]],
+                                                                 dtype=np.uint8)))
+
+        for index, name, mask, data in validation_data:
+            msg = f'index={index}, name={name!r}, data={data!r}'
+            self.assertIs(mask, mask_set[index], msg=msg)
+            self.assertIs(mask, mask_set[name], msg=msg)
+            assert_array_almost_equal(mask.values, data, err_msg=msg)
+
+        self.assertEqual(set(s2l2a_slc_meanings), set(dir(mask_set)))