Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update TimeSeries handling of absolute times #791

Merged
merged 14 commits into from
Jul 27, 2022
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 19 additions & 14 deletions weldx/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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.')

Expand Down Expand Up @@ -398,19 +398,18 @@ 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(
self,
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
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
100 changes: 66 additions & 34 deletions weldx/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand All @@ -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 -----------------------------------------------------

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

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand All @@ -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 ---------------------------------------------------------

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