---
title: Time Series Generation
toc: true
number-sections: true
mainfont: Arial
format:
  html: default
jupyter: python3
---


```{python}
#| echo: false
#| output: false
%load_ext autoreload
%autoreload 2
```

This tutorial covers the generation of time series signals. It is shown how to create linear interpolated signals and periodic signals (superimposing several frequencies).

```{python}
import numpy as np
from matplotlib import pyplot as plt
```

# Linear Signals
Linear interpolated time series signals can be created by specifying points in time `t` and the corresponding values `y`: 

```{python}
from trimes.signal_generation import linear_time_series

t = (0, 2, 2, 3, 5)
y = (2, 2, 1.5, 1.2, 1.2)
sample_time = 1e-3

ts = linear_time_series(t, y, sample_time)
ts.plot(grid=True)
```

If `y` is a list, several lines are generated:

```{python}
t = (0, 2, 2, 3, 5)
y = [(2, 2, 1.5, 1.2, 1.2), (0, 1, 1, 1, 1)]

ts = linear_time_series(t, y, sample_time)
ts.columns = ("upper boundary", "lower boundary")
ts.plot(grid=True)
```

Symmetric signals can also be created using `mirror_y` (mirroring of the signal at a y-value):

```{python}
from trimes.signal_generation import mirror_y

upper_boundary = ts.iloc[:, 0]
ts_envelope = mirror_y(upper_boundary, 1, inplace=True)
ts_envelope.columns = ("upper boundary", "lower boundary")
ts_envelope.plot(grid=True)
```

# Discrete Linear Signals
In the above examples, we created discrete steps from one time step to the other (discontinuity) by setting two consecutive time steps with the same value (e.g. `t = (0, 2, 2, 3, 5)`). If you want to create a signal solely containing discrete steps and don't want to type the values of each step twice, there is a convenience function which accepts a *Timeseries* as input:

```{python}
from trimes.signal_generation import discretize
import pandas as pd

df = pd.DataFrame((1, 3, 0, 2))
df.index = (0, 1, 3, 4.2)
df.columns = ["linear"]

ts = discretize(df.copy())
ts.columns = ["discrete"]
ax = ts.plot()
df.plot(ax=ax, grid=True)

ts
```

# Periodic Signals
The `PeriodicSignal` class can be used to create periodic signals of any shape (flexibly set parameters like frequency, magnitude, offset and initial angle). By default, cosine signals are created:

```{python}
from trimes.signal_generation import PeriodicSignal

sample_time = 1e-4
mag = 110 * np.sqrt(2) / np.sqrt(3)
t = np.arange(0, 0.1, sample_time)
cosine_wave = PeriodicSignal(t, f=50, mag=mag, offset=10, phi=np.pi * 0.3)
cosine_wave_series = cosine_wave.get_signal_series()
cosine_wave_series.plot()
plt.grid()
plt.show()
```

To create gradients, use tuples as parameters (**don't use lists**).

```{python}
plt.close("all")

cosine_wave = PeriodicSignal(
    np.arange(0, 0.1, sample_time), f=(50, 150), mag=(0.9 * mag, 1.5 * mag)
)
cosine_wave.plot(grid=True)
```

The `plot_with_attributes` illustrates the signal as well as its parameters over time.

```{python}
cosine_wave.plot_with_attributes()
plt.grid()
```

Symmetric multi-phase signals can be created based on the `PeriodicSignal` class by using `get_signal_n_phases` (angular phase difference between signals is $360/n$):

```{python}
cosine_wave = PeriodicSignal(
    np.arange(0, 0.1, sample_time), f=(50, 55), mag=(0.9 * mag, 1.1 * mag)
)
cosine_wave_series_3_phase = cosine_wave.get_signal_n_phases(3)
cosine_wave_series_3_phase.plot(grid=True)
```

Signals can be superimposed and concatenated by creating an array of signals. The rows of the array are superimposed and the resulting column is concatenated. The following signal is created:
- two time periods (with varying frequencies)
  - 0-0.1 s
  - 0.1-0.2 s
- several frequencies are superimposed:
  - fundamental: $50$ Hz (actually varies between $50$ and $60$ Hz)
  - fifth harmonic: $5*50=250$ Hz
  - negative sequence: rotating with $-50$ Hz (actually varies between $-50$ and $-60$ Hz)
- $3$ phases

```{python}
from trimes.signal_generation import (
    superimpose_and_concat_periodic_signals,
)

time_spans = [np.arange(0, 0.1, sample_time), np.arange(0.1, 0.2, sample_time)]
signals = [
    [
        PeriodicSignal(time_spans[0], f=(50, 60), mag=mag),
        PeriodicSignal(time_spans[0], f=(50 * 5, 60 * 5), mag=0.05 * mag),
        PeriodicSignal(time_spans[0], f=(-50, -60), mag=0.03 * mag),
    ],    
    [
        PeriodicSignal(time_spans[1], f=50, mag=mag),
        PeriodicSignal(time_spans[1], f=5 * 50, mag=0.05 * mag),
        PeriodicSignal(time_spans[1], f=-50, mag=0.03 * mag),
    ],
]
res = superimpose_and_concat_periodic_signals(
    signals, num_phases=3,
)
res.plot()
plt.grid()
```

Notice that the signals are continuous at the transition between periods (at t = 0.1 s), since the final angle of the previous period is automatically carried over as the initial angle of the next. This behavior can be disabled, in which case the initial angle resets to zero at each period boundary, resulting in a discontinuity at t = 0.1 s:

```{python}
res = superimpose_and_concat_periodic_signals(
    signals, num_phases=3, angle_continuation=False
)
res.loc[0.08:0.12, :].plot()
plt.grid()
```

It is also possible to specify a certain angular jump at the transition:

```{python}
res = superimpose_and_concat_periodic_signals(
    signals, num_phases=3, angular_jump=[np.pi/2] # 90 degree angular jump
)
res.loc[0.08:0.12, :].plot()
plt.grid()
```

The parameters of the signals can be exctracted using `get_attributes_of_superimposed_and_concatenated_signals_over_time` and illustrated:

```{python}
from trimes.signal_generation import (
    get_attributes_of_superimposed_and_concatenated_signals_over_time,
)

attrs_of_columns = get_attributes_of_superimposed_and_concatenated_signals_over_time(
    signals
)
attrs_of_columns[0].plot(title="fundamental")
plt.grid()
attrs_of_columns[1].plot(title="harmonic")
plt.grid()
attrs_of_columns[2].plot(title="negative sequence")
plt.grid()
```

# Noise
To illustrate the addition of noise to signals, we first create a "clean" periodic three-phase signal.

```{python}
import numpy as np
import pandas as pd

sample_time = 1e-4
mag = 110 * np.sqrt(2) / np.sqrt(3)
t = np.arange(0, 0.1, sample_time)
cosine_wave = PeriodicSignal(t, f=50, mag=mag, offset=10, phi=np.pi * 0.3)
cosine_wave_series = cosine_wave.get_signal_n_phases(3)
cosine_wave_series.plot()
plt.grid()
```

There are functions to add normal and uniform distributed noise to time series.

```{python}
from trimes.signal_generation import add_normal_noise, add_uniform_noise

cosine_normal_noise = add_normal_noise(cosine_wave_series, 0.1*mag)
cosine_normal_noise.plot()
plt.grid()
plt.title("Normal noise")

cosine_normal_noise = add_uniform_noise(cosine_wave_series, -0.1*mag, 0.1*mag)
cosine_normal_noise.plot()
plt.grid()
plt.title("Uniform noise")
```

