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

multi_period   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 5
eloc 215
dl 0
loc 375
rs 10
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A expand_energy_prices() 0 19 3
A lifetime_adjusted() 0 2 1
A discount_rate_adjusted() 0 2 1
1
# -*- coding: utf-8 -*-
2
import logging
3
import warnings
4
5
import matplotlib.pyplot as plt
6
import pandas as pd
7
import tsam.timeseriesaggregation as tsam
8
from cost_data import energy_prices
9
from cost_data import investment_costs
10
from oemof.tools import debugging
11
from oemof.tools import logger
12
from shared import prepare_input_data
13
14
from oemof import solph
15
from oemof.solph import Bus
16
from oemof.solph import EnergySystem
17
from oemof.solph import Flow
18
from oemof.solph import Investment
19
from oemof.solph import Model
20
from oemof.solph import Results
21
from oemof.solph import components as cmp
22
23
24
# ---------------- some helper functions --------------------------------------
25
def lifetime_adjusted(lifetime, investment_period_length_in_years):
26
    return int(lifetime / investment_period_length_in_years)
27
28
29
def discount_rate_adjusted(discount_rate, investment_period_length_in_years):
30
    return (1 + discount_rate) ** investment_period_length_in_years - 1
31
32
33
def expand_energy_prices(tindex_agg_full, prices):
34
    years_in_index = sorted(set(tindex_agg_full.year))
35
    years_available = set(prices.index)
36
37
    # Strict check: all years in the index must be present in the table
38
    missing = [y for y in years_in_index if y not in years_available]
39
    if missing:
40
        raise KeyError(f"Missing prices for years in index: {missing}")
41
42
    # Build a year->price lookup and vectorized map
43
    s = pd.DataFrame()
44
    for col in prices.columns:
45
        year_prices = prices[col]
46
        s[col] = pd.Series(
47
            pd.Series(tindex_agg_full.year).map(year_prices).values,
48
            index=tindex_agg_full,
49
            name=col,
50
        )
51
    return s
52
53
54
# -----------------------------------------------------------------------------
55
warnings.filterwarnings(
56
    "ignore", category=debugging.ExperimentalFeatureWarning
57
)
58
logger.define_logging()
59
60
# ---------- read cost data ---------------------------------------------------
61
62
investment_costs = investment_costs()
63
prices = energy_prices()
64
65
# ---------- read time series data and resample--------------------------------
66
67
# read data
68
df_temperature, df_energy = prepare_input_data(plot_resampling=False)
69
70
# resample to one hour
71
df_temperature = df_temperature.resample("1 h").mean()
72
df_energy = df_energy.resample("1 h").mean()
73
74
# create data as one DataFrame
75
time_series_data_full = pd.concat([df_temperature, df_energy], axis=1)
76
77
# drop unnecessary columns and time steps of previous year
78
time_series_data_full = time_series_data_full.drop(
79
    columns=["Air Temperature (°C)", "heat demand (kWh)"]
80
).drop(time_series_data_full.index[0])
81
82
# convert untis from W to kW
83
time_series_data_full = time_series_data_full / 1000
84
time_series_data_full = time_series_data_full.rename(
85
    columns={
86
        "heat demand (W)": "heat demand (kW)",
87
        "electricity demand (W)": "electricity demand (kW)",
88
        "PV (W)": "PV (kW)",
89
    }
90
)
91
92
# -------------- Clustering of input time-series with TSAM --------------------
93
typical_periods = 40
94
hours_per_period = 24
95
96
aggregation = tsam.TimeSeriesAggregation(
97
    timeSeries=time_series_data_full.iloc[:8760],
98
    noTypicalPeriods=typical_periods,
99
    hoursPerPeriod=hours_per_period,
100
    clusterMethod="k_means",
101
    sortValues=False,
102
    rescaleClusterPeriods=False,
103
)
104
aggregation.createTypicalPeriods()
105
106
# create a time index for the aggregated time series
107
tindex_agg = pd.date_range(
108
    "2025-01-01", periods=typical_periods * hours_per_period, freq="h"
109
)
110
111
# ------------ create timeindex etc. for multiperiod --------------------------
112
# Note:
113
# originally the data provided is for investment periods of 5 years each
114
# so years = [2025, 2030, 2035, 2040, 2045]
115
# this was causing a bug in the mulit period calculation of the fixed_costs in
116
# the INVESTFLOWS, therefore years is set to [2025, 2026, 2027, 2028, 2029] in
117
# this eaxample this will be changed, when the bug is fixed
118
119
# list with years in which investment is possible
120
years = [2025, 2026, 2027, 2028, 2029]
121
122
# create a time index for the whole model
123
# Create a list of shifted copies of the original index,
124
# one per investment year
125
base_year = years[0]
126
shifted = [tindex_agg + pd.DateOffset(years=(y - base_year)) for y in years]
127
128
# Concatenate them into one DatetimeIndex
129
tindex_agg_full = shifted[0]
130
for s in shifted[1:]:
131
    tindex_agg_full = tindex_agg_full.append(s)
