Repository: BAMWelDX/weldx

Issue:
CSM interpolation outside of time range
When interpolating between systems with any time dependency along the path, the result will always be time dependent even if the transformation is known to be static because no "real" time dependencies exist (or can exists) for the interpolation timeline.

Here is an example:
```python
import weldx
from weldx import Q_, CoordinateSystemManager, LocalCoordinateSystem

csm = CoordinateSystemManager("base")
# add A as time dependent in base
csm.add_cs("A","base",lcs=LocalCoordinateSystem(coordinates=[[0,0,0],[1,1,1]],time=Q_([0,1],"s")))
# add B as static in A
csm.add_cs("B","A",lcs=LocalCoordinateSystem())

# this is always static because no time dependencies are between A and B
print(csm.get_cs("B","A",time=Q_([2,3,4],"s")).time)
>>> None

# this is time dependent even though the result is known to be static
print(csm.get_cs("B","base",time=Q_([2,3,4],"s")).time)
>>> array([2000000000, 3000000000, 4000000000], dtype='timedelta64[ns]')

print(csm.get_cs("B","base",time=Q_([2,3,4],"s")).coordinates.data)
>>> array([[1., 1., 1.],
>>>        [1., 1., 1.],
>>>        [1., 1., 1.]])
```

This really only is an issue when considering large time interpolations.
I guess we can check in a form similar to (not actual code)
```python
if np.all(time<=csm.time_union.min()) or np.all(time>=csm.time_union.max()):
    # revert to single timestamp
```

Discussion:
CagtayFabry: it probably makes sense to implement this in the `get_cs_on_edge` method
codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/476?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#476](https://codecov.io/gh/BAMWelDX/weldx/pull/476?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (c7902ef) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/8a40edb2b3e7469c79e083636e25fcd45df5ac9f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (8a40edb) will **increase** coverage by `0.08%`.
> The diff coverage is `94.73%`.

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

```diff
@@            Coverage Diff             @@
##           master     #476      +/-   ##
==========================================
+ Coverage   97.16%   97.24%   +0.08%     
==========================================
  Files          89       89              
  Lines        5576     5593      +17     
==========================================
+ Hits         5418     5439      +21     
+ Misses        158      154       -4     
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) | Coverage Δ | |
|---|---|---|
| [weldx/transformations/cs\_manager.py](https://codecov.io/gh/BAMWelDX/weldx/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdHJhbnNmb3JtYXRpb25zL2NzX21hbmFnZXIucHk=) | `98.72% <ø> (ø)` | |
| [weldx/transformations/local\_cs.py](https://codecov.io/gh/BAMWelDX/weldx/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdHJhbnNmb3JtYXRpb25zL2xvY2FsX2NzLnB5) | `97.39% <94.73%> (-0.27%)` | :arrow_down: |
| [weldx/util.py](https://codecov.io/gh/BAMWelDX/weldx/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdXRpbC5weQ==) | `98.36% <0.00%> (-0.28%)` | :arrow_down: |
| [weldx/time.py](https://codecov.io/gh/BAMWelDX/weldx/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdGltZS5weQ==) | `98.01% <0.00%> (+2.97%)` | :arrow_up: |

------

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

pep8speaks: Hello @CagtayFabry! Thanks for updating this PR. 

* In the file [`weldx/tests/test_transformations.py`](https://github.com/BAMWelDX/weldx/blob/995960df11125726301ea8446e4e2093f4d92c83/weldx/tests/test_transformations.py):

> [Line 6:1](https://github.com/BAMWelDX/weldx/blob/995960df11125726301ea8446e4e2093f4d92c83/weldx/tests/test_transformations.py#L6): [F401](https://duckduckgo.com/?q=pep8%20F401) 'typing.Iterable' imported but unused






vhirtham: Need to do some cleanup and update the docstrings of the CSM and LCS methods to reflect the new behavior, but apart from that this should be ready. You can already have a look. I am not sure if there is a way to merge some of the conditions in `LCS.interp_time` but I think there isn't much room for optimizations because coordinates and orientations are pretty independent of each other. Maybe we can put their individual interpolations into small internal functions, but that would be just code cosmetics.
CagtayFabry: I took a quick look and I think the implementation looks good (and pretty readable now with the `Time` class), no need to micro manage there I guess 👍 

however for some of these larger `if` branches and longer functions I sometimes find it easier to have a few inline comments about the code (just for readability), I'll add an example
CagtayFabry: eventually we could also implement stuff like 'partially' overlapping times, so `LCS.time = [0,1,2,3,4,5]` and `interp_time=[3,4,5,6,7,8,9]` would give back `LCS.time=[3,4,5]` (I hope that makes sens 😉 )
vhirtham: I streamlined the behavior of `interp_time` so that it will also return a static system if only a single time value is passed. Before we had a time-dependent system with a single value. Now the time and the extra array dimension are stripped in that case.

> eventually we could also implement stuff like 'partially' overlapping times, so `LCS.time = [0,1,2,3,4,5]` and `interp_time=[3,4,5,6,7,8,9]` would give back `LCS.time=[3,4,5]` (I hope that makes sens 😉 )

I think this will make stuff too complicated. It might be easy to implement in the LCS, but it will be a nightmare in the CSM. We would have to check for this case separately and broadcast the missing values for the multiplication. But this would also require figuring out how many values were stripped on either side of the returned array. I would leave it as is.
CagtayFabry: > I streamlined the behavior of `interp_time` so that it will also return a static system if only a single time value is passed. Before we had a time-dependent system with a single value. Now the time and the extra array dimension are stripped in that case.
> 
Maybe we can keep the time as a dimension but not coordinate of the dataarray, I'll take a look

> > eventually we could also implement stuff like 'partially' overlapping times, so `LCS.time = [0,1,2,3,4,5]` and `interp_time=[3,4,5,6,7,8,9]` would give back `LCS.time=[3,4,5]` (I hope that makes sens 😉 )
> 
> I think this will make stuff too complicated. It might be easy to implement in the LCS, but it will be a nightmare in the CSM. We would have to check for this case separately and broadcast the missing values for the multiplication. But this would also require figuring out how many values were stripped on either side of the returned array. I would leave it as is.
agreed

vhirtham: > Maybe we can keep the time as a dimension but not coordinate of the dataarray, I'll take a look

I don't think that this makes sense. 


CagtayFabry: > > Maybe we can keep the time as a dimension but not coordinate of the dataarray, I'll take a look
> 
> I don't think that this makes sense.

Thought it could be nice to keep some time information but also not really needed right now I guess
vhirtham: @CagtayFabry Funny thing: I can't require your approval because you created the PR, but I can approve myself 😉 So are we good to go?
CagtayFabry: > 
> 
> > > Maybe we can keep the time as a dimension but not coordinate of the dataarray, I'll take a look
> > 
> > 
> > I don't think that this makes sense.
> 
> Thought it could be nice to keep some time information but also not really needed right now I guess

looked into it a little and I think it would be good to differentiate between xarray `coords` and `dimensions` more in our codebase (currently we mostly use coords which is probably not what we actually want)

I'll see about it in a separate PR

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0769365..bb2096a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,10 @@
 - move `sine` utility function to `weldx.welding.util` [[#439]](https://github.com/BAMWelDX/weldx/pull/439)
 - `LocalCoordinateSystem` and `CoordinateSystemManager` function parameters related to time now support all types that
   are also supported by the new `Time` class [[#448]](https://github.com/BAMWelDX/weldx/pull/448)
+- `LocalCoordinateSystem.interp_time` returns static systems if only a single time value is passed or if there is no
+  overlap between the interpolation time range and the coordinate systems time range. This also affects the results of
+  some `CoordinateSystemManager` methods (``get_cs``
+  , ``interp_time``) [[#476]](https://github.com/BAMWelDX/weldx/pull/476)
 
 ### fixes
 
diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py
index cc2a013..b92ae9a 100644
--- a/weldx/transformations/cs_manager.py
+++ b/weldx/transformations/cs_manager.py
@@ -1132,7 +1132,10 @@ class CoordinateSystemManager:
         reference.
 
         If any coordinate system that is involved in the coordinate transformation has
-        a time dependency, the returned coordinate system will also be time dependent.
+        a time dependency, the returned coordinate system will most likely be also time
+        dependent. This won't be the case if only a single time value is passed or if
+        the passed time values do not overlap with any of the time dependent coordinate
+        systems' time ranges.
 
         The timestamps of the returned system depend on the functions time parameter.
         By default, the time union of all involved coordinate systems is taken.
diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py
index 8bf3329..a6d25d7 100644
--- a/weldx/transformations/local_cs.py
+++ b/weldx/transformations/local_cs.py
@@ -547,6 +547,13 @@ class LocalCoordinateSystem(TimeDependent):
         """
         return self.time is not None or self._coord_ts is not None
 
+    @property
+    def has_timeseries(self) -> bool:
+        """Return `True` if the system has a `~weldx.core.TimeSeries` component."""
+        return isinstance(self.coordinates, TimeSeries) or isinstance(
+            self.orientation, TimeSeries
+        )
+
     @property
     def has_reference_time(self) -> bool:
         """Return `True` if the coordinate system has a reference time.
@@ -652,6 +659,43 @@ class LocalCoordinateSystem(TimeDependent):
         """
         return Rot.from_matrix(self.orientation.values)
 
+    def _interp_time_orientation(self, time: Time) -> Union[xr.DataArray, np.ndarray]:
+        """Interpolate the orientation in time."""
+        if self.orientation.ndim == 2:  # don't interpolate static
+            orientation = self.orientation
+        elif time.max() <= self.time.min():  # only use edge timestamp
+            orientation = self.orientation.values[0]
+        elif time.min() >= self.time.max():  # only use edge timestamp
+            orientation = self.orientation.values[-1]
+        else:  # full interpolation with overlapping times
+            orientation = ut.xr_interp_orientation_in_time(self.orientation, time)
+
+        if len(orientation) == 1:  # remove "time dimension" for single value cases
+            return orientation[0].values
+        return orientation
+
+    def _interp_time_coordinates(self, time: Time) -> Union[xr.DataArray, np.ndarray]:
+        """Interpolate the coordinates in time."""
+        if isinstance(self.coordinates, TimeSeries):
+            time_interp = Time(time, self.reference_time)
+            coordinates = self._coords_from_discrete_time_series(
+                self.coordinates.interp_time(time_interp)
+            )
+            if self.has_reference_time:
+                coordinates.weldx.time_ref = self.reference_time
+        elif self.coordinates.ndim == 1:  # don't interpolate static
+            coordinates = self.coordinates
+        elif time.max() <= self.time.min():  # only use edge timestamp
+            coordinates = self.coordinates.values[0]
+        elif time.min() >= self.time.max():  # only use edge timestamp
+            coordinates = self.coordinates.values[-1]
+        else:  # full interpolation with overlapping times
+            coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time)
+
+        if len(coordinates) == 1:  # remove "time dimension" for single value cases
+            return coordinates[0].values
+        return coordinates
+
     def interp_time(
         self,
         time: types_time_like,
@@ -659,11 +703,18 @@ class LocalCoordinateSystem(TimeDependent):
     ) -> LocalCoordinateSystem:
         """Interpolates the data in time.
 
+        Note that the returned system won't be time dependent anymore if only a single
+        time value was passed. The resulting system is constant and the passed time
+        value will be stripped from the result.
+        Additionally, if the passed time range does not overlap with the time range of
+        the coordinate system, the resulting system won't be time dependent neither
+        because the values outside of the coordinate systems time range are considered
+        as being constant.
+
         Parameters
         ----------
         time :
-            Series of times.
-            If passing "None" no interpolation will be performed.
+            Target time values. If `None` is passed, no interpolation will be performed.
         time_ref:
             The reference timestamp
 
@@ -687,17 +738,12 @@ class LocalCoordinateSystem(TimeDependent):
                 "allowed. Also check that the reference time has the correct type."
             )
 
-        orientation = ut.xr_interp_orientation_in_time(self.orientation, time)
+        orientation = self._interp_time_orientation(time)
+        coordinates = self._interp_time_coordinates(time)
 
-        if isinstance(self.coordinates, TimeSeries):
-            time_interp = Time(time, self.reference_time)
-            coordinates = self._coords_from_discrete_time_series(
-                self.coordinates.interp_time(time_interp)
-            )
-            if self.has_reference_time:
-                coordinates.weldx.time_ref = self.reference_time
-        else:
-            coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time)
+        # remove time if orientations and coordinates are single values (static)
+        if orientation.ndim == 2 and coordinates.ndim == 1:
+            time = None
 
         return LocalCoordinateSystem(orientation, coordinates, time)
 


Test patch:
diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py
index 2aa8d13..55806b9 100644
--- a/weldx/tests/test_transformations.py
+++ b/weldx/tests/test_transformations.py
@@ -3,7 +3,7 @@
 import math
 import random
 from copy import deepcopy
-from typing import Any, Dict, Iterable, List, Union
+from typing import Any, Dict, List, Union
 
 import numpy as np
 import pandas as pd
@@ -851,17 +851,17 @@ class TestLocalCoordinateSystem:
         [
             (  # broadcast left
                 TS("2020-02-10"),
-                TDI([1, 2], "D"),
+                TDI([1, 2, 14], "D"),
                 TS("2020-02-10"),
-                r_mat_z([0, 0]),
-                np.array([[2, 8, 7], [2, 8, 7]]),
+                r_mat_z([0, 0, 0.5]),
+                np.array([[2, 8, 7], [2, 8, 7], [4, 9, 2]]),
             ),
             (  # broadcast right
                 TS("2020-02-10"),
-                TDI([29, 30], "D"),
+                TDI([14, 29, 30], "D"),
                 TS("2020-02-10"),
-                r_mat_z([0.5, 0.5]),
-                np.array([[3, 1, 2], [3, 1, 2]]),
+                r_mat_z([0.5, 0.5, 0.5]),
+                np.array([[4, 9, 2], [3, 1, 2], [3, 1, 2]]),
             ),
             (  # pure interpolation
                 TS("2020-02-10"),
@@ -934,6 +934,93 @@ class TestLocalCoordinateSystem:
             lcs_interp_like, orientation_exp, coordinates_exp, True, time, time_ref
         )
 
+    # test_interp_time_discrete_outside_value_range ------------------------------------
+
+    @staticmethod
+    @pytest.mark.parametrize("time_dep_coords", [True, False])
+    @pytest.mark.parametrize("time_dep_orient", [True, False])
+    @pytest.mark.parametrize("all_less", [True, False])
+    def test_issue_289_interp_outside_time_range(
+        time_dep_orient: bool, time_dep_coords: bool, all_less: bool
+    ):
+        """Test if ``interp_time`` if all interp. values are outside the value range.
+
+        In this case it should always return a static system.
+
+        Parameters
+        ----------
+        time_dep_orient :
+            If `True`, the orientation is time dependent
+        time_dep_coords :
+            If `True`, the coordinates are time dependent
+        all_less :
+            If `True`, all interpolation values are less than the time values of the
+            LCS. Otherwise, all values are greater.
+
+        """
+        angles = [45, 135] if time_dep_orient else 135
+        orientation = WXRotation.from_euler("x", angles, degrees=True).as_matrix()
+        coordinates = [[0, 0, 0], [1, 1, 1]] if time_dep_coords else [1, 1, 1]
+        if time_dep_coords or time_dep_orient:
+            time = ["5s", "6s"] if all_less else ["0s", "1s"]
+        else:
+            time = None
+
+        lcs = LCS(orientation, coordinates, time)
+        lcs_interp = lcs.interp_time(["2s", "3s", "4s"])
+
+        exp_angle = 45 if time_dep_orient and all_less else 135
+        exp_orient = WXRotation.from_euler("x", exp_angle, degrees=True).as_matrix()
+        exp_coords = [0, 0, 0] if time_dep_coords and all_less else [1, 1, 1]
+
+        assert lcs_interp.time is None
+        assert lcs_interp.coordinates.values.shape == (3,)
+        assert lcs_interp.orientation.values.shape == (3, 3)
+        assert np.all(lcs_interp.coordinates.data == exp_coords)
+        assert np.all(lcs_interp.orientation.data == exp_orient)
+
+    # test_interp_time_discrete_single_time --------------------------------------------
+
+    @staticmethod
+    def test_interp_time_discrete_single_time():
+        """Test that single value interpolation results in a static system."""
+        orientation = WXRotation.from_euler("x", [45, 135], degrees=True).as_matrix()
+        coordinates = [[0, 0, 0], [2, 2, 2]]
+        time = ["1s", "3s"]
+        lcs = LCS(orientation, coordinates, time)
+
+        exp_coords = [1, 1, 1]
+        exp_orient = WXRotation.from_euler("x", 90, degrees=True).as_matrix()
+
+        lcs_interp = lcs.interp_time("2s")
+        assert lcs_interp.time is None
+        assert lcs_interp.coordinates.values.shape == (3,)
+        assert lcs_interp.orientation.values.shape == (3, 3)
+        assert np.all(lcs_interp.coordinates.data == exp_coords)
+        assert np.allclose(lcs_interp.orientation.data, exp_orient)
+
+    # test_interp_time_discrete_outside_value_range_both_sides -------------------------
+
+    @staticmethod
+    def test_interp_time_discrete_outside_value_range_both_sides():
+        """Test the interpolation is all values are outside of the LCS time range.
+
+        In this special case there is an overlap of the time ranges and we need to
+        ensure that the algorithm does not create a static system as it should if there
+        is no overlap.
+
+        """
+        orientation = WXRotation.from_euler("x", [45, 135], degrees=True).as_matrix()
+        coordinates = [[0, 0, 0], [2, 2, 2]]
+        time = ["2s", "3s"]
+        lcs = LCS(orientation, coordinates, time)
+
+        lcs_interp = lcs.interp_time(["1s", "4s"])
+
+        assert np.all(lcs_interp.time == ["1s", "4s"])
+        assert np.all(lcs_interp.coordinates.data == lcs.coordinates.data)
+        assert np.allclose(lcs_interp.orientation.data, lcs.orientation.data)
+
     # test_interp_time_timeseries_as_coords --------------------------------------------
 
     @staticmethod
@@ -2994,7 +3081,7 @@ class TestCoordinateSystemManager:
                 ["2000-03-08", "2000-03-04", "2000-03-10", "2000-03-16"],
                 r_mat_x([0]),
                 [[1, 0, 0]],
-                ([20], "2000-03-08"),
+                (None, None),
                 False,
             ),
             # get transformed cs at specific times using a DatetimeIndex - all systems,
@@ -3047,7 +3134,7 @@ class TestCoordinateSystemManager:
                 ["2000-03-08", "2000-03-04", "2000-03-10", "2000-03-16"],
                 r_mat_x([0]),
                 [[1, 0, 0]],
-                ([-4], "2000-03-08"),
+                (None, None),
                 False,
             ),
             # get transformed cs at specific times using a DatetimeIndex - all systems
@@ -4069,15 +4156,17 @@ class TestCoordinateSystemManager:
             angles = np.clip(val, clip_min, clip_max)
         else:
             angles = val
+        if len(angles) == 1:
+            angles = angles[0]
         return WXRotation.from_euler("z", angles, degrees=True).as_matrix()
 
     @staticmethod
     def _coordinates_from_value(val, clip_min=None, clip_max=None):
         if clip_min is not None and clip_max is not None:
             val = np.clip(val, clip_min, clip_max)
-        if not isinstance(val, Iterable):
-            val = [val]
-        return [[v, 2 * v, -v] for v in val]
+        if len(val) > 1:
+            return [[v, 2 * v, -v] for v in val]
+        return [val[0], 2 * val[0], -val[0]]
 
     @pytest.mark.parametrize(
         "time, time_ref, systems, csm_has_time_ref, num_abs_systems",
@@ -4159,6 +4248,7 @@ class TestCoordinateSystemManager:
         csm_interp = csm.interp_time(time, time_ref, systems)
 
         # evaluate results
+        time_exp = time_class if len(time_class) > 1 else None
         time_ref_exp = time_class.reference_time
         for k, v in lcs_data.items():
             # create expected lcs
@@ -4174,7 +4264,7 @@ class TestCoordinateSystemManager:
                 lcs_exp = tf.LocalCoordinateSystem(
                     self._orientation_from_value(days_interp + diff, v[1][0], v[1][-1]),
                     self._coordinates_from_value(days_interp + diff, v[1][0], v[1][-1]),
-                    time_class,
+                    time_exp,
                     csm.reference_time if csm.has_reference_time else time_ref_exp,
                 )
             else:
@@ -4189,7 +4279,58 @@ class TestCoordinateSystemManager:
 
         # check time union
         if systems is None or len(systems) == 3:
-            assert np.all(csm_interp.time_union() == time_class.as_pandas())
+            assert np.all(csm_interp.time_union() == time_exp)
+
+    # issue 289 ------------------------------------------------------------------------
+
+    @staticmethod
+    @pytest.mark.parametrize("time_dep_coords", [True, False])
+    @pytest.mark.parametrize("time_dep_orient", [True, False])
+    @pytest.mark.parametrize("all_less", [True, False])
+    def test_issue_289_interp_outside_time_range(
+        time_dep_orient: bool, time_dep_coords: bool, all_less: bool
+    ):
+        """Test if ``get_cs`` behaves as described in pull request #289.
+
+        The requirement is that a static system is returned when all time values of the
+        interpolation are outside of the value range of the involved coordinate systems.
+
+        Parameters
+        ----------
+        time_dep_orient :
+            If `True`, the orientation is time dependent
+        time_dep_coords :
+            If `True`, the coordinates are time dependent
+        all_less :
+            If `True`, all interpolation values are less than the time values of the
+            LCS. Otherwise, all values are greater.
+
+        """
+        angles = [45, 135] if time_dep_orient else 135
+        orientation = WXRotation.from_euler("x", angles, degrees=True).as_matrix()
+        coordinates = [[0, 0, 0], [1, 1, 1]] if time_dep_coords else [1, 1, 1]
+        if time_dep_coords or time_dep_orient:
+            time = ["5s", "6s"] if all_less else ["0s", "1s"]
+        else:
+            time = None
+
+        csm = CSM("R")
+        # add A as time dependent in base
+        csm.create_cs("A", "R", orientation, coordinates, time)
+        # add B as static in A
+        csm.create_cs("B", "A")
+
+        cs_br = csm.get_cs("B", "R", time=["2s", "3s", "4s"])
+
+        exp_angle = 45 if time_dep_orient and all_less else 135
+        exp_orient = WXRotation.from_euler("x", exp_angle, degrees=True).as_matrix()
+        exp_coords = [0, 0, 0] if time_dep_coords and all_less else [1, 1, 1]
+
+        assert cs_br.time is None
+        assert cs_br.coordinates.values.shape == (3,)
+        assert cs_br.orientation.values.shape == (3, 3)
+        assert np.all(cs_br.coordinates.data == exp_coords)
+        assert np.all(cs_br.orientation.data == exp_orient)
 
 
 def test_relabel():