Kontinuierlicher Lastgang

Wir betrachten den Verlauf einer elektrischen Last, d. h., den Verlauf der Leistungsaufnahme \(p(t)\) in kW eines elektrischen Verbrauchers über die Zeit \(t\) in h:

\[ p(t) = 5(1 - e^{-2t}) \]

Als erstes plotten wir diesen Lastgang:

Code
import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(0, 3, num=1000)   # (almost continuous) time in h
p = 5*(1 - np.exp(-2*t))          # power in kW

plt.figure(figsize=(5, 3))
plt.plot(t, p, 'k-')
plt.xlabel('time (h)')
plt.ylabel('power (kW)')
plt.grid(True)

Sampling mit Energieerhaltung

Gemessene Lastgänge werden z. B. alle \(\Delta t =\) 15 Minuten mit einem zugehörigen Wert aufgezeichnet. Dabei werden die kontinuierlichen Werte derart in konstante Werte für jedes Zeitintervall umgewandelt, dass die in den Intervallen enthaltene Energie gleich bleibt. Wir bestimmen daher für unseren Beispiellastgang die mittlere Leistung \(\bar{p}\) zwischen zwei Zeitpunkten \(t_1\) und \(t_2\), d. h. \(\Delta t = t_2 - t_1\):

\[ \begin{aligned} \bar{p} &= \frac{1}{t_2 - t_1} \int_{t_1}^{t_2} p(t) \,\text{d}t \\ &= \frac{1}{t_2 - t_1} \int_{t_1}^{t_2} 5(1 - e^{-2t}) \,\text{d}t \\ &= \frac{5}{t_2 - t_1} \left[ t_2 - t_1 + \frac{1}{2}(e^{-2t_2} - e^{-2t_1}) \right] \end{aligned} \]

Das Integral \(E = \int_{t_1}^{t_2} p(t) \,\text{d}t\) ist gleich der vom Verbraucher aufgenommenen Energie \(E\) im Zeitintervall \([t_1, t_2]\). Die so berechnete mittlere Leistung \(\bar{p}\) führt zum selben Energieverbrauch \(E\) wie der kontinuierliche Lastgang \(p(t)\), denn

\[\bar{p}(t_2 - t_1) = E.\]

Bei der Optimierung von Lasten in einem Energienetzwerk werden typischer Weise keine kontinuierlichen Lastgänge, sondern stückweise konstanten Lastgänge optimiert.

Im folgenden Beispiel verwenden wir eine Abtastzeit von \(\Delta t = 0.5\) Stunden:

Code
dt = 0.5  # sampling time in hours
t_discrete = np.arange(start=0, stop=3, step=dt)  # discrete times in hours
print(f"{t_discrete=}")

p_discrete = []
for t_value in t_discrete:
    t_1 = t_value
    t_2 = t_value + dt
    p_mean = 5/dt * (dt + 1/2 *(np.exp(-2*t_2) - np.exp(-2*t_1)))
    p_discrete.append(p_mean)

# t_discrete = list(t_discrete) + [3]
# p_discrete = p_discrete + [p[-1]]

plt.figure(figsize=(5, 3))
plt.plot(t, p, 'k-', label='continuous')
plt.step(t_discrete, p_discrete, where='post', color='r', label='discrete')
plt.xlabel('time (h)')
plt.ylabel('power (kW)')
plt.legend()
plt.grid(True)
t_discrete=array([0. , 0.5, 1. , 1.5, 2. , 2.5])

Das selbe mit einem DataFrame: Zum Arbeiten mit Zeitreihen eignen sich in Python die pandas DataFrames sehr gut. Wir erstellen ein DataFrame mit den Zeitpunkten und den konstanten Leistungswerten als Spalten.

Code
import pandas as pd

df = pd.DataFrame({'time': t_discrete, 'power':p_discrete})
display(df)

# plot:
ax = df.plot(x='time', y='power', marker ='.',
            drawstyle='steps-post', figsize=(5, 3), legend=False,
            color='r', xlabel='time (h)', ylabel='power (kW)')
ax.plot(t, p, 'k-')
ax.grid(True)
time power
0 0.0 1.839397
1 0.5 3.837279
2 1.0 4.572259
3 1.5 4.842643
4 2.0 4.942112
5 2.5 4.978704

Code
# We check, if the total energies over the complete time interval [0, 3] are equal:
E_continuous = 5*(3 + 1/2 *(np.exp(-2*3) - np.exp(-2*0)))
E_discrete = np.sum(p_discrete)*dt