132
133
print("------- Time Index of Multi-Period Model --------")
134
print("time index: ", tindex_agg_full)
135
print("-------------------------------------------------")
136
137
# create the list of investent periods for the model
138
investment_periods = [
139
    tindex_agg + pd.DateOffset(years=i) for i in range(len(years))
140
]
141
142
143
print("------- Priods of Multi-Period Model --------")
144
print("Investment periods: ", investment_periods)
145
print("---------------------------------------------")
146
147
# create parameters for time series aggregation in oemof-solph
148
# with one dict per year
149
tsa_parameters = [
150
    {
151
        "timesteps_per_period": aggregation.hoursPerPeriod,
152
        "order": aggregation.clusterOrder,
153
        "timeindex": tindex_agg + pd.DateOffset(years=i),
154
    }
155
    for i in range(len(years))
156
]
157
158
timeincrement = [1] * (len(tindex_agg_full))
159
160
# ------------------ calculate discount rate and lifetime ---------------------
161
# the annuity has to be calculated for a period of 5 years
162
investment_period_length_in_years = 5
163
164
# ------------------ create energy system -------------------------------------
165
es = EnergySystem(
166
    timeindex=tindex_agg_full,
167
    timeincrement=timeincrement,
168
    periods=investment_periods,
169
    tsa_parameters=tsa_parameters,
170
    infer_last_interval=False,
171
)
172
173
bus_el = Bus(label="electricity")
174
bus_heat = Bus(label="heat")
175
bus_gas = Bus(label="gas")
176
es.add(bus_el, bus_heat, bus_gas)
177
178
pv = cmp.Source(
179
    label="PV",
180
    outputs={
181
        bus_el: Flow(
182
            fix=pd.concat(
183
                [aggregation.typicalPeriods["PV (kW)"]] * len(years),
184
                ignore_index=True,
185
            ),
186
            nominal_capacity=Investment(
187
                ep_costs=investment_costs[("pv", "specific_costs [Eur/kW]")],
188
                lifetime=lifetime_adjusted(
189
                    20, investment_period_length_in_years
190
                ),
191
                fixed_costs=investment_costs[("pv", "fixed_costs [Eur]")]
192
                / lifetime_adjusted(20, investment_period_length_in_years),
193
                overall_maximum=10,
194
            ),
195
        )
196
    },
197
)
198
es.add(pv)
199
200
# Battery
201
battery = cmp.GenericStorage(
202
    label="Battery",
203
    inputs={bus_el: Flow()},
204
    outputs={bus_el: Flow()},
205
    nominal_capacity=Investment(
206
        ep_costs=investment_costs[("battery", "specific_costs [Eur/kWh]")],
207
        lifetime=lifetime_adjusted(10, investment_period_length_in_years),
208
    ),
209
    min_storage_level=0.0,
210
    max_storage_level=1.0,
211
    loss_rate=0.001,  # 0.1%/h
212
    inflow_conversion_factor=0.95,  # Lade-Wirkungsgrad
213
    outflow_conversion_factor=0.95,  # Entlade-Wirkungsgrad
214
)
215
es.add(battery)
216
217
# Electricity demand
218
house_sink = cmp.Sink(
219
    label="Electricity demand",
220
    inputs={
221
        bus_el: Flow(
222
            fix=pd.concat(
223
                [aggregation.typicalPeriods["electricity demand (kW)"]]
224
                * len(years),
225
                ignore_index=True,
226
            ),
227
            nominal_capacity=1.0,
228
        )
229
    },
230
)
231
es.add(house_sink)
232
233
# Electric vehicle demand
234
# wallbox_sink = cmp.Sink(
235
#     label="Electric Vehicle",
236
#     inputs={
237
#         bus_el: Flow(
238
#             fix=pd.concat(
239
#                 [aggregation.typicalPeriods["ev_charge_kW"]] * len(years),
240
#                 ignore_index=True,
241
#             ),
242
#             nominal_capacity=1.0,
243
#         )
244
#     },
245
# )
246
# es.add(wallbox_sink)
247
248
# Heat Pump
249
hp = cmp.Converter(
250
    label="Heat pump",
251
    inputs={bus_el: Flow()},
252
    outputs={
253
        bus_heat: Flow(
254
            nominal_capacity=Investment(
255
                ep_costs=investment_costs[
256
                    ("heat pump", "specific_costs [Eur/kW]")
257
                ],
258
                lifetime=lifetime_adjusted(
259
                    20, investment_period_length_in_years
260
                ),
261
                fixed_costs=investment_costs[
262
                    ("heat pump", "fixed_costs [Eur]")
263
                ]
264
                / lifetime_adjusted(20, investment_period_length_in_years),
265
            )
266
        )
267
    },
268
    conversion_factors={bus_heat: 3.5},
269
)
270
es.add(hp)
271
272
# Gas Boiler
273
gas_boiler = cmp.Converter(
274
    label="Gas boiler",
275
    inputs={bus_gas: Flow()},
276
    outputs={
277
        bus_heat: Flow(
278
            nominal_capacity=Investment(
279
                ep_costs=investment_costs[
280
                    ("gas boiler", "specific_costs [Eur/kW]")
281
                ],
282
                lifetime=lifetime_adjusted(
283
                    20, investment_period_length_in_years
284
                ),
285
                fixed_costs=investment_costs[
286
                    ("gas boiler", "fixed_costs [Eur]")
287
                ]
288
                / lifetime_adjusted(20, investment_period_length_in_years),
289
                existing=3.5,
290
                age=2,
291
            )
292
        )
293
    },
294
    conversion_factors={bus_heat: 0.9},
295
)
296
es.add(gas_boiler)
297
298
# Heat demand
299
heat_sink = cmp.Sink(
300
    label="Heat demand",
301
    inputs={
302
        bus_heat: Flow(
303
            fix=pd.concat(
304
                [aggregation.typicalPeriods["heat demand (kW)"]] * len(years),
305
                ignore_index=True,
306
            ),
307
            nominal_capacity=1.0,
308
        )
309
    },
310
)
311
es.add(heat_sink)
312
313
# calculate prices for each time step
314
p = expand_energy_prices(tindex_agg_full, prices)
315
316
grid_import = cmp.Source(
317
    label="Grid import",
318
    outputs={bus_el: Flow(variable_costs=p["electricity_prices [Eur/kWh]"])},
319
)
320
es.add(grid_import)
321
322
# Grid feed-in
323
feed_in = cmp.Sink(
324
    label="Grid Feed-in",
325
    inputs={bus_el: Flow(variable_costs=p["pv_feed_in [Eur/kWh]"])},
326
)
327
es.add(feed_in)
328
329
# Gas grid
330
gas_grid = cmp.Source(
331
    label="Gas grid",
332
    outputs={bus_gas: Flow(variable_costs=p["gas_prices [Eur/kWh]"])},
333
)
334
es.add(gas_grid)
335
336
# Create Model and solve it
337
logging.info("Creating Model...")
338
m = Model(es)
339
logging.info("Solving Model...")
340
m.solve(
341
    solver="gurobi",
342
    solve_kwargs={"tee": True},
343
)
344
345
# ----------------- Post Processing -------------------------------------------
346
347
# Create Results
348
results = Results(m)
349
350
# invest and total installed capacity
351
invest = results["invest"]
352
total = results["total"]
353
354
years = [2025, 2030, 2035, 2040, 2045]
355
invest.index = years
356
total.index = years
357
358
fig, (ax1, ax2) = plt.subplots(
359
    2, 1, figsize=(10, 7), sharex=True, constrained_layout=True
360
)
361
362
total.plot(kind="bar", ax=ax1)
363
ax1.set_title("Total installed capacity")
364
ax1.set_ylabel("kW")
365
ax1.grid(True, linewidth=0.3, alpha=0.6)
366
ax1.legend().set_visible(False)
367
368
invest.plot(kind="bar", ax=ax2)
369
ax2.set_title("Invested capacity")
370
ax2.set_xlabel("Years")
371
ax2.set_ylabel("kW")
372
ax2.grid(True, linewidth=0.3, alpha=0.6)
373
374
plt.show()
375
376
# Note: if you want to extract values for the flow, you have to change
377
# to_df() in the class Results() in this way:
378
#
379
#     # overwrite known indexes
380
#     index_type = tuple(dataset.index_set().subsets())[-1].name
381
#     match index_type:
382
#         case "TIMEPOINTS":
383
#             df.index = self.timeindex
384
#         case "TIMESTEPS":
385
#             # df.index = self.timeindex[:-1]
386
#             df.index = self.timeindex
387
#         case _:
388
#             df.index = df.index.get_level_values(-1)
389
#
390
# otherwise including the storage leads to Length mismatch Value Error
391
# why: no clue, something with TIMESTEPS and TIMEPOINTS for storage
392
#
393
# if you changed this you can use
394
# flows = results["flow"]
395
# to look at the time series
396