Repository: openradar/xradar

Issue:
Tree can't be exported with to_cfradial2 if time is first dimension
* xradar version: 0.1.0
* Python version: 3.11
* Operating System: Linux

### Description

While trying to export a DataTree (opened with `open_odim_datatree`) with `to_cfradial2` an error is raised.

```python
ValueError: cannot swap from dimension 'azimuth' because it is not an existing dimension
``` 

This doesn't happen with, if the `first_dim`-kwarg is omitted.

### What I Did

```python
import xarray as xr
import xradar as xd
from open_radar_data import DATASETS
filename = DATASETS.fetch("71_20181220_060628.pvol.h5")
dtree = xd.io.open_odim_datatree(filename, first_dim="time")
xd.io.to_cfradial2(dtree, "odim_as_cfradial2_2.nc")

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [26], in <cell line: 7>()
      5 dtree = xd.io.open_odim_datatree(filename, first_dim="time")
      6 display(dtree)
----> 7 xd.io.to_cfradial2(dtree.copy(), "odim_as_cfradial2_2.nc")

File /home/kai/miniconda/envs/wradlib_310/lib/python3.10/site-packages/xradar/io/export/cfradial2.py:73, in to_cfradial2(dtree, filename, engine, timestep)
     70 for grp in dtree.groups:
     71     if "sweep" in grp:
     72         dtree[grp] = DataTree(
---> 73             conform_cfradial2_sweep_group(
     74                 dtree[grp].to_dataset(), optional=False, dim0="azimuth"
     75             )
     76         )
     78 root = dtree["/"].to_dataset()
     79 # fix Conventions

File /home/kai/miniconda/envs/wradlib_310/lib/python3.10/site-packages/xradar/model.py:1005, in conform_cfradial2_sweep_group(obj, optional, dim0)
   1003 # swap dims, if needed
   1004 if dim0 != "time":
-> 1005     out = out.swap_dims({dim0: "time"})
   1006 # sort in any case
   1007 out = out.sortby("time")

File /home/kai/miniconda/envs/wradlib_310/lib/python3.10/site-packages/xarray/core/dataset.py:3824, in Dataset.swap_dims(self, dims_dict, **dims_kwargs)
   3822 for k, v in dims_dict.items():
   3823     if k not in self.dims:
-> 3824         raise ValueError(
   3825             f"cannot swap from dimension {k!r} because it is "
   3826             "not an existing dimension"
   3827         )
   3828     if v in self.variables and self.variables[v].dims != (k,):
   3829         raise ValueError(
   3830             f"replacement dimension {v!r} is not a 1D "
   3831             f"variable along the old dimension {k!r}"
   3832         )

ValueError: cannot swap from dimension 'azimuth' because it is not an existing dimension
```



