Repository: BAMWelDX/weldx

Issue:
TimeSeries.interp_time should return a new TimeSeries 
Currently, `TimeSeries.interp_time` returns an `xarray.DataArray`. I think it does make more sense to return a new `TimeSeries` instead. If the user really wants to get the underlying xarray object, he can fetch it after the interpolation.

Discussion:
vhirtham: create new issue
CagtayFabry: what do you think about adding a `.time` property to `TimeSeries` directly @vhirtham ? In line with LCS and similar classes
vhirtham: > what do you think about adding a `.time` property to `TimeSeries` directly @vhirtham ? In line with LCS and similar classes

Not sure what you intend, cause the `TimeSeries` already has a `time` property (line 501)
CagtayFabry: > 
> 
> > what do you think about adding a `.time` property to `TimeSeries` directly @vhirtham ? In line with LCS and similar classes
> 
> Not sure what you intend, cause the `TimeSeries` already has a `time` property (line 501)

exactly that, so disregard :wink:
just wondering why we now need to call `data_array.time` here https://github.com/vhirtham/weldx/blob/3b06b74a800d2ac45b9794dfd81d49670d63f4e9/weldx/tests/test_core.py#L416-L419
review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/BAMWelDX/weldx/pull/353"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/353?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#353](https://codecov.io/gh/BAMWelDX/weldx/pull/353?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (44de634) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/3f2e32e36cdf7b6ca4c5dc26ada34fbf2c70c08c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (3f2e32e) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/BAMWelDX/weldx/pull/353/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/353?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)

