Repository: dcs4cop/xcube
Pull request #36 (2019-03-07 13:31:06)

Convert vars to dims
When accepting the PR a new API function (```vars_to_dim```) will be introduced that converts the 
variables of  a data cube into a single dimension. 


Patch:
diff --git a/xcube/api/__init__.py b/xcube/api/__init__.py
index b2e8306f..b982ea7e 100644
--- a/xcube/api/__init__.py
+++ b/xcube/api/__init__.py
@@ -1,5 +1,6 @@
 from .api import XCubeAPI
 from .chunk import chunk_dataset
+from .vars_to_dim import vars_to_dim
 from .dump import dump_dataset
 from .new import new_cube
 from .extract import get_cube_point_indexes, get_cube_values_for_indexes, get_cube_values_for_points, get_dataset_indexes
diff --git a/xcube/api/api.py b/xcube/api/api.py
index 6d09ad50..4f211c39 100644
--- a/xcube/api/api.py
+++ b/xcube/api/api.py
@@ -5,6 +5,7 @@ import numpy as np
 import pandas as pd
 import xarray as xr
 
+from .vars_to_dim import vars_to_dim
 from .chunk import chunk_dataset
 from .dump import dump_dataset
 from .new import new_cube
@@ -223,6 +224,15 @@ class XCubeAPI:
                              chunk_sizes=chunk_sizes,
                              format_name=format_name)
 
+    def vars_to_dim(self, dim_name: str = 'newdim'):
+        """
+        Convert data variables into a dimension
+
+        :param dim_name: The name of the new dimension ['vars']
+        :return: A new data cube with the new dimension.
+        """
+        return vars_to_dim(self._dataset, dim_name)
+
     def dump(self,
              variable_names=None,
              show_var_encoding=False) -> str:
@@ -251,7 +261,6 @@ class XCubeAPI:
         """
         Select data variable from given *dataset* and create new dataset.
 
-        :param dataset: The dataset from which to select variables.
         :param var_names: The names of data variables to select.
         :return: A new dataset. It is empty, if *var_names* is empty. It is *dataset*, if *var_names* is None.
         """
diff --git a/xcube/api/vars_to_dim.py b/xcube/api/vars_to_dim.py
new file mode 100644
index 00000000..57f293b3
--- /dev/null
+++ b/xcube/api/vars_to_dim.py
@@ -0,0 +1,40 @@
+# The MIT License (MIT)
+# Copyright (c) 2019 by the xcube development team and contributors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import xarray as xr
+
+def vars_to_dim(cube: xr.Dataset, dim_name: str = 'newdim'):
+    """
+    Convert data variables into a dimension
+
+    :param cube: The data cube.
+    :param dim_name: The name of the new dimension ['vars']
+    :return: A new data cube with the new dimension.
+    """
+
+    if len(cube.variables) == 0:
+        raise ValueError("ERROR: Cube has no variables. Exiting.")
+
+    da = xr.concat([cube[var_name] for var_name in cube.data_vars], dim_name)
+    var_coords = xr.DataArray([var_name for var_name in cube.data_vars], dims=[dim_name])
+    da = da.assign_coords(**{dim_name: var_coords})
+
+    return xr.Dataset(dict(**{dim_name + '_vars': da}))
diff --git a/xcube/cli/cli.py b/xcube/cli/cli.py
index 96d0a697..7fd6df3f 100644
--- a/xcube/cli/cli.py
+++ b/xcube/cli/cli.py
@@ -118,6 +118,42 @@ def dump(dataset, variable, encoding):
         print(text)
 
 
+# noinspection PyShadowingBuiltins
+@click.command(name="vars2dim")
+@click.argument('cube', metavar='<cube>')
+@click.option('--dim_name', '-d', metavar='<dimension name>', help="Name of the new dimension ['var']")
+@click.option('--output', '-o', metavar='<output>',
+              help="Output file.")
+@click.option('--format', '-f', metavar='<format>', type=click.Choice(['zarr', 'netcdf']),
+              help="Format of the output. If not given, guessed from <output>.")
+def vars2dim(cube, dim_name, output=None, format=None):
+    """
+    Write a new dataset with identical data and compression but with new chunk sizes.
+    """
+
+    from xcube.util.dsio import guess_dataset_format
+    import os
+
+    if not dim_name:
+        dim_name = 'newdim'
+
+    if not output:
+        basename = os.path.splitext(cube)[0]
+        ext = os.path.splitext(cube)[1]
+
+        output = basename + '_converted' + ext
+
+    format_name = format if format else guess_dataset_format(output)
+
+    from xcube.api import open_dataset, vars_to_dim, write_dataset
+
+    with open_dataset(input_path=cube) as ds:
+
+        converted_dataset = vars_to_dim(ds, dim_name=dim_name)
+
+        write_dataset(converted_dataset, output_path=output, format_name=format_name)
+
+
 # noinspection PyShadowingBuiltins
 @click.group()
 @click.version_option(version)
@@ -131,6 +167,7 @@ cli.add_command(chunk)
 cli.add_command(dump)
 cli.add_command(extract)
 cli.add_command(grid)
+cli.add_command(vars2dim)
 cli.add_command(gen)
 
 


Test patch:
diff --git a/test/api/test_api.py b/test/api/test_api.py
index 9668930f..79608880 100644
--- a/test/api/test_api.py
+++ b/test/api/test_api.py
@@ -2,6 +2,7 @@ import unittest
 
 import xarray as xr
 
+from test.sampledata import new_test_dataset
 from xcube.api.api import XCubeAPI
 
 
@@ -23,3 +24,9 @@ class XCubeAPITest(unittest.TestCase):
 
     def test_dump(self):
         self.assertIsInstance(xr.Dataset().xcube.dump(), str)
+
+    def test_vars_to_dim(self):
+        dataset = new_test_dataset(["2010-01-01", "2010-01-02", "2010-01-03", "2010-01-04", "2010-01-05"],
+                                   precipitation=0.4, temperature=275.2)
+
+        self.assertIsInstance(dataset.xcube.vars_to_dim(), xr.Dataset)
diff --git a/test/api/test_vars_to_dim.py b/test/api/test_vars_to_dim.py
new file mode 100644
index 00000000..5966beb6
--- /dev/null
+++ b/test/api/test_vars_to_dim.py
@@ -0,0 +1,22 @@
+import unittest
+import xarray as xr
+
+from test.sampledata import new_test_dataset
+from xcube.api.vars_to_dim import vars_to_dim
+
+
+class VarsToDimTest(unittest.TestCase):
+    def test_vars_to_dim(self):
+        dataset = new_test_dataset(["2010-01-01", "2010-01-02", "2010-01-03", "2010-01-04", "2010-01-05"],
+                                   precipitation=0.4, temperature=275.2)
+
+        print(dataset.dims)
+
+        ds = vars_to_dim(dataset)
+
+        self.assertIn("newdim_vars", ds.variables)
+        newdim_vars = ds["newdim_vars"]
+        self.assertTrue(hasattr(newdim_vars, "encoding"))
+        self.assertEquals(len(dataset.dims), 3)
+
+        self.assertRaises(ValueError, vars_to_dim, xr.Dataset())
diff --git a/test/cli/test_cli.py b/test/cli/test_cli.py
index a6331838..6f890c3d 100644
--- a/test/cli/test_cli.py
+++ b/test/cli/test_cli.py
@@ -8,6 +8,7 @@ import click
 import click.testing
 import xarray as xr
 
+from test.sampledata import new_test_dataset
 from xcube.api.new import new_cube
 from xcube.cli.cli import cli, _parse_kwargs
 
@@ -67,6 +68,29 @@ class DumpTest(CliTest):
         self.assertEqual(0, result.exit_code)
 
 
+class Vars2DimTest(CliTest):
+
+    def test_vars2dim(self):
+        dataset = new_test_dataset(["2010-01-01", "2010-01-02", "2010-01-03", "2010-01-04", "2010-01-05"],
+                                   precipitation=0.4, temperature=275.2)
+        dataset.to_zarr('tt.zarr')
+        output_path = "tt_converted.zarr"
+        result = self.invoke_cli(["vars2dim",
+                                  'tt.zarr'])
+
+        self.assertEqual(0, result.exit_code)
+        self.assertTrue(os.path.isdir(output_path))
+        try:
+            ds = xr.open_zarr(output_path)
+            self.assertIn("newdim_vars", ds.variables)
+            newdim_vars = ds["newdim_vars"]
+            self.assertTrue(hasattr(newdim_vars, "encoding"))
+        finally:
+            print('hello')
+            shutil.rmtree(output_path, ignore_errors=True)
+            shutil.rmtree('tt.zarr', ignore_errors=True)
+
+
 class ChunkTest(CliTest):
 
     def test_chunk_zarr(self):