diff --git a/CHANGELOG.md b/CHANGELOG.md index 9508e911b..799890cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ [[#489]](https://github.com/BAMWelDX/weldx/pull/489) - move xarray related utility functions into `weldx.util.xarray` and all other ones into `weldx.util.util`. Content from both submodules can still be accessed using `weldx.util` [[#490]](https://github.com/BAMWelDX/weldx/pull/490) +- xarray implementations for the `LocalCoordinateSystem` now operate on time as a dimension instead of + coordinates [[#486]](https://github.com/BAMWelDX/weldx/pull/486) ### fixes @@ -54,6 +56,8 @@ - added installation guide with complete environment setup (Jupyterlab with extensions) and possible problems and solutions [[#450]](https://github.com/BAMWelDX/weldx/pull/450) +- split API documentation into user classes/functions and a full API reference + [[#469]](https://github.com/BAMWelDX/weldx/pull/469). ### ASDF diff --git a/tutorials/measurement_example.ipynb b/tutorials/measurement_example.ipynb index ef596c069..0ae66d5fb 100644 --- a/tutorials/measurement_example.ipynb +++ b/tutorials/measurement_example.ipynb @@ -15,13 +15,11 @@ "source": [ "import numpy as np\n", "import pandas as pd\n", - "import sympy\n", "\n", "import weldx\n", "import weldx.measurement as msm\n", "import weldx.transformations as tf\n", "from weldx import Q_\n", - "from weldx import util\n", "from weldx.welding.util import sine" ] }, @@ -40,7 +38,7 @@ "## Generating the measurement data\n", "We start by creating some \"dummy\" datasets that represent the current and voltage measurements.\n", "In a real application, these would be the datasets that we would copy from our measurement equipment (e.g. downloaded form a HKS-WeldQAS, oscilloscope or similar systems).\n", - "The values in these dataset represent the actual physical current and voltage data in A and V." + "The values in this dataset represent the actual physical current and voltage data in A and V." ] }, { @@ -94,7 +92,7 @@ "metadata": {}, "source": [ "## Equipment and Software\n", - "Next, let's define some of the equipment and software that is used throughout the measurement chain. \n", + "Next, let's define some equipment and software that is used throughout the measurement chain.\n", "We will use and add more information to these objects later.\n", "In out example, two types of hardware equipment are used:\n", "\n", @@ -474,7 +472,7 @@ "metadata": {}, "source": [ "## Writing to ASDF\n", - "Once we have define all object we can write them to a ASDF file. To make the file easier to read we place some elements earlier in the tree." + "Once we have defined all object we can write them to an ASDF file. To make the file easier to read we place some elements earlier in the tree." ] }, { @@ -538,4 +536,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/weldx/asdf/extension.py b/weldx/asdf/extension.py index b54d3332d..8c95239a7 100644 --- a/weldx/asdf/extension.py +++ b/weldx/asdf/extension.py @@ -1,3 +1,4 @@ +"""ASDF-extensions for weldx types.""" from typing import List from asdf.extension import ManifestExtension diff --git a/weldx/asdf/file.py b/weldx/asdf/file.py index baece3d3e..7cf492fc8 100644 --- a/weldx/asdf/file.py +++ b/weldx/asdf/file.py @@ -339,10 +339,10 @@ def add_history_entry(self, change_desc: str, software: dict = None) -> None: software : Optional software used to make the change. - See Also + Notes -------- The software entry will be inferred from the constructor or, if not defined, - from `software_history_entry`. + from ``software_history_entry``. """ if software is None: diff --git a/weldx/asdf/validators.py b/weldx/asdf/validators.py index ddc5bc723..cb93c89ff 100644 --- a/weldx/asdf/validators.py +++ b/weldx/asdf/validators.py @@ -1,3 +1,4 @@ +"""ASDF-validators for weldx types.""" import re from typing import Any, Callable, Dict, Iterator, List, Mapping, OrderedDict @@ -7,7 +8,6 @@ from weldx.constants import Q_ from weldx.constants import WELDX_UNIT_REGISTRY as UREG - from .types import WxSyntaxError from .util import _get_instance_shape diff --git a/weldx/tags/core/transformations/local_coordinate_system.py b/weldx/tags/core/transformations/local_coordinate_system.py index dc92bfac0..3f36676a4 100644 --- a/weldx/tags/core/transformations/local_coordinate_system.py +++ b/weldx/tags/core/transformations/local_coordinate_system.py @@ -40,7 +40,7 @@ def to_yaml_tree(self, obj: LocalCoordinateSystem, tag: str, ctx) -> dict: # ctx.set_array_storage(coordinates.data, "inline") tree["coordinates"] = coordinates - if "time" in obj.dataset.coords: + if "time" in obj.dataset.dims: tree["time"] = obj.time.as_timedelta_index() if obj.reference_time is not None: diff --git a/weldx/tests/transformations/test_cs_manager.py b/weldx/tests/transformations/test_cs_manager.py index f57986cd7..4f6338432 100644 --- a/weldx/tests/transformations/test_cs_manager.py +++ b/weldx/tests/transformations/test_cs_manager.py @@ -1357,9 +1357,7 @@ def test_get_local_coordinate_system_time_dep( """ # setup ------------------------------------------- # set reference times - for i, _ in enumerate(time_refs): - if time_refs[i] is not None: - time_refs[i] = pd.Timestamp(time_refs[i]) + time_refs = [t if t is None else pd.Timestamp(t) for t in time_refs] # moves in positive x-direction time_1 = TDI([0, 3, 12], "D") diff --git a/weldx/tests/transformations/test_local_cs.py b/weldx/tests/transformations/test_local_cs.py index 94a7002e4..3dd43eb02 100644 --- a/weldx/tests/transformations/test_local_cs.py +++ b/weldx/tests/transformations/test_local_cs.py @@ -1426,10 +1426,18 @@ def test_coordinate_system_time_interpolation(): ) # test xr_interp_orientation_in_time for single time point interpolation - orientation = ut.xr_interp_orientation_in_time( - lcs.orientation.isel({"time": [1]}), time_0 - ) - assert np.allclose(orientation, orientation[1, :, :]) + for i, _ in enumerate(time_0): + # test for scalar value as coordinate + orientation_interp = ut.xr_interp_orientation_in_time( + lcs.orientation.isel({"time": i}), time_0 + ) + assert np.allclose(orientation_interp, lcs.orientation[i]) + + # test for scalar value as dimension + orientation_interp = ut.xr_interp_orientation_in_time( + lcs.orientation.isel({"time": [i]}), time_0 + ) + assert np.allclose(orientation_interp, lcs.orientation[i]) # exceptions -------------------------------- # wrong parameter type diff --git a/weldx/transformations/cs_manager.py b/weldx/transformations/cs_manager.py index b92ae9a63..c47ea0a3a 100644 --- a/weldx/transformations/cs_manager.py +++ b/weldx/transformations/cs_manager.py @@ -1089,9 +1089,9 @@ def _get_cs_on_edge( ): """Get a lcs on a graph edge for the get_cs method.""" invert = False - lcs = self.graph.edges[edge]["lcs"] + lcs: LocalCoordinateSystem = self.graph.edges[edge]["lcs"] - # lcs has an expression as coordinates + # lcs was defined inverted if lcs is None: lcs = self.graph.edges[(edge[1], edge[0])]["lcs"] invert = True diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py index d59eb2cf8..477061981 100644 --- a/weldx/transformations/local_cs.py +++ b/weldx/transformations/local_cs.py @@ -78,10 +78,8 @@ def __init__( # warn about dropped time data if ( time is not None - and "time" not in orientation.coords - and ( - isinstance(coordinates, TimeSeries) or "time" not in coordinates.coords - ) + and "time" not in orientation.dims + and (isinstance(coordinates, TimeSeries) or "time" not in coordinates.dims) ): warnings.warn( "Provided time is dropped because of the given coordinates and " @@ -382,8 +380,8 @@ def _unify_time_axis( """Unify time axis of orientation and coordinates if both are DataArrays.""" if ( not isinstance(coordinates, TimeSeries) - and ("time" in orientation.coords) - and ("time" in coordinates.coords) + and ("time" in orientation.dims) + and ("time" in coordinates.dims) and (not np.all(orientation.time.data == coordinates.time.data)) ): time_union = Time.union([orientation.time, coordinates.time]) @@ -595,7 +593,7 @@ def time(self) -> Union[Time, None]: Time-like data array representing the time union of the LCS """ - if "time" in self._dataset.coords: + if "time" in self._dataset.dims: return Time(self._dataset.time, self.reference_time) return None @@ -664,22 +662,18 @@ 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]: + def _interp_time_orientation(self, time: Time) -> xr.DataArray: """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]: + if "time" not in self.orientation.dims: # don't interpolate static + return self.orientation + if time.max() <= self.time.min(): # only use edge timestamp + return self.orientation.values[0] + if time.min() >= self.time.max(): # only use edge timestamp + return self.orientation.values[-1] + # full interpolation with overlapping times + return ut.xr_interp_orientation_in_time(self.orientation, time) + + def _interp_time_coordinates(self, time: Time) -> xr.DataArray: """Interpolate the coordinates in time.""" if isinstance(self.coordinates, TimeSeries): time_interp = Time(time, self.reference_time) @@ -688,18 +682,15 @@ 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: # 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 + return coordinates + if "time" not in self.coordinates.dims: # don't interpolate static + return self.coordinates + if time.max() <= self.time.min(): # only use edge timestamp + return self.coordinates[0] + if time.min() >= self.time.max(): # only use edge timestamp + return self.coordinates[-1] + # full interpolation with overlapping times + return ut.xr_interp_coordinates_in_time(self.coordinates, time) def interp_time( self, diff --git a/weldx/util/util.py b/weldx/util/util.py index 868adfcbf..422a8c96b 100644 --- a/weldx/util/util.py +++ b/weldx/util/util.py @@ -177,7 +177,9 @@ def inherit_docstrings(cls): func.__doc__ = getattr(parent, name).__doc__ if not func.__doc__: warnings.warn( - f"could not derive docstring for {cls}.{name}", stacklevel=2 + f"could not derive docstring for {cls}.{name}", + stacklevel=2, + category=ImportWarning, ) return cls diff --git a/weldx/util/xarray.py b/weldx/util/xarray.py index 46ebb519c..ed0bbc7f3 100644 --- a/weldx/util/xarray.py +++ b/weldx/util/xarray.py @@ -174,6 +174,8 @@ def xr_is_orthogonal_matrix(da: xr.DataArray, dims: List[str]) -> bool: True if all matrices are orthogonal. """ + if not set(dims).issubset(set(da.dims)): + raise ValueError(f"Could not find {dims=} in DataArray.") eye = np.eye(len(da.coords[dims[0]]), len(da.coords[dims[1]])) return np.allclose(xr_matmul(da, da, dims, trans_b=True), eye) @@ -556,15 +558,15 @@ def xr_3d_matrix(data: np.ndarray, time: Time = None) -> xr.DataArray: def xr_interp_orientation_in_time( - dsx: xr.DataArray, times: types_time_like + da: xr.DataArray, time: types_time_like ) -> xr.DataArray: """Interpolate an xarray DataArray that represents orientation data in time. Parameters ---------- - dsx : + da : xarray DataArray containing the orientation as matrix - times : + time : Time data Returns @@ -573,41 +575,43 @@ def xr_interp_orientation_in_time( Interpolated data """ - if "time" not in dsx.coords: - return dsx + if "time" not in da.dims: + return da + if len(da.time) == 1: # remove "time dimension" for static case + return da.isel({"time": 0}) - times = Time(times).as_pandas_index() - times_ds = Time(dsx).as_pandas_index() - time_ref = dsx.weldx.time_ref - - if len(times_ds) > 1: - # extract intersecting times and add time range boundaries of the data set - times_ds_limits = pd.Index([times_ds.min(), times_ds.max()]) - times_union = times.union(times_ds_limits) - times_intersect = times_union[ - (times_union >= times_ds_limits[0]) & (times_union <= times_ds_limits[1]) - ] - - # interpolate rotations in the intersecting time range - rotations_key = Rot.from_matrix(dsx.transpose(..., "c", "v").data) - times_key = times_ds.view(np.int64) - rotations_interp = Slerp(times_key, rotations_key)( - times_intersect.view(np.int64) - ) - dsx_out = xr_3d_matrix(rotations_interp.as_matrix(), times_intersect) - else: - # TODO: this case is not really well defined, maybe avoid? - dsx_out = dsx + time = Time(time).as_pandas_index() + time_da = Time(da).as_pandas_index() + time_ref = da.weldx.time_ref + + if not len(time_da) > 1: + raise ValueError("Invalid time format for interpolation.") + + # extract intersecting times and add time range boundaries of the data set + times_ds_limits = pd.Index([time_da.min(), time_da.max()]) + times_union = time.union(times_ds_limits) + times_intersect = times_union[ + (times_union >= times_ds_limits[0]) & (times_union <= times_ds_limits[1]) + ] + + # interpolate rotations in the intersecting time range + rotations_key = Rot.from_matrix(da.transpose(..., "time", "c", "v").data) + times_key = time_da.view(np.int64) + rotations_interp = Slerp(times_key, rotations_key)(times_intersect.view(np.int64)) + da = xr_3d_matrix(rotations_interp.as_matrix(), times_intersect) # use interp_like to select original time values and correctly fill time dimension - dsx_out = xr_interp_like(dsx_out, {"time": times}, fillna=True) + da = xr_interp_like(da, {"time": time}, fillna=True) # resync and reset to correct format if time_ref: - dsx_out.weldx.time_ref = time_ref - dsx_out = dsx_out.weldx.time_ref_restore() + da.weldx.time_ref = time_ref + da = da.weldx.time_ref_restore().transpose(..., "time", "c", "v") + + if len(da.time) == 1: # remove "time dimension" for static case + return da.isel({"time": 0}) - return dsx_out.transpose(..., "c", "v") + return da def xr_interp_coordinates_in_time( @@ -628,12 +632,19 @@ def xr_interp_coordinates_in_time( Interpolated data """ + if "time" not in da.dims: + return da + times = Time(times).as_pandas_index() da = da.weldx.time_ref_unset() da = xr_interp_like( da, {"time": times}, assume_sorted=True, broadcast_missing=False, fillna=True ) da = da.weldx.time_ref_restore() + + if len(da.time) == 1: # remove "time dimension" for static cases + return da.isel({"time": 0}) + return da @@ -678,7 +689,7 @@ def time_ref_unset(self) -> xr.DataArray: def time_ref_restore(self) -> xr.DataArray: """Convert DatetimeIndex back to TimedeltaIndex + reference Timestamp.""" da = self._obj.copy() - if "time" not in da.coords: + if "time" not in da.dims: return da if is_datetime64_dtype(da.time): @@ -701,7 +712,7 @@ def reset_reference_time(self, time_ref_new: pd.Timestamp) -> xr.DataArray: def time_ref(self) -> Union[pd.Timestamp, None]: """Get the time_ref value or `None` if not set.""" da = self._obj - if "time" in da.coords and "time_ref" in da.time.attrs: + if "time" in da.dims and "time_ref" in da.time.attrs: return da.time.attrs["time_ref"] return None diff --git a/weldx/visualization/matplotlib_impl.py b/weldx/visualization/matplotlib_impl.py index 040f9434a..473a281bd 100644 --- a/weldx/visualization/matplotlib_impl.py +++ b/weldx/visualization/matplotlib_impl.py @@ -127,7 +127,7 @@ def draw_coordinate_system_matplotlib( """ if not (show_vectors or show_origin): return - if "time" in coordinate_system.dataset.coords: + if "time" in coordinate_system.dataset.dims: if time_idx is None: time_idx = 0 if isinstance(time_idx, int):