diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 93e74fab5..01d255d9a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ fixes - `MathematicalExpression` now uses SciPy and NumPy in numerical function evaluation. This enables it to use advanced integration methods and fixes lengths computation of `DynamicShapeSegment` [:pull:`770`]. - Fix errors in tutorial about quality standards [:pull:`777`] +- Correct wrong handling of absolute times of the `TimeSeries` class [:pull:`791`] ******************** 0.6.1 (19.05.2022) diff --git a/weldx/core.py b/weldx/core.py index 1d47876ae..8a1a544d4 100644 --- a/weldx/core.py +++ b/weldx/core.py @@ -16,7 +16,7 @@ import weldx.util as ut from weldx.constants import Q_, U_, UNITS_KEY -from weldx.time import Time, TimeDependent, types_time_like +from weldx.time import Time, TimeDependent, types_time_like, types_timestamp_like from weldx.util import check_matplotlib_available if TYPE_CHECKING: # pragma: no cover @@ -301,7 +301,7 @@ def __init__( data: Union[pint.Quantity, MathematicalExpression], time: types_time_like = None, interpolation: str = None, - reference_time: pd.Timestamp = None, + reference_time: types_timestamp_like = None, ): """Construct a TimSeries. @@ -325,12 +325,12 @@ def __init__( self._shape = None self._units = None self._interp_counter = 0 - self._reference_time = reference_time + self._reference_time = None if isinstance(data, (pint.Quantity, xr.DataArray)): - self._initialize_discrete(data, time, interpolation) + self._initialize_discrete(data, time, interpolation, reference_time) elif isinstance(data, MathematicalExpression): - self._init_expression(data) + self._init_expression(data, reference_time) else: raise TypeError(f'The data type "{type(data)}" is not supported.') @@ -398,12 +398,10 @@ def _check_data_array(data_array: xr.DataArray): def _create_data_array( data: Union[pint.Quantity, xr.DataArray], time: Time ) -> xr.DataArray: - if isinstance(data, xr.DataArray): - return data return ( xr.DataArray(data=data) .rename({"dim_0": "time"}) - .assign_coords({"time": time.as_timedelta_index()}) + .assign_coords({"time": time.as_data_array()}) ) def _initialize_discrete( @@ -411,6 +409,7 @@ def _initialize_discrete( data: Union[pint.Quantity, xr.DataArray], time: types_time_like = None, interpolation: str = None, + reference_time=None, ): """Initialize the internal data with discrete values.""" # set default interpolation @@ -421,7 +420,6 @@ def _initialize_discrete( self._check_data_array(data) data = data.transpose("time", ...) self._data = data - # todo: set _reference_time? else: # expand dim for scalar input data = Q_(data) @@ -431,13 +429,12 @@ def _initialize_discrete( # constant value case if time is None: time = pd.Timedelta(0) - time = Time(time) + time = Time(time, reference_time) - self._reference_time = time.reference_time self._data = self._create_data_array(data, time) self.interpolation = interpolation - def _init_expression(self, data): + def _init_expression(self, data, reference_time): """Initialize the internal data with a mathematical expression.""" if data.num_variables != 1: raise Exception( @@ -464,6 +461,8 @@ def _init_expression(self, data): # assign internal variables self._data = data self._time_var_name = time_var_name + if reference_time is not None: + self._reference_time = pd.Timestamp(reference_time) # check that all parameters of the expression support time arrays try: @@ -480,9 +479,13 @@ def _init_expression(self, data): def _interp_time_discrete(self, time: Time) -> xr.DataArray: """Interpolate the time series if its data is composed of discrete values.""" + data = self._data + if self.time is None and time.is_absolute: + data = data.weldx.reset_reference_time(time.reference_time) # type: ignore + return ut.xr_interp_like( - self._data, - {"time": time.as_data_array()}, + data, + time.as_data_array(), method=self.interpolation, assume_sorted=False, broadcast_missing=False, @@ -587,6 +590,8 @@ def time(self) -> Union[None, Time]: @property def reference_time(self) -> Union[pd.Timestamp, None]: """Get the reference time.""" + if self.is_discrete: + return self._data.weldx.time_ref # type: ignore[union-attr] return self._reference_time def interp_time( diff --git a/weldx/tests/test_core.py b/weldx/tests/test_core.py index b3ea4f0d4..2cbf44656 100644 --- a/weldx/tests/test_core.py +++ b/weldx/tests/test_core.py @@ -200,6 +200,7 @@ def test_evaluate_exceptions(ma_def, variables, exception_type, test_name): ma_def.evaluate(**variables) @staticmethod + @pytest.mark.slow def test_integrate_length_computation(): """Ensure we can integrate with Sympy during length computation.""" from weldx import DynamicShapeSegment @@ -237,7 +238,7 @@ class TestTimeSeries: me_params_vec = {"a": Q_([2, 0, 1], "m/s"), "b": Q_([-2, 3, 0], "m")} - ts_constant = TimeSeries(value_constant) + ts_const = TimeSeries(value_constant) ts_disc_step = TimeSeries(values_discrete, time_discrete, "step") ts_disc_linear = TimeSeries(values_discrete, time_discrete, "linear") ts_expr = TimeSeries(ME(me_expr_str, me_params)) @@ -252,25 +253,40 @@ class TestTimeSeries: (Q_(1, "m"), None, None, (1,)), (Q_([3, 7, 1], "m"), TDI([0, 1, 2], unit="s"), "step", (3,)), (Q_([3, 7, 1], ""), Q_([0, 1, 2], "s"), "step", (3,)), + (Q_([3, 7, 1], ""), DTI(["2010", "2011", "2012"]), "step", (3,)), ], ) - def test_construction_discrete(data: pint.Quantity, time, interpolation, shape_exp): + @pytest.mark.parametrize("reference_time", [None, "2000-01-01"]) + def test_construction_discrete( + data: pint.Quantity, time, interpolation, shape_exp, reference_time + ): """Test the construction of the TimeSeries class.""" + if reference_time is not None and isinstance(time, (pd.DatetimeIndex)): + pytest.skip() + # set expected values time_exp = time - if isinstance(time_exp, pint.Quantity): - time_exp = pd.TimedeltaIndex(time_exp.m, unit="s") + + if time_exp is not None: + time_exp = Time(time, reference_time) exp_interpolation = interpolation if len(data.shape) == 0 and interpolation is None: exp_interpolation = "step" # create instance - ts = TimeSeries(data=data, time=time, interpolation=interpolation) + ts = TimeSeries( + data=data, + time=time, + interpolation=interpolation, + reference_time=reference_time, + ) # check assert np.all(ts.data == data) - assert np.all(ts.time == time_exp) + if time_exp is not None: + assert ts.reference_time == time_exp.reference_time + assert ts.time.all_close(time_exp) assert ts.interpolation == exp_interpolation assert ts.shape == shape_exp assert data.is_compatible_with(ts.units) @@ -280,7 +296,7 @@ def test_construction_discrete(data: pint.Quantity, time, interpolation, shape_e if time_exp is None: assert "time" not in ts.data_array else: - assert np.all(ts.data_array.time == time_exp) + assert Time(ts.data_array.time).all_close(time_exp) # test_construction_expression ----------------------------------------------------- @@ -321,15 +337,22 @@ def test_construction_expression(data, shape_exp, unit_exp): ([1, 2, 3], "time", dict(time=TDI([1, 2, 3])), TypeError), ], ) - def test_init_data_array(data, dims, coords, exception_type): + @pytest.mark.parametrize("reference_time", [None, "2000-01-01"]) + def test_init_data_array(data, dims, coords, reference_time, exception_type): """Test the `__init__` method with an xarray as data parameter.""" da = xr.DataArray(data=data, dims=dims, coords=coords) + exp_time_ref = None + if reference_time is not None: + da.weldx.time_ref = reference_time + exp_time_ref = pd.Timestamp(reference_time) + if exception_type is not None: with pytest.raises(exception_type): TimeSeries(da) else: ts = TimeSeries(da) assert ts.data_array.dims[0] == "time" + assert ts.reference_time == exp_time_ref # test_construction_exceptions ----------------------------------------------------- @@ -371,21 +394,21 @@ def test_construction_exceptions( @pytest.mark.parametrize( "ts, ts_other, result_exp", [ - (ts_constant, TS(value_constant), True), + (ts_const, TS(value_constant), True), (ts_disc_step, TS(values_discrete, time_discrete, "step"), True), (ts_expr, TS(ME(me_expr_str, me_params)), True), - (ts_constant, ts_disc_step, False), - (ts_constant, ts_expr, False), + (ts_const, ts_disc_step, False), + (ts_const, ts_expr, False), (ts_disc_step, ts_expr, False), - (ts_constant, 1, False), + (ts_const, 1, False), (ts_disc_step, 1, False), (ts_expr, 1, False), - (ts_constant, "wrong", False), + (ts_const, "wrong", False), (ts_disc_step, "wrong", False), (ts_expr, "wrong", False), - (ts_constant, TS(Q_(1337, "m")), False), - (ts_constant, TS(Q_(1, "mm")), False), - (ts_constant, TS(Q_(1, "s")), False), + (ts_const, TS(Q_(1337, "m")), False), + (ts_const, TS(Q_(1, "mm")), False), + (ts_const, TS(Q_(1, "s")), False), (ts_disc_step, TS(values_discrete, time_wrong_values, "step"), False), (ts_disc_step, TS(values_discrete_wrong, time_discrete, "step"), False), (ts_disc_step, TS(values_unit_prefix_wrong, time_discrete, "step"), False), @@ -422,16 +445,11 @@ def test_comparison(ts, ts_other, result_exp): @pytest.mark.parametrize( "ts, time, magnitude_exp, unit_exp", [ - (ts_constant, time_single, 1, "m"), - (ts_constant, time_single_q, 1, "m"), - (ts_constant, time_mul, [1, 1, 1, 1, 1, 1, 1, 1], "m"), - ( - ts_constant, - time_mul + pd.Timestamp("2020"), - [1, 1, 1, 1, 1, 1, 1, 1], - "m", - ), - (ts_constant, time_mul_q, [1, 1, 1, 1, 1, 1, 1, 1], "m"), + (ts_const, time_single, 1, "m"), + (ts_const, time_single_q, 1, "m"), + (ts_const, time_mul, [1, 1, 1, 1, 1, 1, 1, 1], "m"), + (ts_const, time_mul + pd.Timestamp("2020"), [1, 1, 1, 1, 1, 1, 1, 1], "m"), + (ts_const, time_mul_q, [1, 1, 1, 1, 1, 1, 1, 1], "m"), (ts_disc_step, time_single, 12, "mm"), (ts_disc_step, time_single_q, 12, "mm"), (ts_disc_step, time_mul, [10, 10, 11, 11, 12, 14, 16, 16], "mm"), @@ -449,18 +467,35 @@ def test_comparison(ts, ts_other, result_exp): (ts_expr_vec, time_mul, results_exp_vec, "m"), ], ) - def test_interp_time(ts, time, magnitude_exp, unit_exp): + @pytest.mark.parametrize("reference_time", [None, "2000-01-01"]) + def test_interp_time(ts, time, magnitude_exp, unit_exp, reference_time): """Test the interp_time function.""" + if reference_time is not None: + if isinstance(ts.data, xr.DataArray): + ts = TimeSeries( + ts.data_array, + reference_time=reference_time, + interpolation=ts.interpolation, + ) + else: + ts = TimeSeries( + ts.data, + time=ts.time, + reference_time=reference_time, + interpolation=ts.interpolation, + ) + time = Time(time, time_ref=reference_time) + result = ts.interp_time(time) assert np.all(np.isclose(result.data.magnitude, magnitude_exp)) assert result.units == U_(unit_exp) - time = Time(time) + time = Time(time, reference_time) if len(time) == 1: assert result.time is None else: - assert np.all(Time(result.time, result._reference_time) == time) + assert result.time.all_close(time) # test_interp_time_warning --------------------------------------------------------- @@ -478,7 +513,7 @@ def test_interp_time_warning(): # test_interp_time_exceptions ------------------------------------------------------ @staticmethod - @pytest.mark.parametrize("ts", [ts_constant, ts_disc_step, ts_disc_linear, ts_expr]) + @pytest.mark.parametrize("ts", [ts_const, ts_disc_step, ts_disc_linear, ts_expr]) @pytest.mark.parametrize( "time, exception_type, test_name", [ @@ -641,11 +676,8 @@ def test_call_operator_expression(u, v, w): params = dict(u=u, v=v, w=w) gs = GenericSeries(expression, parameters=parameters, units=units) - print(gs.ndims) - # perform interpolation gs_interp = gs(**params) - print(gs_interp) # calculate expected result params = {k: Q_(val) for k, val in params.items()} @@ -884,7 +916,7 @@ def _dim_length(i, d, c): for i, (k, v) in enumerate(units.items()): coords[k] = Q_(np.zeros(i + 2), v) - shape = tuple([_dim_length(i, d, coords) for i, d in enumerate(dims)]) + shape = tuple(_dim_length(i, d, coords) for (i, d) in enumerate(dims)) data = Q_(np.ones(shape)) if exception is None: diff --git a/weldx/time.py b/weldx/time.py index eb1d97a1d..43375e189 100644 --- a/weldx/time.py +++ b/weldx/time.py @@ -508,7 +508,10 @@ def as_data_array(self, timedelta_base: bool = True) -> DataArray: else: t = self.index da = xr.DataArray(t, coords={"time": t}, dims=["time"]) - da.time.attrs["time_ref"] = self.reference_time + if self.reference_time is not None: + da.weldx.time_ref = self.reference_time + + da.attrs = da.time.attrs return da @property