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

multi_period   A

Complexity

Total Complexity 0

Size/Duplication

Total Lines 275
Duplicated Lines 0 %

Importance

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