Passed
Pull Request — dev (#1226)
by Uwe
01:53
created

tsam_timeseriesaggregation   A

Complexity

Total Complexity 0

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 0
eloc 195
dl 0
loc 307
rs 10
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
3
"""
4
SPDX-FileCopyrightText: Patrik Schönfeldt
5
SPDX-FileCopyrightText: DLR e.V.
6
7
SPDX-License-Identifier: MIT
8
"""
9
10
import logging
11
import warnings
12
from pathlib import Path
13
import tsam.timeseriesaggregation as tsam
14
15
import numpy as np
16
import pandas as pd
17
from matplotlib import pyplot as plt
18
from oemof.tools import debugging
19
from oemof.tools import logger
20
21
from oemof.solph import Bus
22
from oemof.solph import EnergySystem
23
from oemof.solph import Flow
24
from oemof.solph import Investment
25
from oemof.solph import Model
26
from oemof.solph import Results
27
from oemof.solph import components as cmp
28
from oemof import solph
29
30
warnings.filterwarnings(
31
    "ignore", category=debugging.ExperimentalFeatureWarning
32
)
33
logger.define_logging()
34
35
file_path = Path(__file__).parent
36
37
df = pd.read_csv(
38
    Path(file_path, "energy.csv"),
39
)
40
df["time"] = pd.to_datetime(df["Unix Epoch"], unit="s")
41
# time als Index setzen
42
df = df.set_index("time")
43
df = df.drop(columns=["Unix Epoch"])
44
print(df)
45
46
time_index = df.index
47
48
# Dummy pv profile
49
h = np.arange(len(time_index))
50
pv_profile = df["PV (W)"]
51
52
# Dummy electricity profile
53
df["house_elec_kW"] = 0.3 + 0.7 * np.random.rand(len(time_index))
54
55
# Dummy heat profile
56
df["house_heat_kW"] = 0.3 + 0.7 * np.random.rand(len(time_index))
57
58
# EV-Ladeprofil
59
df["ev_charge_kW"] = (
60
    0.0  # wird automatisch auf alle Zeitschritte gebroadcastet
61
)
62
63
# COP-Profil (konstant, später evtl. temperaturabhängig)
64
df["cop_hp"] = 3.5
65
66
df = df.resample("1h").mean()
67
# Clustering of Input time-series with TSAM
68
typical_periods = 40
69
hours_per_period = 24
70
71
aggregation = tsam.TimeSeriesAggregation(
72
    timeSeries=df.iloc[:8760],
73
    noTypicalPeriods=typical_periods,
74
    hoursPerPeriod=hours_per_period,
75
    clusterMethod="k_means",
76
    sortValues=False,
77
    rescaleClusterPeriods=False,
78
)
79
aggregation.createTypicalPeriods()
80
tindex_agg = pd.date_range(
81
    "2022-01-01", periods=typical_periods * hours_per_period, freq="H"
82
)
83
es = EnergySystem(
84
    timeindex=tindex_agg,
85
    timeincrement=[1] * len(tindex_agg),
86
    periods=[tindex_agg],
87
    tsa_parameters=[
88
        {
89
            "timesteps_per_period": aggregation.hoursPerPeriod,
90
            "order": aggregation.clusterOrder,
91
            "timeindex": aggregation.timeIndex,
92
        }
93
    ],
94
    infer_last_interval=False,
95
)
96
97
bus_el = Bus(label="electricity")
98
bus_heat = Bus(label="heat")
99
es.add(bus_el, bus_heat)
100
101
pv = cmp.Source(
102
    label="PV",
103
    outputs={
104
        bus_el: Flow(
105
            fix=pv_profile,
106
            nominal_capacity=Investment(ep_costs=400, lifetime=20, maximum=10),
107
        )
108
    },
109
)
110
es.add(pv)
111
112
# Battery
113
battery = cmp.GenericStorage(
114
    label="Battery",
115
    inputs={bus_el: Flow()},
116
    outputs={bus_el: Flow()},
117
    nominal_capacity=Investment(ep_costs=500, lifetime=20),  # kWh
118
    min_storage_level=0.0,
119
    max_storage_level=1.0,
120
    loss_rate=0.001,  # 0.1%/h
121
    inflow_conversion_factor=0.95,  # Lade-Wirkungsgrad
122
    outflow_conversion_factor=0.95,  # Entlade-Wirkungsgrad
123
)
124
es.add(battery)
125
126
# Electricity demand
127
house_sink = cmp.Sink(
128
    label="Electricity demand",
129
    inputs={
130
        bus_el: Flow(
131
            fix=aggregation.typicalPeriods["house_elec_kW"],
132
            nominal_capacity=1.0,
133
        )
134
    },
135
)
136
es.add(house_sink)
137
138
# Electric vehicle demand
139
wallbox_sink = cmp.Sink(
140
    label="Electric Vehicle",
141
    inputs={
142
        bus_el: Flow(
143
            fix=aggregation.typicalPeriods["ev_charge_kW"],
144
            nominal_capacity=1.0,
145
        )
146
    },
147
)
148
es.add(wallbox_sink)
149
150
# Heat Pump
151
hp = cmp.Converter(
152
    label="Heat pump",
153
    inputs={bus_el: Flow()},
154
    outputs={
155
        bus_heat: Flow(nominal_capacity=Investment(ep_costs=500, lifetime=20))
156
    },
157
    conversion_factors={bus_heat: aggregation.typicalPeriods["cop_hp"]},
158
)
159
es.add(hp)
160
161
# Heat demand
162
heat_sink = cmp.Sink(
163
    label="Heat demand",
164
    inputs={
165
        bus_heat: Flow(
166
            fix=aggregation.typicalPeriods["house_heat_kW"],
167
            nominal_capacity=5.0,
168
        )
169
    },
170
)
171
es.add(heat_sink)
172
173
grid_import = cmp.Source(
174
    label="Grid import", outputs={bus_el: Flow(variable_costs=0.30)}
175
)
176
es.add(grid_import)
177
178
# Grid feed-in
179
feed_in = cmp.Sink(
180
    label="Grid Feed-in", inputs={bus_el: Flow(variable_costs=-0.08)}
181
)
182
es.add(feed_in)
183
184
# Create Model and solve it
185
logging.info("Creating Model...")
186
m = Model(es)
187
logging.info("Solving Model...")
188
m.solve(solver="cbc", solve_kwargs={"tee": False})
189
190
# Create Results
191
results = solph.processing.results(m)
192
193
results_bus_el = solph.views.node(results, bus_el)
194
results_bus_heat = solph.views.node(results, bus_heat)
195
196
my_results = results_bus_el["period_scalars"]
197
198
# installed capacity of pv-system in kW
199
pv_invest_kW = results[(pv, bus_el)]["period_scalars"]["invest"]
200
201
# installed capacity of storage in kWh
202
storage_invest_kWh = results[(battery, None)]["period_scalars"]["invest"]
203
204
205
# installed capacity of hp in kW
206
hp_invest_kW = results[(hp, bus_heat)]["period_scalars"]["invest"]
207
208
investments = pd.Series(
209
    {
210
        "PV (kW)": pv_invest_kW.iloc[0],
211
        "Battery (kWh)": storage_invest_kWh.iloc[0],
212
        "HP (kW_th)": hp_invest_kW.iloc[0],
213
    }
214
)
215
investments.squeeze().plot(kind="bar")
216
217
day = 186  # day of the year
218
n = 2
219
220
# Concatenate flows:
221
flows = pd.concat([flow["sequences"] for flow in results.values()], axis=1)
222
flows.columns = list(results.keys())
223
224
soc = flows[battery, None]
225
flows = flows.drop(columns=[(battery, None)])
226
227
supply = flows[
228
    [c for c in flows.columns if getattr(c[1], "label", None) == "electricity"]
229
].copy()
230
supply.columns = [c[0].label for c in supply.columns]
231
232
233
demand = flows[
234
    [c for c in flows.columns if getattr(c[0], "label", None) == "electricity"]
235
].copy()
236
demand.columns = [c[1].label for c in demand.columns]
237
238
# A plot from GPT :-)
239
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
240
# Top: Electricity bus — supply vs. demand (negative stack), net balance
241
sup_handles = axes[0].stackplot(
242
    supply.index,
243
    *[supply[c] for c in supply.columns],
244
    labels=list(supply.columns),
245
    alpha=0.8,
246
)
247
dem_handles = axes[0].stackplot(
248
    demand.index,
249
    *[-demand[c] for c in demand.columns],
250
    labels=list(demand.columns),
251
    alpha=0.7,
252
)
253
254
net = supply.sum(axis=1) - demand.sum(axis=1)
255
(net_line,) = axes[0].plot(
256
    net.index, net, color="k", linewidth=1.3, label="Net balance"
257
)
258
axes[0].axhline(0, color="gray", linestyle="--", linewidth=0.8)
259
axes[0].set_ylabel("Power [kW]")
260
axes[0].set_title("Electricity bus: supply (positive) vs demand (negative)")
261
262
# Legend combining both stacks and net line
263
handles = sup_handles + dem_handles + [net_line]
264
labels = list(supply.columns) + list(demand.columns) + ["Net balance"]
265
axes[0].legend(handles, labels, ncol=2, fontsize=9, loc="upper left")
266
267
# Optional: overlay SOC on right axis
268
if soc is not None:
269
    ax2 = axes[0].twinx()
270
    ax2.plot(
271
        soc.index, soc, color="tab:purple", linewidth=1.2, label="Battery SOC"
272
    )
273
    ax2.set_ylabel("Energy [kWh]")
274
    ax2.legend(loc="upper right")
275
276
277
hp_heat = flows[
278
    [c for c in flows.columns if getattr(c[1], "label", None) == "heat"]
279
].copy()
280
hp_heat.columns = [c[0].label for c in hp_heat.columns]
281
282
heat_dem = flows[
283
    [c for c in flows.columns if getattr(c[0], "label", None) == "heat"]
284
].copy()
285
heat_dem.columns = [c[1].label for c in heat_dem.columns]
286
287
axes[1].plot(hp_heat.index, hp_heat, label="HP heat output", linewidth=2)
288
axes[1].plot(
289
    heat_dem.index, heat_dem, label="Heat demand", linewidth=2, linestyle="--"
290
)
291
axes[1].fill_between(
292
    heat_dem.index,
293
    hp_heat.squeeze(),
294
    heat_dem.squeeze(),
295
    where=(heat_dem.squeeze() > hp_heat.squeeze()),
296
    color="tab:red",
297
    alpha=0.2,
298
    label="Unmet heat",
299
)
300
axes[1].set_ylabel("Heat [kW]")
301
axes[1].set_title("Heat bus")
302
axes[1].legend(loc="upper left")
303
axes[1].set_xlabel("Time")
304
305
plt.tight_layout()
306
plt.show()
307