Passed
Pull Request — dev (#1226)
by Patrik
01:51
created

tsam_timeseriesaggregation   A

Complexity

Total Complexity 0

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 0
eloc 184
dl 0
loc 289
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"] = 0.0  # wird automatisch auf alle Zeitschritte gebroadcastet
60
61
# COP-Profil (konstant, später evtl. temperaturabhängig)
62
df["cop_hp"] = 3.5
63
64
df = df.resample("1h").mean()
65
#Clustering of Input time-series with TSAM
66
typical_periods = 40
67
hours_per_period = 24
68
69
aggregation = tsam.TimeSeriesAggregation(
70
    timeSeries=df.iloc[:8760],
71
    noTypicalPeriods=typical_periods,
72
    hoursPerPeriod=hours_per_period,
73
    clusterMethod="k_means",
74
    sortValues=False,
75
    rescaleClusterPeriods=False,
76
)
77
aggregation.createTypicalPeriods()
78
tindex_agg = pd.date_range(
79
    "2022-01-01", periods=typical_periods * hours_per_period , freq="H"
80
)
81
es = EnergySystem(
82
    timeindex=tindex_agg,
83
    timeincrement=[1] * len(tindex_agg),
84
    periods=[tindex_agg],
85
    tsa_parameters=[
86
        {
87
            "timesteps_per_period": aggregation.hoursPerPeriod,
88
            "order": aggregation.clusterOrder,
89
            "timeindex": aggregation.timeIndex,
90
        }
91
    ],
92
    infer_last_interval=False,
93
)
94
95
bus_el = Bus(label="electricity")
96
bus_heat = Bus(label="heat")
97
es.add(bus_el, bus_heat)
98
99
pv = cmp.Source(
100
    label="PV",
101
    outputs={
102
        bus_el: Flow(fix=pv_profile, nominal_capacity=Investment(ep_costs=400,
103
                                                                 lifetime=20,
104
                                                                 maximum=10))
105
    },
106
)
107
es.add(pv)
108
109
# Battery
110
battery = cmp.GenericStorage(
111
    label="Battery",
112
    inputs={bus_el: Flow()},
113
    outputs={bus_el: Flow()},
114
    nominal_capacity=Investment(ep_costs=500,
115
                                lifetime=20),  # kWh
116
    min_storage_level=0.0,
117
    max_storage_level=1.0,
118
    loss_rate=0.001,  # 0.1%/h
119
    inflow_conversion_factor=0.95,  # Lade-Wirkungsgrad
120
    outflow_conversion_factor=0.95,  # Entlade-Wirkungsgrad
121
)
122
es.add(battery)
123
124
# Electricity demand
125
house_sink = cmp.Sink(
126
    label="Electricity demand",
127
    inputs={bus_el: Flow(fix=aggregation.typicalPeriods["house_elec_kW"], nominal_capacity=1.0)},
128
)
129
es.add(house_sink)
130
131
# Electric vehicle demand
132
wallbox_sink = cmp.Sink(
133
    label="Electric Vehicle",
134
    inputs={bus_el: Flow(fix=aggregation.typicalPeriods["ev_charge_kW"], nominal_capacity=1.0)},
135
)
136
es.add(wallbox_sink)
137
138
# Heat Pump
139
hp = cmp.Converter(
140
    label="Heat pump",
141
    inputs={bus_el: Flow()},
142
    outputs={bus_heat: Flow(nominal_capacity=Investment(ep_costs=500,
143
                                                        lifetime=20))},
144
    conversion_factors={bus_heat: aggregation.typicalPeriods["cop_hp"]},
145
)
146
es.add(hp)
147
148
# Heat demand
149
heat_sink = cmp.Sink(
150
    label="Heat demand",
151
    inputs={bus_heat: Flow(fix=aggregation.typicalPeriods["house_heat_kW"], nominal_capacity=5.0)},
152
)
153
es.add(heat_sink)
154
155
grid_import = cmp.Source(
156
    label="Grid import", outputs={bus_el: Flow(variable_costs=0.30)}
157
)
158
es.add(grid_import)
159
160
# Grid feed-in
161
feed_in = cmp.Sink(
162
    label="Grid Feed-in", inputs={bus_el: Flow(variable_costs=-0.08)}
163
)
164
es.add(feed_in)
165
166
# Create Model and solve it
167
logging.info("Creating Model...")
168
m = Model(es)
169
logging.info("Solving Model...")
170
m.solve(solver="cbc", solve_kwargs={"tee": False})
171
172
# Create Results
173
results = solph.processing.results(m)
174
175
results_bus_el = solph.views.node(results, bus_el)
176
results_bus_heat = solph.views.node(results, bus_heat)
177
178
my_results = results_bus_el["period_scalars"]
179
180
# installed capacity of pv-system in kW
181
pv_invest_kW = (
182
    results[(pv, bus_el)]["period_scalars"]["invest"]
183
)
184
185
# installed capacity of storage in kWh
186
storage_invest_kWh = (
187
    results[(battery, None)]["period_scalars"]["invest"]
188
)
189
190
191
# installed capacity of hp in kW
192
hp_invest_kW = (
193
    results[(hp, bus_heat)]["period_scalars"]["invest"]
194
)
195
196
investments = pd.Series({
197
    "PV (kW)": pv_invest_kW.iloc[0],
198
    "Battery (kWh)": storage_invest_kWh.iloc[0],
199
    "HP (kW_th)": hp_invest_kW.iloc[0],
200
})
201
investments.squeeze().plot(kind="bar")
202
203
day = 186  # day of the year
204
n = 2
205
206
# Concatenate flows:
207
flows = pd.concat([flow["sequences"] for flow in results.values()], axis=1)
208
flows.columns = list(results.keys())
209
210
soc = flows[battery,None]
211
flows = flows.drop(columns=[(battery, None)])
212
213
supply = flows[[c for c in flows.columns
214
                if getattr(c[1], "label", None) == "electricity"]].copy()
215
supply.columns = [c[0].label for c in supply.columns]
216
217
218
demand = flows[[c for c in flows.columns
219
                if getattr(c[0], "label", None) == "electricity"]].copy()
220
demand.columns = [c[1].label for c in demand.columns]
221
222
# A plot from GPT :-)
223
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
224
# Top: Electricity bus — supply vs. demand (negative stack), net balance
225
sup_handles = axes[0].stackplot(
226
    supply.index,
227
    *[supply[c] for c in supply.columns],
228
    labels=list(supply.columns),
229
    alpha=0.8,
230
)
231
dem_handles = axes[0].stackplot(
232
    demand.index,
233
    *[-demand[c] for c in demand.columns],
234
    labels=list(demand.columns),
235
    alpha=0.7,
236
)
237
238
net = supply.sum(axis=1) - demand.sum(axis=1)
239
(net_line,) = axes[0].plot(
240
    net.index, net, color="k", linewidth=1.3, label="Net balance"
241
)
242
axes[0].axhline(0, color="gray", linestyle="--", linewidth=0.8)
243
axes[0].set_ylabel("Power [kW]")
244
axes[0].set_title("Electricity bus: supply (positive) vs demand (negative)")
245
246
# Legend combining both stacks and net line
247
handles = sup_handles + dem_handles + [net_line]
248
labels = list(supply.columns) + list(demand.columns) + ["Net balance"]
249
axes[0].legend(handles, labels, ncol=2, fontsize=9, loc="upper left")
250
251
# Optional: overlay SOC on right axis
252
if soc is not None:
253
    ax2 = axes[0].twinx()
254
    ax2.plot(
255
        soc.index, soc, color="tab:purple", linewidth=1.2, label="Battery SOC"
256
    )
257
    ax2.set_ylabel("Energy [kWh]")
258
    ax2.legend(loc="upper right")
259
260
261
hp_heat = flows[[c for c in flows.columns
262
                if getattr(c[1], "label", None) == "heat"]].copy()
263
hp_heat.columns = [c[0].label for c in hp_heat.columns]
264
265
heat_dem = flows[[c for c in flows.columns
266
                if getattr(c[0], "label", None) == "heat"]].copy()
267
heat_dem.columns = [c[1].label for c in heat_dem.columns]
268
269
axes[1].plot(hp_heat.index, hp_heat, label="HP heat output", linewidth=2)
270
axes[1].plot(
271
    heat_dem.index, heat_dem, label="Heat demand", linewidth=2, linestyle="--"
272
)
273
axes[1].fill_between(
274
    heat_dem.index,
275
    hp_heat.squeeze(),
276
    heat_dem.squeeze(),
277
    where=(heat_dem.squeeze() > hp_heat.squeeze()),
278
    color="tab:red",
279
    alpha=0.2,
280
    label="Unmet heat",
281
)
282
axes[1].set_ylabel("Heat [kW]")
283
axes[1].set_title("Heat bus")
284
axes[1].legend(loc="upper left")
285
axes[1].set_xlabel("Time")
286
287
plt.tight_layout()
288
plt.show()
289