def _interp_time_orientation(self, time: Time) -> xr.DataArray: """Interpolate the orientation in time.""" 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.isel(time=0).data if time.min() >= self.time.max(): # only use edge timestamp return self.orientation.isel(time=-1).data # full interpolation with overlapping times return ut.xr_interp_orientation_in_time(self.orientation, time)
def test_quantity(arg, unit, expected): """Test conversion to pint.Quantity with different scales.""" t = Time(arg) q = Time(arg).as_quantity(unit) expected = Q_(expected, unit) assert np.allclose(q, expected) if t.is_absolute: assert t.reference_time == q.time_ref if unit == "s": assert np.all(q == t.quantity)
def test_time_classes(inputs, time_ref): data = write_read_buffer({"root": inputs}) assert np.all(data["root"] == inputs) if isinstance(inputs, pd.Index) and not inputs.is_monotonic: # this is not valid for the time class, hence cancel here return t1 = Time(inputs, time_ref) t2 = write_read_buffer({"root": t1})["root"] assert t1.equals(t2)
def test_interp_time(ts, time, magnitude_exp, unit_exp): """Test the interp_time function.""" result = ts.interp_time(time) assert np.all(np.isclose(result.data.magnitude, magnitude_exp)) assert result.units == U_(unit_exp) time = Time(time) if len(time) == 1: assert result.time is None else: assert np.all(Time(result.time, result._reference_time) == time)
def test_resample(number_or_interval, exp_values): """Test resample method.""" t_ref = "2000-01-01" t_delta = Time(Q_([3, 5, 8, 9], "s")) t_abs = t_delta + t_ref exp_delta = Time(Q_(exp_values, "s")) exp_abs = exp_delta + t_ref result_delta = t_delta.resample(number_or_interval) result_abs = t_abs.resample(number_or_interval) assert result_delta.all_close(exp_delta) assert result_abs.all_close(exp_abs)
def xr_interp_coordinates_in_time(da: xr.DataArray, times: types_time_like) -> xr.DataArray: """Interpolate an xarray DataArray that represents 3d coordinates in time. Parameters ---------- da : xarray.DataArray xarray DataArray times : pandas.TimedeltaIndex or pandas.DatetimeIndex Time data Returns ------- xarray.DataArray Interpolated data """ if "time" not in da.dims: # not time dependent 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
def test_union(test_instance, list_of_objects, time_exp): """Test input types for Time.union function. Parameters ---------- list_of_objects: List with input objects time_exp: Expected result time """ if test_instance: instance = Time(list_of_objects[0]) assert np.all(instance.union(list_of_objects[1:]) == time_exp) else: assert np.all(Time.union(list_of_objects) == time_exp)
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) 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 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.isel(time=0).data if time.min() >= self.time.max(): # only use edge timestamp return self.coordinates.isel(time=-1).data # full interpolation with overlapping times return ut.xr_interp_coordinates_in_time(self.coordinates, time)
def xr_3d_vector( data: wxt.ArrayLike, time: types_time_like = None, add_dims: list[str] = None, add_coords: dict[str, Any] = None, ) -> xr.DataArray: """Create an xarray 3d vector with correctly named dimensions and coordinates. Parameters ---------- data Full data array. time Optional values that will fill the 'time' dimension. add_dims Addition dimensions to add between ["time", "c"]. If either "c" or "time" are present in add_dims they are used to locate the dimension position in the passed array. add_coords Additional coordinates to assign to the xarray. ("c" and "time" coordinates will be assigned automatically) Returns ------- xarray.DataArray """ if add_dims is None: add_dims = [] if add_coords is None: add_coords = {} dims = ["c"] coords = dict(c=["x", "y", "z"]) # if data is static but time passed we discard time information if time is not None and Q_(data).ndim == 1: time = None # remove duplicates and keep order dims = list(dict.fromkeys(add_dims + dims)) if time is not None: if "time" not in dims: # prepend to beginning if not already set dims = ["time"] + dims coords["time"] = time # type: ignore[assignment] if "time" in coords: coords["time"] = Time(coords["time"]).index coords = dict(add_coords, **coords) da = xr.DataArray(data=data, dims=dims, coords=coords).transpose(..., "c") return da.astype(float).weldx.time_ref_restore()
def time(self) -> Union[Time, None]: """Get the time union of the local coordinate system (None if system is static). Returns ------- xarray.DataArray Time-like data array representing the time union of the LCS """ if "time" in self._dataset.coords: return Time(self._dataset.time, self.reference_time) return None
def _initialize_delta_type(cls_type, values, unit): """Initialize the passed time type.""" if cls_type is np.timedelta64: if isinstance(values, List): return np.array(values, dtype=f"timedelta64[{unit}]") return np.timedelta64(values, unit) if cls_type is Time: return Time(Q_(values, unit)) if cls_type is str: if not isinstance(values, List): return f"{values}{unit}" return [f"{v}{unit}" for v in values] return cls_type(values, unit)
def _build_time( coordinates: Union[types_coordinates, TimeSeries], time: types_time_like, time_ref: types_timestamp_like, ) -> Union[Time, None]: if time is None: if isinstance(coordinates, TimeSeries) and coordinates.is_discrete: time = coordinates.time # this branch is relevant if coordinates and orientations are xarray types elif time_ref is not None: time = time_ref return Time(time, time_ref) if time is not None else None
def _default_dicts(): """Return two equivalent deeply nested structures to be modified by tests.""" a = { "foo": np.arange(3), "x": { 0: [1, 2, 3] }, "bar": True, "s": {1, 2, 3}, "t": Time(["1s", "2s", "3s"]), } b = copy.deepcopy(a) return a, b
def interp_time( self, time: types_time_like, time_ref: types_timestamp_like = None, ) -> 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 : Target time values. If `None` is passed, no interpolation will be performed. time_ref: The reference timestamp Returns ------- LocalCoordinateSystem Coordinate system with interpolated data """ if (not self.is_time_dependent) or (time is None): return self time = Time(time, time_ref) if self.has_reference_time != time.is_absolute: raise TypeError( "Only 1 reference time provided for time dependent coordinate " "system. Either the reference time of the coordinate system or the " "one passed to the function is 'None'. Only cases where the " "reference times are both 'None' or both contain a timestamp are " "allowed. Also check that the reference time has the correct type." ) 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 return LocalCoordinateSystem(orientation, coordinates, time)
def _unify_time_axis( orientation: xr.DataArray, coordinates: Union[xr.DataArray, TimeSeries] ) -> tuple: """Unify time axis of orientation and coordinates if both are DataArrays.""" if ( not isinstance(coordinates, TimeSeries) 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]) orientation = ut.xr_interp_orientation_in_time(orientation, time_union) coordinates = ut.xr_interp_coordinates_in_time(coordinates, time_union) return (orientation, coordinates)
def test_init( self, input_vals: Union[type, tuple[type, str]], set_time_ref: bool, scl: bool, arr: bool, ): """Test the `__init__` method of the time class. Parameters ---------- input_vals : Either a compatible time type or a tuple of two values. The tuple is needed in case the tested time type can either represent relative time values as well as absolute ones. In this case, the first value is the type. The second value is a string specifying if the type represents absolute ("datetime") or relative ("timedelta") values. set_time_ref : If `True`, a reference time will be passed to the `__init__` method scl : If `True`, the data of the passed type consists of a single value. arr : If `True`, the data of the passed type is an array """ input_type, is_timedelta = self._parse_time_type_test_input(input_vals) # skip matrix cases that do not work -------------------- if arr and input_type in [Timedelta, Timestamp]: return if not arr and input_type in [DTI, TDI]: return # create input values ----------------------------------- delta_val = [1, 2, 3] abs_val = [f"2000-01-01 16:00:0{v}" for v in delta_val] time = _initialize_time_type(input_type, delta_val, abs_val, is_timedelta, arr, scl) time_ref = "2000-01-01 15:00:00" if set_time_ref else None # create `Time` instance -------------------------------- time_class_instance = Time(time, time_ref) # check results ----------------------------------------- exp = self._get_init_exp_values(is_timedelta, time_ref, scl, delta_val, abs_val) assert time_class_instance.is_absolute == exp["is_absolute"] assert time_class_instance.reference_time == exp["time_ref"] assert np.all(time_class_instance.as_timedelta() == exp["timedelta"]) if exp["is_absolute"]: assert np.all(time_class_instance.as_datetime() == exp["datetime"]) else: with pytest.raises(TypeError): time_class_instance.as_datetime()
def xr_interp_orientation_in_time(da: xr.DataArray, time: types_time_like) -> xr.DataArray: """Interpolate an xarray DataArray that represents orientation data in time. Parameters ---------- da : xarray DataArray containing the orientation as matrix time : Time data Returns ------- xarray.DataArray Interpolated data """ if "time" not in da.dims: return da if len(da.time) == 1: # remove "time dimension" for static case return da.isel({"time": 0}) 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 da = xr_interp_like(da, {"time": time}, fillna=True) # resync and reset to correct format if time_ref: 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 da
def time_ref(self, value: types_timestamp_like): """Convert INPLACE to new reference time. If no reference time exists, the new value will be assigned. """ if value is None: raise TypeError("'None' is not allowed as value.") if "time" in self._obj.coords: value = Time(value).as_timestamp() if self._obj.weldx.time_ref and is_timedelta64_dtype( self._obj.time): if value == self._obj.weldx.time_ref: return _attrs = self._obj.time.attrs time_delta = value - self._obj.weldx.time_ref self._obj["time"] = self._obj.time.data - time_delta self._obj.time.attrs = _attrs # restore old attributes ! self._obj.time.attrs[ "time_ref"] = value # set new time_ref value else: self._obj.time.attrs["time_ref"] = value
def check_coordinate_system( lcs: tf.LocalCoordinateSystem, orientation_expected: Union[np.ndarray, list[list[Any]], DataArray], coordinates_expected: Union[np.ndarray, list[Any], DataArray], positive_orientation_expected: bool = True, time=None, time_ref=None, ): """Check the values of a coordinate system. Parameters ---------- lcs : Coordinate system that should be checked orientation_expected : Expected orientation coordinates_expected : Expected coordinates positive_orientation_expected : Expected orientation time : A pandas.DatetimeIndex object, if the coordinate system is expected to be time dependent. None otherwise. time_ref: The expected reference time """ orientation_expected = np.array(orientation_expected) coordinates_expected = np.array(coordinates_expected) if time is not None: assert orientation_expected.ndim == 3 or coordinates_expected.ndim == 2 assert np.all(lcs.time == Time(time, time_ref)) assert lcs.reference_time == time_ref check_coordinate_system_orientation( lcs.orientation, orientation_expected, positive_orientation_expected ) assert np.allclose(lcs.coordinates.values, coordinates_expected, atol=1e-9)
def xr_3d_matrix(data: wxt.ArrayLike, time: Time = None) -> xr.DataArray: """Create an xarray 3d matrix with correctly named dimensions and coordinates. Parameters ---------- data : Data time : Optional time data (Default value = None) Returns ------- xarray.DataArray """ if time is not None and np.array(data).ndim == 3: if isinstance(time, Time): time = time.as_pandas_index() da = xr.DataArray( data=data, dims=["time", "c", "v"], coords={ "time": time, "c": ["x", "y", "z"], "v": [0, 1, 2] }, ) else: da = xr.DataArray( data=data, dims=["c", "v"], coords={ "c": ["x", "y", "z"], "v": [0, 1, 2] }, ) return da.astype(float).weldx.time_ref_restore()
def test_convert_util(): """Test basic conversion functions from/to xarray/pint.""" t = pd.date_range("2020", periods=10, freq="1s") ts = t[0] arr = xr.DataArray( np.arange(10), dims=["time"], coords={"time": t - ts}, ) arr.time.weldx.time_ref = ts time = Time(arr) assert len(time) == len(t) assert time.equals(Time(t)) time_q = time.as_quantity() assert np.all(time_q == Q_(range(10), "s")) assert time_q.time_ref == ts arr2 = time.as_data_array() assert arr.time.identical(arr2.time)
def from_yaml_tree(self, node: dict, tag: str, ctx): """Construct Time from tree.""" return Time(node["values"], node.get("reference_time"))
def to_yaml_tree(self, obj: Time, tag: str, ctx) -> dict: """Convert to python dict.""" tree = dict() tree["values"] = obj.as_pandas() tree["reference_time"] = obj._time_ref return tree
def test_add_timedelta( self, other_type, other_on_rhs: bool, unit: str, time_class_is_array: bool, other_is_array: bool, ): """Test the `__add__` method if the `Time` class represents a time delta. Parameters ---------- other_type : The type of the other object other_on_rhs : If `True`, the other type is on the rhs of the + sign and on the lhs otherwise unit : The time unit to use time_class_is_array : If `True`, the `Time` instance contains 3 time values and 1 otherwise other_is_array : If `True`, the other time object contains 3 time values and 1 otherwise """ other_type, is_timedelta = self._parse_time_type_test_input(other_type) # skip array cases where the type does not support arrays if other_type in [Timedelta, Timestamp] and other_is_array: return if not other_is_array and other_type in [DTI, TDI]: return # skip __radd__ cases where we got conflicts with the other types' __add__ if not other_on_rhs and other_type in ( Q_, np.ndarray, np.timedelta64, np.datetime64, DTI, TDI, ): return # setup rhs delta_val = [4, 6, 8] if unit == "s": abs_val = [f"2000-01-01 10:00:0{v}" for v in delta_val] else: abs_val = [f"2000-01-01 1{v}:00:00" for v in delta_val] other = _initialize_time_type( other_type, delta_val, abs_val, is_timedelta, other_is_array, not other_is_array, unit, ) # setup lhs time_class_values = [1, 2, 3] if time_class_is_array else [1] time_class = Time(Q_(time_class_values, unit)) # setup expected values add = delta_val if other_is_array else delta_val[0] exp_val = np.array(time_class_values) + add exp_val += 0 if is_timedelta else time_class_values[0] - exp_val[0] exp_time_ref = None if is_timedelta else abs_val[0] exp = Time(Q_(exp_val, unit), exp_time_ref) # calculate and evaluate result res = time_class + other if other_on_rhs else other + time_class assert res.reference_time == exp.reference_time assert np.all(res.as_timedelta() == exp.as_timedelta()) assert np.all(res == exp)
def test_resample_exceptions(values, number_or_interval, raises): """Test possible exceptions of the resample method.""" with pytest.raises(raises): Time(values).resample(number_or_interval)
def test_add_datetime( other_type, other_on_rhs: bool, time_class_is_array: bool, other_is_array: bool, ): """Test the `__add__` method if the `Time` class represents a datetime. Parameters ---------- other_type : The type of the other object other_on_rhs : If `True`, the other type is on the rhs of the + sign and on the lhs otherwise time_class_is_array : If `True`, the `Time` instance contains 3 time values and 1 otherwise other_is_array : If `True`, the other time object contains 3 time values and 1 otherwise """ # skip array cases where the type does not support arrays if other_type in [Timedelta, Timestamp] and other_is_array: return if not other_is_array and other_type in [DTI, TDI]: return # skip __radd__ cases where we got conflicts with the other types' __add__ if not other_on_rhs and other_type in (Q_, np.ndarray, np.timedelta64, TDI): return # setup rhs delta_val = [4, 6, 8] other = _initialize_time_type( other_type, delta_val, None, True, other_is_array, not other_is_array, "s", ) # setup lhs time_class_values = [1, 2, 3] if time_class_is_array else [1] time_class = Time(Q_(time_class_values, "s"), "2000-01-01 10:00:00") # setup expected values add = delta_val if other_is_array else delta_val[0] exp_val = np.array(time_class_values) + add exp_time_ref = time_class.reference_time exp = Time(Q_(exp_val, "s"), exp_time_ref) # calculate and evaluate result res = time_class + other if other_on_rhs else other + time_class assert res.reference_time == exp.reference_time assert np.all(res.as_timedelta() == exp.as_timedelta()) assert np.all(res == exp)
def test_duration(values, exp_duration): """Test the duration property.""" t = Time(values) assert t.duration.all_close(exp_duration)
def _date_diff(date_1: str, date_2: str, unit: str) -> int: """Calculate the diff between two dates in the specified unit.""" return int( Time(Timestamp(date_1) - Timestamp(date_2)).as_quantity().m_as(unit))
def test_pandas_index(arg, expected): """Test conversion to appropriate pd.TimedeltaIndex or pd.DatetimeIndex.""" t = Time(arg) assert np.all(t.as_pandas_index() == expected) assert np.all(t.as_pandas_index() == t.index) assert np.all(t.as_timedelta_index() == t.timedelta)
def test_sub( self, other_type, other_on_rhs: bool, unit: str, time_class_is_array: bool, time_class_is_timedelta: bool, other_is_array: bool, ): """Test the `__sub__` method of the `Time` class. Parameters ---------- other_type : The type of the other object other_on_rhs : If `True`, the other type is on the rhs of the + sign and on the lhs otherwise unit : The time unit to use time_class_is_array : If `True`, the `Time` instance contains 3 time values and 1 otherwise time_class_is_timedelta : If `True`, the `Time` instance represents a time delta and a datetime otherwise other_is_array : If `True`, the other time object contains 3 time values and 1 otherwise """ other_type, other_is_timedelta = self._parse_time_type_test_input( other_type) if other_on_rhs: lhs_is_array = time_class_is_array lhs_is_timedelta = time_class_is_timedelta rhs_is_array = other_is_array rhs_is_timedelta = other_is_timedelta else: lhs_is_array = other_is_array lhs_is_timedelta = other_is_timedelta rhs_is_array = time_class_is_array rhs_is_timedelta = time_class_is_timedelta # skip array cases where the type does not support arrays or scalars if other_type in [Timedelta, Timestamp] and other_is_array: return if not other_is_array and other_type in [DTI, TDI]: return # skip __rsub__ cases where we got conflicts with the other types' __sub__ if not other_on_rhs and other_type in ( Q_, np.ndarray, np.timedelta64, np.datetime64, DTI, TDI, ): return # skip cases where an absolute time is on the rhs, since pandas does # not support this case (and it does not make sense) if lhs_is_timedelta and not rhs_is_timedelta: return # skip cases where the lhs is a scalar and the rhs is an array because it will # always involve non monotonically increasing array values, which is forbidden. if rhs_is_array and not lhs_is_array: return # test values vals_lhs = [3, 5, 9] if lhs_is_array else [3] vals_rhs = [1, 2, 3] if rhs_is_array else [1] # setup rhs other_val = vals_rhs if other_on_rhs else vals_lhs if unit == "s": abs_val = [f"2000-01-01 10:00:0{v}" for v in other_val] else: abs_val = [f"2000-01-01 1{v}:00:00" for v in other_val] other = _initialize_time_type( other_type, other_val, abs_val, other_is_timedelta, other_is_array, not other_is_array, unit, ) # setup lhs time_class_values = vals_lhs if other_on_rhs else vals_rhs time_class_time_ref = None if time_class_is_timedelta else "2000-01-01 11:00:00" time_class = Time(Q_(time_class_values, unit), time_class_time_ref) # setup expected values sub = vals_rhs if other_is_array else vals_rhs[0] exp_val = np.array(vals_lhs) - sub if not other_is_timedelta: if time_class_is_timedelta: exp_val -= time_class_values[0] + exp_val[0] else: d = self._date_diff(time_class_time_ref, abs_val[0], unit) + vals_rhs[0] exp_val += d if other_on_rhs else (d + exp_val[0]) * -1 exp_time_ref = None if not other_is_timedelta and time_class_is_timedelta: exp_time_ref = abs_val[0] elif other_is_timedelta and not time_class_is_timedelta: exp_time_ref = time_class_time_ref exp = Time(Q_(exp_val, unit), exp_time_ref) # calculate and evaluate result res = time_class - other if other_on_rhs else other - time_class assert res.reference_time == exp.reference_time assert np.all(res.as_timedelta() == exp.as_timedelta()) assert np.all(res == exp)