Skip to content

Commit

Permalink
Refactor interpolation (#486)
Browse files Browse the repository at this point in the history
* add user API section

* fix import and grammar

* handle user api doc inside python package

* fix See Also

* refactor interpolation to consider time in xarray dimensions instead of coordinates

* skip pytest and lint without code changes

* skip pytest for win and osx unless master or PR

* always run on action changes

* dummy comment

* test pytest exclude

* trigger?

* filter?

* actions filter

* skip pip install and fix ci trigger

* update pytest filter

* skip build CI for doc or changelog changes

* exclude full pytest matrix on draft PR

* try with default

* remove defaults

* include PR state changes

* Update test_transformations.py

* added toctree captions for tutorials

* index: skip first 8 lines of readme.me and set caption in rst.

* moved api doc string from rst to py and extended it

* added module docstrings

* fix headings

* amend changelog

* emit ImportWarning if doc str could not be derived.

* use same template in autosummary

* fix docstring of Q_

* use class template where appropriate.

* better heading

* check for time in dims

* update CHANGELOG.md

* update test after merge

* fix merge

* fix orth check

Co-authored-by: Martin K. Scherer <m.scherer@fu-berlin.de>
Co-authored-by: Martin K. Scherer <marscher@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 24, 2021
1 parent cd7f2f3 commit 2d7dba5
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 88 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
10 changes: 4 additions & 6 deletions tutorials/measurement_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
Expand All @@ -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."
]
},
{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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."
]
},
{
Expand Down Expand Up @@ -538,4 +536,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
}
}
1 change: 1 addition & 0 deletions weldx/asdf/extension.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""ASDF-extensions for weldx types."""
from typing import List

from asdf.extension import ManifestExtension
Expand Down
4 changes: 2 additions & 2 deletions weldx/asdf/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion weldx/asdf/validators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""ASDF-validators for weldx types."""
import re
from typing import Any, Callable, Dict, Iterator, List, Mapping, OrderedDict

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion weldx/tags/core/transformations/local_coordinate_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 1 addition & 3 deletions weldx/tests/transformations/test_cs_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 12 additions & 4 deletions weldx/tests/transformations/test_local_cs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions weldx/transformations/cs_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 25 additions & 34 deletions weldx/transformations/local_cs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion weldx/util/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 44 additions & 33 deletions weldx/util/xarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
Loading

0 comments on commit 2d7dba5

Please sign in to comment.