From 7c35f02604237616bdf43855cc18b8ca5770780b Mon Sep 17 00:00:00 2001 From: Cagtay Fabry <43667554+CagtayFabry@users.noreply.github.com> Date: Sat, 14 Aug 2021 21:38:34 +0200 Subject: [PATCH 01/13] add initial implementation --- weldx/transformations/cs_manager.py | 12 ++++++++++++ weldx/transformations/local_cs.py | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py index cc2a01300..a662ad948 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -1291,6 +1291,18 @@ def get_cs( if time is not None: time = Time(time, time_ref) + if all( + [not self.graph.edges[edge]["lcs"].has_timeseries for edge in path_edges] + ): + times = [self.graph.edges[edge]["lcs"].time for edge in path_edges] + times = [t for t in times if t is not None] + if times: + path_union = Time.union(times) + if np.all(time.index <= path_union.min()): + time = path_union.min() + elif np.all(time.index >= path_union.max()): + time = path_union.max() + # calculate result lcs if len(path_edges) == 1: return self._get_cs_on_edge(path_edges[0], time, time_ref, time_ref is None) diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index bee81576e..cd4d741f7 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -547,6 +547,11 @@ def is_time_dependent(self) -> bool: """ return self.time is not None or self._coord_ts is not None + @property + def has_timeseries(self) -> bool: + """Return `True` if the coordinate system has a `TimeSeries` component.""" + return self._coord_ts is not None + @property def has_reference_time(self) -> bool: """Return `True` if the coordinate system has a reference time. From c7902efbb359bb3ceae377da5e6201340f754cd4 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 16 Aug 2021 15:50:25 +0200 Subject: [PATCH 02/13] Implement first draft and test --- weldx/tests/test_transformations.py | 55 +++++++++++++++++++++++++---- weldx/transformations/cs_manager.py | 22 ++++++------ weldx/transformations/local_cs.py | 21 +++++++++-- 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index 2aa8d1369..aecf56a7b 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -851,17 +851,17 @@ def test_reset_reference_time_exceptions( [ ( # broadcast left TS("2020-02-10"), - TDI([1, 2], "D"), + TDI([1, 2, 10], "D"), TS("2020-02-10"), - r_mat_z([0, 0]), - np.array([[2, 8, 7], [2, 8, 7]]), + r_mat_z([0, 0, 0]), + np.array([[2, 8, 7], [2, 8, 7], [2, 8, 7]]), ), ( # broadcast right TS("2020-02-10"), - TDI([29, 30], "D"), + TDI([22, 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([[3, 1, 2], [3, 1, 2], [3, 1, 2]]), ), ( # pure interpolation TS("2020-02-10"), @@ -4191,6 +4191,49 @@ def test_interp_time( if systems is None or len(systems) == 3: assert np.all(csm_interp.time_union() == time_class.as_pandas()) + # 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, time_dep_coords, all_less + ): + 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") + + # 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) + + # this is time dependent even though the result is known to be static + print(csm.get_cs("B", "R", time=Q_([2, 3, 4], "s")).time) + + print(csm.get_cs("B", "R", time=Q_([2, 3, 4], "s")).coordinates.data) + + 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(): """Test relabeling unmerged and merged CSM nodes. diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py index a662ad948..e20748ed1 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -1291,17 +1291,17 @@ def get_cs( if time is not None: time = Time(time, time_ref) - if all( - [not self.graph.edges[edge]["lcs"].has_timeseries for edge in path_edges] - ): - times = [self.graph.edges[edge]["lcs"].time for edge in path_edges] - times = [t for t in times if t is not None] - if times: - path_union = Time.union(times) - if np.all(time.index <= path_union.min()): - time = path_union.min() - elif np.all(time.index >= path_union.max()): - time = path_union.max() + # if all( + # [not self.graph.edges[edge]["lcs"].has_timeseries for edge in path_edges] + # ): + # times = [self.graph.edges[edge]["lcs"].time for edge in path_edges] + # times = [t for t in times if t is not None] + # if times: + # path_union = Time.union(times) + # if np.all(time.index <= path_union.min()): + # time = path_union.min() + # elif np.all(time.index >= path_union.max()): + # time = path_union.max() # calculate result lcs if len(path_edges) == 1: diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index cd4d741f7..0bbe8a97a 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -692,7 +692,14 @@ def interp_time( "allowed. Also check that the reference time has the correct type." ) - orientation = ut.xr_interp_orientation_in_time(self.orientation, time) + if self.orientation.ndim == 2: + orientation = self.orientation + elif time.max() < self.time.min(): + orientation = self.orientation.values[0] + elif time.min() > self.time.max(): + orientation = self.orientation.values[-1] + else: + orientation = ut.xr_interp_orientation_in_time(self.orientation, time) if isinstance(self.coordinates, TimeSeries): time_interp = Time(time, self.reference_time) @@ -702,7 +709,17 @@ def interp_time( if self.has_reference_time: coordinates.weldx.time_ref = self.reference_time else: - coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) + if self.coordinates.ndim == 1: + coordinates = self.coordinates + elif time.max() < self.time.min(): + coordinates = self.coordinates.values[0] + elif time.min() > self.time.max(): + coordinates = self.coordinates.values[-1] + else: + coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) + + if orientation.ndim == 2 and coordinates.ndim == 1: + time = None return LocalCoordinateSystem(orientation, coordinates, time) From a4cc84c168ae237daa0156f5d2c811b642d60283 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 16 Aug 2021 17:03:08 +0200 Subject: [PATCH 03/13] Changed behavior for boundary intersections --- weldx/tests/test_transformations.py | 25 ++++++++++++++----------- weldx/transformations/local_cs.py | 21 +++++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index aecf56a7b..dfd6699ec 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -851,17 +851,17 @@ def test_reset_reference_time_exceptions( [ ( # broadcast left TS("2020-02-10"), - TDI([1, 2, 10], "D"), + TDI([1, 2, 14], "D"), TS("2020-02-10"), - r_mat_z([0, 0, 0]), - np.array([[2, 8, 7], [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([22, 29, 30], "D"), + TDI([14, 29, 30], "D"), TS("2020-02-10"), r_mat_z([0.5, 0.5, 0.5]), - np.array([[3, 1, 2], [3, 1, 2], [3, 1, 2]]), + np.array([[4, 9, 2], [3, 1, 2], [3, 1, 2]]), ), ( # pure interpolation TS("2020-02-10"), @@ -2994,7 +2994,7 @@ def test_get_local_coordinate_system_no_time_dep( ["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 +3047,7 @@ def test_get_local_coordinate_system_no_time_dep( ["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 +4069,17 @@ def _orientation_from_value(val, clip_min=None, clip_max=None): 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 +4161,7 @@ def test_interp_time( 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 +4177,7 @@ def test_interp_time( 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: diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index 0bbe8a97a..ecbbf3ed3 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -692,15 +692,17 @@ def interp_time( "allowed. Also check that the reference time has the correct type." ) + # calculate orientations if self.orientation.ndim == 2: orientation = self.orientation - elif time.max() < self.time.min(): + elif time.max() <= self.time.min(): orientation = self.orientation.values[0] - elif time.min() > self.time.max(): + elif time.min() >= self.time.max(): orientation = self.orientation.values[-1] else: orientation = ut.xr_interp_orientation_in_time(self.orientation, time) + # calculate coordinates if isinstance(self.coordinates, TimeSeries): time_interp = Time(time, self.reference_time) coordinates = self._coords_from_discrete_time_series( @@ -708,15 +710,14 @@ def interp_time( ) if self.has_reference_time: coordinates.weldx.time_ref = self.reference_time + elif self.coordinates.ndim == 1: + coordinates = self.coordinates + elif time.max() <= self.time.min(): + coordinates = self.coordinates.values[0] + elif time.min() >= self.time.max(): + coordinates = self.coordinates.values[-1] else: - if self.coordinates.ndim == 1: - coordinates = self.coordinates - elif time.max() < self.time.min(): - coordinates = self.coordinates.values[0] - elif time.min() > self.time.max(): - coordinates = self.coordinates.values[-1] - else: - coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) + coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) if orientation.ndim == 2 and coordinates.ndim == 1: time = None From 995960df11125726301ea8446e4e2093f4d92c83 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 16 Aug 2021 17:09:03 +0200 Subject: [PATCH 04/13] Add docstring --- weldx/tests/test_transformations.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index dfd6699ec..3177c6e64 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -4203,6 +4203,12 @@ def test_interp_time( def test_issue_289_interp_outside_time_range( time_dep_orient, time_dep_coords, all_less ): + """Test if ``interp_time`` 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. + + """ 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] @@ -4217,14 +4223,6 @@ def test_issue_289_interp_outside_time_range( # add B as static in A csm.create_cs("B", "A") - # 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) - - # this is time dependent even though the result is known to be static - print(csm.get_cs("B", "R", time=Q_([2, 3, 4], "s")).time) - - print(csm.get_cs("B", "R", time=Q_([2, 3, 4], "s")).coordinates.data) - cs_br = csm.get_cs("B", "R", time=["2s", "3s", "4s"]) exp_angle = 45 if time_dep_orient and all_less else 135 From 324f912feecdcb0e640fd9c5c282aff8aec5d9dc Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 16 Aug 2021 17:11:16 +0200 Subject: [PATCH 05/13] Fix flake8 issue --- weldx/tests/test_transformations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index 3177c6e64..985c6930d 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 From 411945f6fa5571c82f4d2256844b917475c41998 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 16 Aug 2021 17:24:59 +0200 Subject: [PATCH 06/13] Add test for LCS and update test docstrings --- weldx/tests/test_transformations.py | 59 ++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index 985c6930d..a964c35cf 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -934,6 +934,51 @@ def test_interp_time_discrete( 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_timeseries_as_coords -------------------------------------------- @staticmethod @@ -4201,13 +4246,23 @@ def test_interp_time( @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, time_dep_coords, all_less + time_dep_orient: bool, time_dep_coords: bool, all_less: bool ): - """Test if ``interp_time`` behaves as described in pull request #289. + """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() From 76b95174e84f547d2179f1b5e51c637ee4d20ad7 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 09:06:01 +0200 Subject: [PATCH 07/13] Remove first draft --- weldx/transformations/cs_manager.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py index e20748ed1..cc2a01300 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -1291,18 +1291,6 @@ def get_cs( if time is not None: time = Time(time, time_ref) - # if all( - # [not self.graph.edges[edge]["lcs"].has_timeseries for edge in path_edges] - # ): - # times = [self.graph.edges[edge]["lcs"].time for edge in path_edges] - # times = [t for t in times if t is not None] - # if times: - # path_union = Time.union(times) - # if np.all(time.index <= path_union.min()): - # time = path_union.min() - # elif np.all(time.index >= path_union.max()): - # time = path_union.max() - # calculate result lcs if len(path_edges) == 1: return self._get_cs_on_edge(path_edges[0], time, time_ref, time_ref is None) From b1a096ba26033a352e6b012a44c7179a27fe5c6b Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 09:49:04 +0200 Subject: [PATCH 08/13] Update interp_time for single value interpolation --- weldx/tests/test_transformations.py | 21 ++++++++++++++++++++- weldx/transformations/local_cs.py | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index a964c35cf..8d335d791 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -979,6 +979,25 @@ def test_issue_289_interp_outside_time_range( 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(): + 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_timeseries_as_coords -------------------------------------------- @staticmethod @@ -4237,7 +4256,7 @@ def test_interp_time( # 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 ------------------------------------------------------------------------ diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index 07bb2fe4b..eae17e743 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -719,6 +719,10 @@ def interp_time( else: coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) + if len(orientation) == 1: + orientation = orientation[0].values + if len(coordinates) == 1: + coordinates = coordinates[0].values if orientation.ndim == 2 and coordinates.ndim == 1: time = None From ff2aa77d4cbc68fb4bc66309066d75137c1b9572 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 10:25:54 +0200 Subject: [PATCH 09/13] Move implementations to private methods and update docstrings --- weldx/tests/test_transformations.py | 23 +++++++++ weldx/transformations/local_cs.py | 80 +++++++++++++++++------------ 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/weldx/tests/test_transformations.py b/weldx/tests/test_transformations.py index 8d335d791..55806b9a9 100644 --- a/weldx/tests/test_transformations.py +++ b/weldx/tests/test_transformations.py @@ -983,6 +983,7 @@ def test_issue_289_interp_outside_time_range( @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"] @@ -998,6 +999,28 @@ def test_interp_time_discrete_single_time(): 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 diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index eae17e743..7e2671ab8 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -657,6 +657,43 @@ def as_rotation(self) -> Rot: # pragma: no cover """ 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: + orientation = self.orientation + elif time.max() <= self.time.min(): + orientation = self.orientation.values[0] + elif time.min() >= self.time.max(): + orientation = self.orientation.values[-1] + else: + orientation = ut.xr_interp_orientation_in_time(self.orientation, time) + + if len(orientation) == 1: + 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: + coordinates = self.coordinates + elif time.max() <= self.time.min(): + coordinates = self.coordinates.values[0] + elif time.min() >= self.time.max(): + coordinates = self.coordinates.values[-1] + else: + coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) + + if len(coordinates) == 1: + return coordinates[0].values + return coordinates + def interp_time( self, time: types_time_like, @@ -664,11 +701,18 @@ def interp_time( ) -> 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 @@ -692,37 +736,9 @@ def interp_time( "allowed. Also check that the reference time has the correct type." ) - # calculate orientations - if self.orientation.ndim == 2: - orientation = self.orientation - elif time.max() <= self.time.min(): - orientation = self.orientation.values[0] - elif time.min() >= self.time.max(): - orientation = self.orientation.values[-1] - else: - orientation = ut.xr_interp_orientation_in_time(self.orientation, time) + orientation = self._interp_time_orientation(time) + coordinates = self._interp_time_coordinates(time) - # calculate coordinates - 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: - coordinates = self.coordinates - elif time.max() <= self.time.min(): - coordinates = self.coordinates.values[0] - elif time.min() >= self.time.max(): - coordinates = self.coordinates.values[-1] - else: - coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) - - if len(orientation) == 1: - orientation = orientation[0].values - if len(coordinates) == 1: - coordinates = coordinates[0].values if orientation.ndim == 2 and coordinates.ndim == 1: time = None From aefca7a30126cc633e7a1edba6a2fe07c3b237fb Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 10:34:35 +0200 Subject: [PATCH 10/13] Add line comments --- weldx/transformations/local_cs.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index 7e2671ab8..0b4d24e62 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -659,16 +659,16 @@ def as_rotation(self) -> Rot: # pragma: no cover def _interp_time_orientation(self, time: Time) -> Union[xr.DataArray, np.ndarray]: """Interpolate the orientation in time.""" - if self.orientation.ndim == 2: + if self.orientation.ndim == 2: # don't interpolate static orientation = self.orientation - elif time.max() <= self.time.min(): + elif time.max() <= self.time.min(): # only use edge timestamp orientation = self.orientation.values[0] - elif time.min() >= self.time.max(): + elif time.min() >= self.time.max(): # only use edge timestamp orientation = self.orientation.values[-1] - else: + else: # full interpolation with overlapping times orientation = ut.xr_interp_orientation_in_time(self.orientation, time) - if len(orientation) == 1: + if len(orientation) == 1: # remove "time dimension" for single value cases return orientation[0].values return orientation @@ -681,16 +681,16 @@ def _interp_time_coordinates(self, time: Time) -> Union[xr.DataArray, np.ndarray ) if self.has_reference_time: coordinates.weldx.time_ref = self.reference_time - elif self.coordinates.ndim == 1: + elif self.coordinates.ndim == 1: # don't interpolate static coordinates = self.coordinates - elif time.max() <= self.time.min(): + elif time.max() <= self.time.min(): # only use edge timestamp coordinates = self.coordinates.values[0] - elif time.min() >= self.time.max(): + elif time.min() >= self.time.max(): # only use edge timestamp coordinates = self.coordinates.values[-1] - else: + else: # full interpolation with overlapping times coordinates = ut.xr_interp_coordinates_in_time(self.coordinates, time) - if len(coordinates) == 1: + if len(coordinates) == 1: # remove "time dimension" for single value cases return coordinates[0].values return coordinates @@ -739,6 +739,7 @@ def interp_time( orientation = self._interp_time_orientation(time) coordinates = self._interp_time_coordinates(time) + # remove time if orientations and coordinates are single values (static) if orientation.ndim == 2 and coordinates.ndim == 1: time = None From 8008393528fe49384445013770073e13716403d4 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 10:45:21 +0200 Subject: [PATCH 11/13] Update changelog and docstrings --- CHANGELOG.md | 4 ++++ weldx/transformations/cs_manager.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78fb11ace..3b0e374dc 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 cc2a01300..b92ae9a63 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -1132,7 +1132,10 @@ def get_cs( 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. From 5e9989605101efe7fd2c1922cf06f96272f6ae78 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 15:58:02 +0200 Subject: [PATCH 12/13] Update weldx/transformations/local_cs.py Co-authored-by: Cagtay Fabry <43667554+CagtayFabry@users.noreply.github.com> --- weldx/transformations/local_cs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index 0b4d24e62..61284ee6b 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -550,7 +550,7 @@ def is_time_dependent(self) -> bool: @property def has_timeseries(self) -> bool: """Return `True` if the coordinate system has a `TimeSeries` component.""" - return self._coord_ts is not None + return isinstance(self.coordinates, TimeSeries) or isinstance(self.orientation, TimeSeries) @property def has_reference_time(self) -> bool: From c271231c80cb62b07dff58b7a5544567636e1167 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Tue, 17 Aug 2021 16:42:54 +0200 Subject: [PATCH 13/13] Fix linter and doc failures --- weldx/transformations/local_cs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index 61284ee6b..a6d25d715 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -549,8 +549,10 @@ def is_time_dependent(self) -> bool: @property def has_timeseries(self) -> bool: - """Return `True` if the coordinate system has a `TimeSeries` component.""" - return isinstance(self.coordinates, TimeSeries) or isinstance(self.orientation, TimeSeries) + """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: