import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from trimes.signal_generation import PeriodicSignal
= 0
t_start = 5
t_end = 1e-3
average_sample_time = np.arange(t_start, t_end, average_sample_time)
time = time + (np.random.rand(len(time)) - 0.5) * 1e-5
time
= PeriodicSignal(time, mag=0.8, f=0.3)
sig_a = PeriodicSignal(time, mag=0.6, f=0.3)
sig_b
= pd.DataFrame({"a": sig_a.get_signal(), "b": sig_b.get_signal()}, index=time)
df_ts = "time"
df_ts.index.name =True) df_ts.plot(grid
Time Series Comparison
This tutorial covers the comparison between time series signals and the calculation of metrics. Starting from simple subtraction, metrics like root mean squared error for upper and lower boundaries (envelope) are calculated. Finally, step response data is analyzed. The tutorial also illustrates some helper functions for plotting (e.g. fill between two time series).
1 Addition and Subtraction
First we create a time series with two curves between 0 and 5 seconds. The time samples are randomly varied (sample time is not constant). This could be for example results of simulations with an adaptive (variable) step solver. Note that there is a separate tutorial on signal generation.
Next we create a reference signal with a different sampling time compared to above.
= np.linspace(t_start - 1, t_end + 1, 100)
time = PeriodicSignal(time, mag=1, f=0.3, phi=-2)
sig_ref = sig_ref.get_signal_series()
series_reference
df_ts.plot()= "reference"
series_reference.name =True)
series_reference.plot(grid
="upper center") plt.legend(loc
We calculate the difference between signals ‘a’/‘b’ and the reference. The reference is automatically resampled according to the sample time of signals ‘a’/‘b’, i.e. the option resample_reference
is set to True
. The option is available in most of the functions in the following but is set to False
by default to avoid unnecessary resampling that deteriorates the performance. There is also the option resample_ts
to resample the time series ‘a’/‘b’ to the reference time base. Instead of resampling at every function call, consider using resample
from trimes.base
to align the sample time only once before the comparison if the sample time remains constant.
A diagram with two y-axes is used for visualization (original signals on the left axis, difference on the right). In addition, the area between ‘b’ and ‘reference’ is filled.
from trimes.comparison import subtract
from trimes.plots import plot_2y, fill_between
= subtract(df_ts, series_reference, resample_reference=True)
difference = ("diff a", "diff b")
difference.columns
= pd.concat([df_ts, series_reference])
signals_a_b_and_reference = plot_2y(
ax1, ax2 ={"linestyle": "--"}
signals_a_b_and_reference, difference, kwargs_ax2
)"a"], series_reference, alpha=0.2, hatch="/")
fill_between(df_ts[ plt.grid()
Addition works similar:
from trimes.comparison import add
sum = add(df_ts, series_reference, resample_reference=True)
sum.columns = ("a + ref", "b + ref")
= sum.plot()
ax =ax)
signals_a_b_and_reference.plot(ax plt.grid()
2 Compare to Reference Signal
A common use case is to check how well a signal aligns with a reference signal. We first create a periodic signal and a reference (note that there is a separate tutorial on signal generation).
from trimes.signal_generation import PeriodicSignal, linear_time_series
from trimes.plots import fill_between
= 1e-3
sample_time = PeriodicSignal(
signal 0, 5 + sample_time, sample_time),
np.arange(=0.5,
f=(1.5, 1),
offset=(2, 0.01),
mag=np.pi / 2,
phi
)= signal.get_signal_series()
wave = "wave"
wave.name = wave.plot()
ax
= (0, 2, 5)
t = (2, 1, 1)
y = 1e-3
sample_time = linear_time_series(t, y, sample_time)
reference = "reference"
reference.name =ax, grid=True)
reference.plot(ax=0.2)
fill_between(wave, reference, alpha plt.legend()
comparison_series
and comparison_df
calculate any error metric (default is integral_abs_error
, i.e. area) for series and dataframes. Some metrics are defined in trimes.metrics
. Further metrics from the scikit-learn package can be used.
from trimes.comparison import comparison_series, comparison_df
from trimes.metrics import integral_squared_error
from sklearn.metrics import root_mean_squared_error
print(comparison_series(wave, reference))
print(comparison_series(wave, reference, metric=integral_squared_error))
= pd.concat([wave, wave * 0.1], axis=1)
df_waves print(
comparison_df(=root_mean_squared_error, sample_time=sample_time
df_waves, reference, metric
) )
3.4983548161487557
0.004193576094696082
[0.91582761 1.13841978]
3 Boundaries and Envelopes
3.1 Create Boundaries as Linear Time Series Signals
We will create boundaries and check whether a time series signal remains within the envelope and calculate error metrics.
from trimes.signal_generation import linear_time_series, mirror_y
= (0, 2, 3, 3, 5)
t = (2, 2, 1.5, 1.2, 1.2)
y = 1e-3
sample_time = linear_time_series(t, y, sample_time)
ts = mirror_y(ts, 1, inplace=True)
ts_envelope = ("upper boundary", "lower boundary")
ts_envelope.columns =True, linestyle="--") ts_envelope.plot(grid
Create the periodic signal:
from trimes.signal_generation import PeriodicSignal
= PeriodicSignal(
signal 0, 5 + sample_time, sample_time),
np.arange(=0.5,
f=(1.5, 1),
offset=(2, 0.01),
mag=np.pi / 2,
phi
)= signal.get_signal_series()
wave = "wave"
wave.name = plt.subplot()
ax =ax)
wave.plot(ax=ax, linestyle="--")
ts_envelope.plot(ax
plt.legend() plt.grid()
3.2 Check Boundaries
greater_than_series
and smaller_than_series
compare the wave to the boundary and return a boolean array.
from trimes.comparison import greater_than_series
= ts_envelope.iloc[:, 0]
upper_boundary greater_than_series(wave, upper_boundary)
array([False, False, False, ..., False, False, False])
apply_operator_series
can be used in a similar way using any suitable operator from the built-in operator
module.
import operator
from trimes.comparison import apply_operator_series
from trimes.plots import fill_between
= apply_operator_series(wave, ts_envelope.iloc[:, 0], operator.gt)
greater = apply_operator_series(wave, ts_envelope.iloc[:, 1], operator.lt)
smaller
= plt.subplot()
ax =ax)
wave.plot(ax
=ax, linestyle="--")
ts_envelope.plot(ax0], alpha=0.5)
fill_between(wave.iloc[greater], ts_envelope.iloc[greater, 1], alpha=0.5)
fill_between(wave.iloc[smaller], ts_envelope.iloc[smaller,
plt.legend() plt.grid()
For convenience there is also a method that checks upper and lower boundary at once:
from trimes.comparison import outside_envelope
outside_envelope(wave, ts_envelope)
array([False, False, False, ..., False, False, False])
If you are interested in the time a condition is fullfilled (like a signal being outside an envelope), the get_time_bool
function calculates the duration from a boolean array and the sampling time.
from trimes.comparison import get_time_bool
= outside_envelope(wave, ts_envelope)
out get_time_bool(out, ts_envelope.index.values)
np.float64(2.1029999999999998)
3.3 Calculate Metric
Next we calculate metrics such as the area where the wave exceeds the envelope. comparison_series
/comparison_df
accept an operator as input argument to consider only time spans where the condition is True
.
from trimes.comparison import comparison_series, comparison_df
from trimes.metrics import integral_squared_error
from sklearn.metrics import root_mean_squared_error
print(
comparison_series(
wave,0],
ts_envelope.iloc[:,
operator.gt,
)
)print(
comparison_series(1], operator.lt, metric=root_mean_squared_error
wave, ts_envelope.iloc[:,
)
)= pd.concat([wave, wave * 1.1], axis=1)
df_waves print(
comparison_df(
df_waves,0],
ts_envelope.iloc[:,
operator.gt,=integral_squared_error,
metric
) )
0.6832649919854159
0.07301156189253198
[0.00036104 0.00072028]
For convenience there are also methods for envelopes instead of a single boundary.
from trimes.comparison import envelope_comparison_series, envelope_comparison_df
envelope_comparison_series(wave, ts_envelope) envelope_comparison_df(df_waves, ts_envelope)
array([0.77815754, 1.11895373])
4 Step Response Info
trimes provides an interface to the control package to get the step response info (overshoot etc.) of time series. Let’s first create a step response signal.
= np.arange(0, 5 + sample_time, sample_time)
t = 10 - 5 * np.exp(-t)
offset = PeriodicSignal(
step_response 0, 5 + sample_time, sample_time),
np.arange(=1,
f=offset,
offset=(1, 0.01),
mag=np.pi / 2,
phi
)= step_response.get_signal_series()
step_series = "step response"
step_series.name =True)
step_series.plot(grid plt.legend()
Get the step response info. Note that the y-values in the results are relative to the initial value.
from trimes.control import step_info_series
= step_info_series(step_series)
info info
{'RiseTime': np.float64(1.303),
'SettlingTime': np.float64(4.405),
'SettlingMin': np.float64(3.906303780009649),
'SettlingMax': np.float64(5.140223553657352),
'Overshoot': np.float64(3.501861127732394),
'Undershoot': np.float64(2.0758071455346307),
'Peak': np.float64(5.140223553657352),
'PeakTime': np.float64(3.742),
'SteadyStateValue': np.float64(4.966310265004573)}
There is also a function to illustrate the results.
from trimes.control import plot_step_info
=True)
step_series.plot(grid
plot_step_info(step_series, info) plt.legend()
4.896908976649615