print(f"{E_continuous = } kWh")
print(f"{E_discrete   = } kWh")
E_continuous = 12.506196880441667 kWh
E_discrete   = 12.506196880441665 kWh

Resampling

Das Ändern des Sampling-Intervalls wird als Resampling bezeichnet und wird am einfachsten mit dem DateFrame-Methode resample durchgeführt. Dazu muss das DataFrame einen Index haben, der die Zeitpunkte als datetime-Objekte enthält.

Code
datetime_vector = pd.Timestamp('2023-12-02 14:00:00') + pd.to_timedelta(t_discrete, unit='h')
print(datetime_vector)
DatetimeIndex(['2023-12-02 14:00:00', '2023-12-02 14:30:00',
               '2023-12-02 15:00:00', '2023-12-02 15:30:00',
               '2023-12-02 16:00:00', '2023-12-02 16:30:00'],
              dtype='datetime64[ns]', freq=None)
Code
df_30Min = pd.DataFrame(p_discrete, index=datetime_vector, columns=['power'])
df_30Min
power
2023-12-02 14:00:00 1.839397
2023-12-02 14:30:00 3.837279
2023-12-02 15:00:00 4.572259
2023-12-02 15:30:00 4.842643
2023-12-02 16:00:00 4.942112
2023-12-02 16:30:00 4.978704
Code
df_30Min.plot(figsize=(5, 3), drawstyle='steps-post', grid=True, marker ='.',
              legend=False, color='r', xlabel='time (h)', ylabel='power (kW)');

Code
# check if energy is conserved:
df_30Min.sum()*dt
power    12.506197
dtype: float64

Downsampling

Code
# downsampling from 30 min to 1 h
df_1H = df_30Min.resample(rule='1H').mean()  # mean to keep the energy constant!
df_1H
power
2023-12-02 14:00:00 2.838338
2023-12-02 15:00:00 4.707451
2023-12-02 16:00:00 4.960408
Code
df_1H.plot(figsize=(5, 3), drawstyle='steps-post', grid=True, marker ='.',
           legend=False, color='r', xlabel='time (h)', ylabel='power (kW)');

Code
# check if energy is conserved:
dt = 1  # h
df_1H.sum()*dt
power    12.506197
dtype: float64

Upsampling

Code
# upsampling from 30 min to 15 min
df_15Min = df_30Min.resample(rule='15Min').ffill()
df_15Min
power
2023-12-02 14:00:00 1.839397
2023-12-02 14:15:00 1.839397
2023-12-02 14:30:00 3.837279
2023-12-02 14:45:00 3.837279
2023-12-02 15:00:00 4.572259
2023-12-02 15:15:00 4.572259
2023-12-02 15:30:00 4.842643
2023-12-02 15:45:00 4.842643
2023-12-02 16:00:00 4.942112
2023-12-02 16:15:00 4.942112
2023-12-02 16:30:00 4.978704
Code
# Add a value at the end of the time series!
df_15Min.loc['2023-12-02 16:45:00', 'power'] = 4.978704
df_15Min
power
2023-12-02 14:00:00 1.839397
2023-12-02 14:15:00 1.839397
2023-12-02 14:30:00 3.837279
2023-12-02 14:45:00 3.837279
2023-12-02 15:00:00 4.572259
2023-12-02 15:15:00 4.572259
2023-12-02 15:30:00 4.842643
2023-12-02 15:45:00 4.842643
2023-12-02 16:00:00 4.942112
2023-12-02 16:15:00 4.942112
2023-12-02 16:30:00 4.978704
2023-12-02 16:45:00 4.978704
Code
df_15Min.plot(figsize=(5, 3), drawstyle='steps-post', grid=True, marker ='.',
              legend=False, color='r', xlabel='time (h)', ylabel='power (kW)');

Code
# check if energy is conserved:
dt = 0.25  # h
df_15Min.sum()*dt
power    12.506197
dtype: float64

Übung

Der Lastgang des Verbrauchers A ist in 15-Minuten-Schritten durch die folgenden kW-Werte gegeben: 30, 12, 10, 45, 50, 20, 40, 10. Der Lastgang des Verbrauchers B ist in 30-Minuten-Schritten durch die folgenden kW-Werte gegeben: 0, 70, 0, 90.

  1. Erstellen Sie ein DataFrame, das den Gesamtlastgang der beiden Verbraucher in 15-Minuten-Schritten enthält.
  2. Wie groß ist die maximale, absolute Leistungsänderung des Gesamtlastgangs?

Hinweis: Verwenden Sie die pandas-Funktion pd.date_range um einen DataFrame-Index mit gewünschten Zeitpunkten zu erstellen.