Repository: BAMWelDX/weldx

Issue:
TimeSeries __init__ should understand xarray structures
As the title says, a `TimeSeries` should be constructible from a xarray structure that matches the internal format. This is currently not possible but would make it much easier to convert the data from our lab.

Discussion:
pep8speaks: Hello @vhirtham! Thanks for opening this PR. 

* In the file [`weldx/core.py`](https://github.com/BAMWelDX/weldx/blob/0a32ebeb0933f49a1784bad3756a22635999cb2d/weldx/core.py):

> [Line 354:25](https://github.com/BAMWelDX/weldx/blob/0a32ebeb0933f49a1784bad3756a22635999cb2d/weldx/core.py#L354): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ':'
> [Line 354:25](https://github.com/BAMWelDX/weldx/blob/0a32ebeb0933f49a1784bad3756a22635999cb2d/weldx/core.py#L354): [E999](https://duckduckgo.com/?q=pep8%20E999) SyntaxError: invalid syntax
> [Line 354:27](https://github.com/BAMWelDX/weldx/blob/0a32ebeb0933f49a1784bad3756a22635999cb2d/weldx/core.py#L354): [E251](https://duckduckgo.com/?q=pep8%20E251) unexpected spaces around keyword / parameter equals

Do see the [Hitchhiker's guide to code style](https://goo.gl/hqbW4r)
codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#429](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (0a32ebe) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/d29a2de0c74264db977ffd21677c823690133663?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (d29a2de) will **decrease** coverage by `0.21%`.
> The diff coverage is `52.00%`.

> :exclamation: Current head 0a32ebe differs from pull request most recent head 0682252. Consider uploading reports for the commit 0682252 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/BAMWelDX/weldx/pull/429/graphs/tree.svg?width=650&height=150&src=pr&token=wdof1qQTsn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)

```diff
@@            Coverage Diff             @@
##           master     #429      +/-   ##
==========================================
- Coverage   97.25%   97.03%   -0.22%     
==========================================
  Files          87       87              
  Lines        5388     5403      +15     
==========================================
+ Hits         5240     5243       +3     
- Misses        148      160      +12     
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) | Coverage Δ | |
|---|---|---|
| [weldx/core.py](https://codecov.io/gh/BAMWelDX/weldx/pull/429/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvY29yZS5weQ==) | `93.93% <52.00%> (-5.14%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Last update [d29a2de...0682252](https://codecov.io/gh/BAMWelDX/weldx/pull/429?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff8f2cb..28c2458 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@
 
 ### changes
 
+- `TimeSeries.__init__` accepts `xarray.DataArray` as `data`
+  parameter [[#429]](https://github.com/BAMWelDX/weldx/pull/429)
+
 ### fixes
 
 ### documentation
diff --git a/weldx/core.py b/weldx/core.py
index 5953816..b760ea6 100644
--- a/weldx/core.py
+++ b/weldx/core.py
@@ -295,7 +295,7 @@ class TimeSeries:
         self._units = None
         self._interp_counter = 0
 
-        if isinstance(data, pint.Quantity):
+        if isinstance(data, (pint.Quantity, xr.DataArray)):
             self._initialize_discrete(data, time, interpolation)
         elif isinstance(data, MathematicalExpression):
             self._init_expression(data)
@@ -346,9 +346,25 @@ class TimeSeries:
             )
         return representation + f"Units:\n\t{self.units}\n"
 
+    @staticmethod
+    def _check_data_array(data_array: xr.DataArray):
+        """Raise an exception if the 'DataArray' can't be used as 'self._data'."""
+        try:
+            ut.xr_check_coords(data_array, dict(time={"dtype": ["timedelta64[ns]"]}))
+        except (KeyError, TypeError, ValueError) as e:
+            raise type(e)(
+                "The provided 'DataArray' does not match the required pattern. It "
+                "needs to have a dimension called 'time' with coordinates of type "
+                "'timedelta64[ns]'. The error reported by the comparison function was:"
+                f"\n{e}"
+            )
+
+        if not isinstance(data_array.data, pint.Quantity):
+            raise TypeError("The data of the 'DataArray' must be a 'pint.Quantity'.")
+
     def _initialize_discrete(
         self,
-        data: pint.Quantity,
+        data: Union[pint.Quantity, xr.DataArray],
         time: Union[None, pd.TimedeltaIndex, pint.Quantity],
         interpolation: str,
     ):
@@ -357,24 +373,29 @@ class TimeSeries:
         if interpolation is None:
             interpolation = "step"
 
-        # expand dim for scalar input
-        data = Q_(data)
-        if not np.iterable(data):
-            data = np.expand_dims(data, 0)
-
-        # constant value case
-        if time is None:
-            time = pd.TimedeltaIndex([0])
-
-        if isinstance(time, pint.Quantity):
-            time = ut.to_pandas_time_index(time)
-        if not isinstance(time, pd.TimedeltaIndex):
-            raise ValueError(
-                '"time" must be a time quantity or a "pandas.TimedeltaIndex".'
-            )
+        if isinstance(data, xr.DataArray):
+            self._check_data_array(data)
+            data = data.transpose("time", ...)
+            self._data = data
+        else:
+            # expand dim for scalar input
+            data = Q_(data)
+            if not np.iterable(data):
+                data = np.expand_dims(data, 0)
+
+            # constant value case
+            if time is None:
+                time = pd.TimedeltaIndex([0])
+
+            if isinstance(time, pint.Quantity):
+                time = ut.to_pandas_time_index(time)
+            if not isinstance(time, pd.TimedeltaIndex):
+                raise ValueError(
+                    '"time" must be a time quantity or a "pandas.TimedeltaIndex".'
+                )
 
-        dax = xr.DataArray(data=data)
-        self._data = dax.rename({"dim_0": "time"}).assign_coords({"time": time})
+            dax = xr.DataArray(data=data)
+            self._data = dax.rename({"dim_0": "time"}).assign_coords({"time": time})
         self.interpolation = interpolation
 
     def _init_expression(self, data):


Test patch:
diff --git a/weldx/tests/test_core.py b/weldx/tests/test_core.py
index 2c4f7be..84fbb18 100644
--- a/weldx/tests/test_core.py
+++ b/weldx/tests/test_core.py
@@ -5,6 +5,7 @@ import numpy as np
 import pandas as pd
 import pint
 import pytest
+import xarray as xr
 
 import weldx.util as ut
 from weldx.constants import WELDX_QUANTITY as Q_
@@ -292,6 +293,30 @@ class TestTimeSeries:
         assert ts.data_array is None
         assert Q_(1, unit_exp).check(UREG.get_dimensionality(ts.units))
 
+    # test_init_data_array -------------------------------------------------------------
+
+    @staticmethod
+    @pytest.mark.parametrize(
+        "data, dims, coords, exception_type",
+        [
+            (Q_([1, 2, 3], "m"), "time", dict(time=TDI([1, 2, 3])), None),
+            (Q_([1, 2, 3], "m"), "a", dict(a=TDI([1, 2, 3])), KeyError),
+            (Q_([[1, 2]], "m"), ("a", "time"), dict(a=[2], time=TDI([1, 2])), None),
+            (Q_([1, 2, 3], "m"), "time", None, KeyError),
+            (Q_([1, 2, 3], "m"), "time", dict(time=[1, 2, 3]), TypeError),
+            ([1, 2, 3], "time", dict(time=TDI([1, 2, 3])), TypeError),
+        ],
+    )
+    def test_init_data_array(data, dims, coords, exception_type):
+        """Test the `__init__` method with an xarray as data parameter."""
+        da = xr.DataArray(data=data, dims=dims, coords=coords)
+        if exception_type is not None:
+            with pytest.raises(exception_type):
+                TimeSeries(da)
+        else:
+            ts = TimeSeries(da)
+            assert ts.data_array.dims[0] == "time"
+
     # test_construction_exceptions -----------------------------------------------------
 
     values_def = Q_([5, 7, 3, 6, 8], "m")