```diff
@@           Coverage Diff           @@
##           master     #353   +/-   ##
=======================================
  Coverage   97.46%   97.47%           
=======================================
  Files          86       86           
  Lines        5172     5192   +20     
=======================================
+ Hits         5041     5061   +20     
  Misses        131      131           
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/353?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/353/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvY29yZS5weQ==) | `99.47% <100.00%> (+0.06%)` | :arrow_up: |
| [weldx/util.py](https://codecov.io/gh/BAMWelDX/weldx/pull/353/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdXRpbC5weQ==) | `98.74% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/353?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/353?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Last update [3f2e32e...44de634](https://codecov.io/gh/BAMWelDX/weldx/pull/353?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).

vhirtham: > > > what do you think about adding a `.time` property to `TimeSeries` directly @vhirtham ? In line with LCS and similar classes
> > 
> > 
> > Not sure what you intend, cause the `TimeSeries` already has a `time` property (line 501)
> 
> exactly that, so disregard 😉
> just wondering why we now need to call `data_array.time` here https://github.com/vhirtham/weldx/blob/3b06b74a800d2ac45b9794dfd81d49670d63f4e9/weldx/tests/test_core.py#L416-L419

The reason was that `.time` returns `None` if the internal time data has only one value, e.g the TS is constant. But we checked is the stored value matches the passed one. Don't know if that made sense, but I modified the test in favor of using `.time`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4d1e28..ea5da23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,13 +9,12 @@
   structures. [[#328]](https://github.com/BAMWelDX/weldx/pull/328)
 - added `WeldxFile` wrapper to handle asdf files with history and schemas more
   easily. [[#341]](https://github.com/BAMWelDX/weldx/pull/341).
-- added `WeldxFile` wrapper to handle asdf files with history and schemas more
-  easily. [[#341]](https://github.com/BAMWelDX/weldx/pull/341).
 - add `"step"` as additional method to `util.xr_interp_like` [[#363]](https://github.com/BAMWelDX/weldx/pull/363)
 - add `util.compare_nested_eq` decorator for dataclasses with array-like
   fields [[#378]](https://github.com/BAMWelDX/weldx/pull/378)
 - adds a `dataclass_serialization_class` utility function that automatically generates the asdf serialization class for 
   python dataclasses. [[#380]](https://github.com/BAMWelDX/weldx/pull/380)
+- Added method to set the interpolation method to the `TimeSeries` [[#353]](https://github.com/BAMWelDX/weldx/pull/353)
 
 ### changes
 
@@ -24,7 +23,11 @@
 - `get_yaml_header` received a new option parse, which optionally returns the parsed YAML header
   as `asdf.tagged.TaggedDict`. [[#338]](https://github.com/BAMWelDX/weldx/pull/338)
 - refactor `asdf_json_repr` into `view_tree` [[#339]](https://github.com/BAMWelDX/weldx/pull/339)
-- The `MeasurementChain` is now internally based on a `networkx.DiGraph`. New functions are also added to the class to
+- `TimeSeries.interp_time` [[#353]](https://github.com/BAMWelDX/weldx/pull/353)
+    - now returns a new `TimeSeries` instead of a `xarray.DataArray`
+    - if the data has already been interpolated before, a warning is emitted
+    - `TimeSeries` supports now all interpolation methods supported by xarray
+- The `MeasurementChain` is now internally based on a `networkx.DiGraph`. New functions are also added to the class to 
   simplify its usage. [[#326]](https://github.com/BAMWelDX/weldx/pull/326)
   The following additional changes were applied during the update of the `MeasurementChain`:
     - renamed `DataTransformation` class to `SignalTransformation`
@@ -38,6 +41,7 @@
 - Add new tutorial about the `MeasurementChain` [[#326]](https://github.com/BAMWelDX/weldx/pull/326)
 - Updated the measurement tutorial [[#326]](https://github.com/BAMWelDX/weldx/pull/326)
 
+
 ### ASDF
 
 - fix inline array serialization for new 64bit inline limit [[#218]](https://github.com/BAMWelDX/weldx/pull/218)
diff --git a/tutorials/timeseries_01.ipynb b/tutorials/timeseries_01.ipynb
index 9416471..1d6b284 100644
--- a/tutorials/timeseries_01.ipynb
+++ b/tutorials/timeseries_01.ipynb
@@ -39,6 +39,7 @@
    "outputs": [],
    "source": [
     "def plot_ts(ts):\n",
+    "    ts = ts.data_array\n",
     "    fig, ax = plt.subplots(1,1)\n",
     "    x = ts.time.data.astype('timedelta64[ms]').astype(int)/1000.0\n",
     "    plt.plot(x,ts.data.magnitude,'-o')\n",
@@ -397,7 +398,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.2"
+   "version": "3.9.4"
   }
  },
  "nbformat": 4,
diff --git a/tutorials/welding_example_02_weaving.ipynb b/tutorials/welding_example_02_weaving.ipynb
index f16ac41..b6574f5 100644
--- a/tutorials/welding_example_02_weaving.ipynb
+++ b/tutorials/welding_example_02_weaving.ipynb
@@ -629,7 +629,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.2"
+   "version": "3.9.4"
   }
  },
  "nbformat": 4,
diff --git a/weldx/core.py b/weldx/core.py
index 0c6849e..60aa615 100644
--- a/weldx/core.py
+++ b/weldx/core.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
+from warnings import warn
 
 import numpy as np
 import pandas as pd
@@ -247,13 +248,21 @@ class MathematicalExpression:
 class TimeSeries:
     """Describes the behaviour of a quantity in time."""
 
-    _valid_interpolations = ["step", "linear"]
+    _valid_interpolations = [
+        "step",
+        "linear",
+        "nearest",
+        "zero",
+        "slinear",
+        "quadratic",
+        "cubic",
+    ]
 
     def __init__(
         self,
         data: Union[pint.Quantity, MathematicalExpression],
         time: Union[None, pd.TimedeltaIndex, pint.Quantity] = None,
-        interpolation: str = "linear",
+        interpolation: str = None,
     ):
         """Construct a TimSeries.
 
@@ -276,68 +285,12 @@ class TimeSeries:
         self._time_var_name = None
         self._shape = None
         self._units = None
+        self._interp_counter = 0
 
         if isinstance(data, pint.Quantity):
-            if not np.iterable(data):  # expand dim for scalar input
-                data = np.expand_dims(data, 0)
-            if time is None:  # constant value case
-                time = pd.TimedeltaIndex([0])
-                interpolation = None
-            elif interpolation not in self._valid_interpolations:
-                raise ValueError(
-                    "A valid interpolation method must be specified if discrete "
-                    f'values are used. "{interpolation}" is not supported'
-                )
-            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,
-                attrs={"interpolation": interpolation},
-            )
-            self._data = dax.rename({"dim_0": "time"}).assign_coords({"time": time})
-
+            self._initialize_discrete(data, time, interpolation)
         elif isinstance(data, MathematicalExpression):
-
-            if data.num_variables != 1:
-                raise Exception(
-                    "The mathematical expression must have exactly 1 free "
-                    "variable that represents time."
-                )
-            time_var_name = data.get_variable_names()[0]
-            try:
-                eval_data = data.evaluate(**{time_var_name: Q_(1, "second")})
-                self._units = eval_data.units
-                if np.iterable(eval_data):
-                    self._shape = eval_data.shape
-                else:
-                    self._shape = (1,)
-            except pint.errors.DimensionalityError:
-                raise Exception(
-                    "Expression can not be evaluated with "
-                    '"weldx.Quantity(1, "seconds")"'
-                    ". Ensure that every parameter posses the correct unit."
-                )
-
-            self._data = data
-            self._time_var_name = time_var_name
-
-            try:
-                self.interp_time(Q_([1, 2], "second"))
-                self.interp_time(Q_([1, 2, 3], "second"))
-            except Exception as e:
-                raise Exception(
-                    "The expression can not be evaluated with arrays of time deltas. "
-                    "Ensure that all parameters that are multiplied with the time "
-                    "variable have an outer dimension of size 1. This dimension is "
-                    "broadcasted during multiplication. The original error message was:"
-                    f' "{str(e)}"'
-                )
-
+            self._init_expression(data)
         else:
             raise TypeError(f'The data type "{type(data)}" is not supported.')
 
@@ -385,6 +338,135 @@ class TimeSeries:
             )
         return representation + f"Units:\n\t{self.units}\n"
 
+    def _initialize_discrete(
+        self,
+        data: pint.Quantity,
+        time: Union[None, pd.TimedeltaIndex, pint.Quantity],
+        interpolation: str,
+    ):
+        """Initialize the internal data with discrete values."""
+        # set default interpolation
+        if interpolation is None:
+            interpolation = "step"
+
+        # expand dim for scalar input
+        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})
+        self.interpolation = interpolation
+
+    def _init_expression(self, data):
+        """Initialize the internal data with a mathematical expression."""
+        if data.num_variables != 1:
+            raise Exception(
+                "The mathematical expression must have exactly 1 free "
+                "variable that represents time."
+            )
+
+        # check that the expression can be evaluated with a time quantity
+        time_var_name = data.get_variable_names()[0]
+        try:
+            eval_data = data.evaluate(**{time_var_name: Q_(1, "second")})
+            self._units = eval_data.units
+            if np.iterable(eval_data):
+                self._shape = eval_data.shape
+            else:
+                self._shape = (1,)
+        except pint.errors.DimensionalityError:
+            raise Exception(
+                "Expression can not be evaluated with "
+                '"weldx.Quantity(1, "seconds")"'
+                ". Ensure that every parameter posses the correct unit."
+            )
+
+        # assign internal variables
+        self._data = data
+        self._time_var_name = time_var_name
+
+        # check that all parameters of the expression support time arrays
+        try:
+            self.interp_time(Q_([1, 2], "second"))
+            self.interp_time(Q_([1, 2, 3], "second"))
+        except Exception as e:
+            raise Exception(
+                "The expression can not be evaluated with arrays of time deltas. "
+                "Ensure that all parameters that are multiplied with the time "
+                "variable have an outer dimension of size 1. This dimension is "
+                "broadcasted during multiplication. The original error message was:"
+                f' "{str(e)}"'
+            )
+
+    def _interp_time_discrete(
+        self, time: Union[pd.TimedeltaIndex, pint.Quantity]
+    ) -> xr.DataArray:
+        """Interpolate the time series if its data is composed of discrete values.
+
+        See `interp_time` for interface description.
+
+        """
+        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".'
+            )
+
+        return ut.xr_interp_like(
+            self._data,
+            {"time": time},
+            method=self.interpolation,
+            assume_sorted=False,
+            broadcast_missing=False,
+        )
+
+    def _interp_time_expression(
+        self, time: Union[pd.TimedeltaIndex, pint.Quantity], time_unit: str
+    ) -> xr.DataArray:
+        """Interpolate the time series if its data is a mathematical expression.
+
+        See `interp_time` for interface description.
+
+        """
+        # Transform time to both formats
+        if isinstance(time, pint.Quantity) and time.check(UREG.get_dimensionality("s")):
+            time_q = time
+            time_pd = ut.to_pandas_time_index(time)
+        elif isinstance(time, pd.TimedeltaIndex):
+            time_q = ut.pandas_time_delta_to_quantity(time, time_unit)
+            time_pd = time
+        else:
+            raise ValueError(
+                '"time" must be a time quantity or a "pandas.TimedeltaIndex".'
+            )
+
+        if len(self.shape) > 1 and np.iterable(time_q):
+            while len(time_q.shape) < len(self.shape):
+                time_q = time_q[:, np.newaxis]
+
+        # evaluate expression
+        data = self._data.evaluate(**{self._time_var_name: time_q})
+        data = data.astype(float).to_reduced_units()  # float conversion before reduce!
+
+        # create data array
+        if not np.iterable(data):  # make sure quantity is not scalar value
+            data = np.expand_dims(data, 0)
+
+        dax = xr.DataArray(data=data)  # don't know exact dimensions so far
+        return dax.rename({"dim_0": "time"}).assign_coords({"time": time_pd})
+
     @property
     def data(self) -> Union[pint.Quantity, MathematicalExpression]:
         """Return the data of the TimeSeries.
@@ -433,6 +515,18 @@ class TimeSeries:
             return self._data.attrs["interpolation"]
         return None
 
+    @interpolation.setter
+    def interpolation(self, interpolation):
+        if isinstance(self._data, xr.DataArray):
+            if interpolation not in self._valid_interpolations:
+                raise ValueError(
+                    "A valid interpolation method must be specified if discrete "
+                    f'values are used. "{interpolation}" is not supported'
+                )
+            if self.time is None and interpolation != "step":
+                interpolation = "step"
+            self.data_array.attrs["interpolation"] = interpolation
+
     @property
     def time(self) -> Union[None, pd.TimedeltaIndex]:
         """Return the data's timestamps.
@@ -449,7 +543,7 @@ class TimeSeries:
 
     def interp_time(
         self, time: Union[pd.TimedeltaIndex, pint.Quantity], time_unit: str = "s"
-    ) -> xr.DataArray:
+    ) -> "TimeSeries":
         """Interpolate the TimeSeries in time.
 
         If the internal data consists of discrete values, an interpolation with the
@@ -470,55 +564,24 @@ class TimeSeries:
 
         Returns
         -------
-        xarray.DataArray:
-            A data array containing the interpolated data.
+        TimeSeries :
+            A new `TimeSeries` object containing the interpolated data.
 
         """
-        if isinstance(self._data, xr.DataArray):
-            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".'
-                )
-            # constant values are also treated by this branch
-            if self._data.attrs["interpolation"] == "linear" or self.shape[0] == 1:
-                return ut.xr_interp_like(
-                    self._data,
-                    {"time": time},
-                    assume_sorted=False,
-                    broadcast_missing=False,
-                )
-
-            dax = self._data.reindex({"time": time}, method="ffill")
-            return dax.fillna(self._data[0])
-
-        # Transform time to both formats
-        if isinstance(time, pint.Quantity) and time.check(UREG.get_dimensionality("s")):
-            time_q = time
-            time_pd = ut.to_pandas_time_index(time)
-        elif isinstance(time, pd.TimedeltaIndex):
-            time_q = ut.pandas_time_delta_to_quantity(time, time_unit)
-            time_pd = time
-        else:
-            raise ValueError(
-                '"time" must be a time quantity or a "pandas.TimedeltaIndex".'
+        if self._interp_counter > 0:
+            warn(
+                "The data of the time series has already been interpolated "
+                f"{self._interp_counter} time(s)."
             )
 
-        if len(self.shape) > 1 and np.iterable(time_q):
-            while len(time_q.shape) < len(self.shape):
-                time_q = time_q[:, np.newaxis]
-
-        # evaluate expression
-        data = self._data.evaluate(**{self._time_var_name: time_q})
-        data = data.astype(float).to_reduced_units()  # float conversion before reduce!
-
-        # create data array
-        if not np.iterable(data):  # make sure quantity is not scalar value
-            data = np.expand_dims(data, 0)
+        if isinstance(self._data, xr.DataArray):
+            dax = self._interp_time_discrete(time)
+        else:
+            dax = self._interp_time_expression(time, time_unit)
 
-        dax = xr.DataArray(data=data)  # don't know exact dimensions so far
-        return dax.rename({"dim_0": "time"}).assign_coords({"time": time_pd})
+        ts = TimeSeries(data=dax.data, time=time, interpolation=self.interpolation)
+        ts._interp_counter = self._interp_counter + 1
+        return ts
 
     @property
     def shape(self) -> Tuple:
diff --git a/weldx/util.py b/weldx/util.py
index ddd69ae..611d22d 100644
--- a/weldx/util.py
+++ b/weldx/util.py
@@ -281,7 +281,7 @@ def lcs_coords_from_ts(
         A DataArray with correctly labeled dimensions to be used for LCS creation.
 
     """
-    ts_data = ts.interp_time(time=time)
+    ts_data = ts.interp_time(time=time).data_array
     # assign vector coordinates and convert to mm
     ts_data = ts_data.rename({"dim_1": "c"}).assign_coords({"c": ["x", "y", "z"]})
     ts_data.data = ts_data.data.to("mm").magnitude


Test patch:
diff --git a/weldx/tests/test_core.py b/weldx/tests/test_core.py
index 72376a2..2c4f7be 100644
--- a/weldx/tests/test_core.py
+++ b/weldx/tests/test_core.py
@@ -242,10 +242,13 @@ class TestTimeSeries:
     def test_construction_discrete(data, time, interpolation, shape_exp):
         """Test the construction of the TimeSeries class."""
         # set expected values
-        if isinstance(time, pint.Quantity):
-            time_exp = pd.TimedeltaIndex(time.magnitude, unit="s")
-        else:
-            time_exp = time
+        time_exp = time
+        if isinstance(time_exp, pint.Quantity):
+            time_exp = pd.TimedeltaIndex(time_exp.m, unit="s")
+
+        exp_interpolation = interpolation
+        if len(data.m.shape) == 0 and interpolation is None:
+            exp_interpolation = "step"
 
         # create instance
         ts = TimeSeries(data=data, time=time, interpolation=interpolation)
@@ -253,12 +256,12 @@ class TestTimeSeries:
         # check
         assert np.all(ts.data == data)
         assert np.all(ts.time == time_exp)
-        assert ts.interpolation == interpolation
+        assert ts.interpolation == exp_interpolation
         assert ts.shape == shape_exp
         assert data.check(UREG.get_dimensionality(ts.units))
 
         assert np.all(ts.data_array.data == data)
-        assert ts.data_array.attrs["interpolation"] == interpolation
+        assert ts.data_array.attrs["interpolation"] == exp_interpolation
         if time_exp is None:
             assert "time" not in ts.data_array
         else:
@@ -302,7 +305,6 @@ class TestTimeSeries:
         "data, time, interpolation, exception_type, test_name",
         [
             (values_def, time_def, "int", ValueError, "# unknown interpolation"),
-            (values_def, time_def, None, ValueError, "# wrong interp. parameter type"),
             (values_def, time_def.magnitude, "step", ValueError, "# invalid time type"),
             (me_too_many_vars, None, None, Exception, "# too many free variables"),
             (me_param_units, None, None, Exception, "# incompatible parameter units"),
@@ -409,12 +411,28 @@ class TestTimeSeries:
         result = ts.interp_time(time)
 
         assert np.all(np.isclose(result.data.magnitude, magnitude_exp))
-        assert Q_(1, str(result.data.units)) == Q_(1, unit_exp)
+        assert Q_(1, str(result.units)) == Q_(1, unit_exp)
 
-        if isinstance(time, pint.Quantity):
-            assert np.all(result.time == ut.to_pandas_time_index(time))
-        else:
-            assert np.all(result.time == time)
+        exp_time = time
+        if isinstance(exp_time, pint.Quantity):
+            exp_time = ut.to_pandas_time_index(time)
+        if len(exp_time) == 1:
+            exp_time = None
+
+        assert np.all(result.time == exp_time)
+
+    # test_interp_time_warning ---------------------------------------------------------
+
+    @staticmethod
+    def test_interp_time_warning():
+        """Test if a warning is emitted when interpolating already interpolated data."""
+        ts = TimeSeries(data=Q_([1, 2, 3], "m"), time=Q_([0, 1, 2], "s"))
+        with pytest.warns(None) as recorded_warnings:
+            ts_interp = ts.interp_time(Q_([0.25, 0.5, 0.75, 1], "s"))
+        assert len(recorded_warnings) == 0
+
+        with pytest.warns(UserWarning):
+            ts_interp.interp_time(Q_([0.4, 0.6], "s"))
 
     # test_interp_time_exceptions ------------------------------------------------------