Discussion:
kmuehlbauer: I'm still thinking about a proper test for that.
codecov[bot]: # [Codecov](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) Report
> Merging [#94](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) (ca512b5) into [main](https://codecov.io/gh/openradar/xradar/commit/010b104034c1ab14d9e4f4ca72ea15fe1014c0df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) (010b104) will **increase** coverage by `0.38%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head ca512b5 differs from pull request most recent head 18887cc. Consider uploading reports for the commit 18887cc to get more accurate results

```diff
@@            Coverage Diff             @@
##             main      #94      +/-   ##
==========================================
+ Coverage   87.75%   88.14%   +0.38%     
==========================================
  Files          19       19              
  Lines        3284     3289       +5     
==========================================
+ Hits         2882     2899      +17     
+ Misses        402      390      -12     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `88.14% <100.00%> (+0.38%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) | Coverage Δ | |
|---|---|---|
| [xradar/io/export/cfradial2.py](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#diff-eHJhZGFyL2lvL2V4cG9ydC9jZnJhZGlhbDIucHk=) | `86.95% <ø> (+52.17%)` | :arrow_up: |
| [xradar/model.py](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#diff-eHJhZGFyL21vZGVsLnB5) | `96.17% <100.00%> (+0.66%)` | :arrow_up: |
| [xradar/io/backends/iris.py](https://codecov.io/gh/openradar/xradar/pull/94?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#diff-eHJhZGFyL2lvL2JhY2tlbmRzL2lyaXMucHk=) | `84.75% <0.00%> (-0.10%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar)

kmuehlbauer: Added cfradial2 roundtrip test and test first_dim in one go. Pulling this in.
kmuehlbauer: There has been an issue with ReadTheDocs. I've reported that at RTD and they had a look immediately as it seems. Thanks RTD!

Patch:
diff --git a/docs/history.md b/docs/history.md
index 763f277..ebc1a86 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -1,5 +1,9 @@
 # History
 
+## Development Version
+
+* FIX: check for dim0 if not given, only swap_dims if needed ({issue}`92`), ({pull}`94`) by [@kmuehlbauer](https://github.com/kmuehlbauer)
+
 ## 0.1.0 (2023-02-23)
 
 * Add an example on reading multiple sweeps into a single object ({pull}`69`) by [@mgrover1](https://github.com/mgrover1)
diff --git a/xradar/io/export/cfradial2.py b/xradar/io/export/cfradial2.py
index 8535391..8a8cc73 100644
--- a/xradar/io/export/cfradial2.py
+++ b/xradar/io/export/cfradial2.py
@@ -71,7 +71,8 @@ def to_cfradial2(dtree, filename, engine=None, timestep=None):
         if "sweep" in grp:
             dtree[grp] = DataTree(
                 conform_cfradial2_sweep_group(
-                    dtree[grp].to_dataset(), optional=False, dim0="azimuth"
+                    dtree[grp].to_dataset(),
+                    optional=False,
                 )
             )
 
diff --git a/xradar/model.py b/xradar/model.py
index 4fb7947..9f5d885 100644
--- a/xradar/model.py
+++ b/xradar/model.py
@@ -989,7 +989,14 @@ def determine_cfradial2_sweep_variables(obj, optional, dim0):
     return keep_vars
 
 
-def conform_cfradial2_sweep_group(obj, optional, dim0):
+def conform_cfradial2_sweep_group(obj, optional, dim0=None):
+    if dim0 is None:
+        # handling first dimension
+        dim0 = "elevation" if obj.sweep_mode.load() == "rhi" else "azimuth"
+        if dim0 not in obj.dims:
+            dim0 = "time"
+            assert dim0 in obj.dims
+
     keep_vars = determine_cfradial2_sweep_variables(obj, optional, dim0)
     # calculate variables to remove and remove them
     var = set(obj.data_vars)
@@ -1001,7 +1008,7 @@ def conform_cfradial2_sweep_group(obj, optional, dim0):
     out.attrs = {}
 
     # swap dims, if needed
-    if dim0 != "time":
+    if dim0 != "time" and dim0 in obj.dims:
         out = out.swap_dims({dim0: "time"})
     # sort in any case
     out = out.sortby("time")


Test patch:
diff --git a/tests/io/test_io.py b/tests/io/test_io.py
index 003bee4..9bca5b8 100644
--- a/tests/io/test_io.py
+++ b/tests/io/test_io.py
@@ -4,7 +4,11 @@
 
 """Tests for `io` module."""
 
+import tempfile
+
+import datatree
 import numpy as np
+import pytest
 import xarray as xr
 
 import xradar.io
@@ -629,3 +633,27 @@ def test_odim_roundtrip(odim_file2):
     for d0, d1 in zip(dtree.groups, dtree2.groups):
         print(d0, d1)
         xr.testing.assert_equal(dtree[d0].ds, dtree2[d1].ds)
+
+
+@pytest.mark.parametrize("first_dim", ["time", "auto"])
+def test_cfradfial2_roundtrip(cfradial1_file, first_dim):
+    dtree0 = open_cfradial1_datatree(cfradial1_file, first_dim=first_dim)
+    # first write to cfradial2
+    outfile = tempfile.NamedTemporaryFile(mode="w+b").name
+    xradar.io.to_cfradial2(dtree0.copy(), outfile)
+    # then open cfradial2 file
+    dtree1 = datatree.open_datatree(outfile)
+    # and write again
+    outfile1 = tempfile.NamedTemporaryFile(mode="w+b").name
+    xradar.io.to_cfradial2(dtree1.copy(), outfile1)
+    # and open second cfradial2
+    dtree2 = datatree.open_datatree(outfile1)
+    # check equality
+    for d0, d1, d2 in zip(dtree0.groups, dtree1.groups, dtree2.groups):
+        if "sweep" in d0:
+            if first_dim == "auto":
+                first_dim = "azimuth"
+            assert first_dim in dtree0[d0].dims
+            assert "time" in dtree1[d1].dims
+            assert "time" in dtree2[d2].dims
+        xr.testing.assert_equal(dtree1[d1].ds, dtree2[d2].ds)