Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
db058d4
Add raw BDEW profiles
p-snft Apr 4, 2025
7d154b6
Split dynamisation_function function from ElecSlp class
p-snft Apr 4, 2025
da3139e
Set bdew datapath outside ElecSlp class
p-snft Apr 4, 2025
1c2e3cb
Merge branch 'revision/individual_eslp_classes' into feature/bdew-loa…
p-snft Apr 5, 2025
1aa34c3
Add stub for BDEW25 profiles
p-snft Apr 7, 2025
ea4bbda
Merge branch 'dev' into feature/bdew-load-profiles-2025
p-snft Apr 7, 2025
65c203d
Start structuring SLP25 generation along SLP00 generation
p-snft Apr 7, 2025
dc97710
Finish static versions of SLP25
p-snft Apr 7, 2025
b5f5372
Improve imports
p-snft Apr 7, 2025
4581830
Add dynamic BDEW25 profiles
p-snft Apr 7, 2025
6258eb0
Fix PV25 file naming
p-snft Apr 7, 2025
ce065cf
Define BDEW25Profile init before other class members
p-snft Apr 7, 2025
69f767b
Fix BDEW25 scaling
p-snft Apr 7, 2025
cb68b4d
Add short BDEW25 example
p-snft Apr 7, 2025
f986772
Test resolution of BDEW25Profile index
p-snft Apr 7, 2025
605f9fb
Adhere to Black
p-snft Apr 7, 2025
f4f65bf
Add Holiday handling to SLP25
p-snft Apr 8, 2025
c34bd67
Add docstrings for and allow resampling of SLP25
p-snft Apr 8, 2025
d0737a6
Show different resolutions in SLP25 example
p-snft Apr 8, 2025
9f3e06b
Update to stable Pandas API
p-snft Apr 8, 2025
056724e
Fix typo in docstring
p-snft Apr 8, 2025
a742657
Add tests for 2025 BDEW SLPs
p-snft Apr 8, 2025
0ae7e22
Adhere to Black
p-snft Apr 8, 2025
1b036fb
Test abstract BDEW25 profiles cannot be instanciated
p-snft Apr 8, 2025
6c42552
Do not resample SLP25 if resolution is correct
p-snft Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions examples/bdew_slp25.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
SPDX-FileCopyrightText: Deutsches Zentrum für Luft- und Raumfahrt
SPDX-FileCopyrightText: Patrik Schönfeldt


SPDX-License-Identifier: MIT
"""

import datetime

import matplotlib.pyplot as plt
import pandas as pd


from demandlib import bdew

index15m = pd.date_range(
start="2020-01-01 00:00",
end="2020-01-31 23:45",
freq="15min",
)
index1m = pd.date_range(
start="2020-01-01 00:00",
end="2020-01-31 23:45",
freq="1min",
)
index3h = pd.date_range(
start="2020-01-01 00:00",
end="2020-01-31 23:45",
freq="3h",
)

holidays = [
datetime.date(2020, 1, 1),
datetime.date(2020, 1, 2),
datetime.date(2020, 1, 3),
datetime.date(2020, 1, 4),
datetime.date(2020, 1, 5),
]

h25_holidays = bdew.H25(index1m, holidays)
h25_std = bdew.H25(index15m)
h25_3h = bdew.H25(index3h)

plt.step(h25_holidays.index, h25_holidays, label="Holidays", where="post")
plt.step(h25_std.index, h25_std, label="Standard", where="post")
plt.step(h25_3h.index, h25_3h, label="Three hours", where="post")
plt.legend()

plt.show()
8 changes: 8 additions & 0 deletions src/demandlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
__version__ = "0.2.2a1"

from . import bdew
from . import vdi

__all__ = [
"bdew",
"vdi",
]
19 changes: 17 additions & 2 deletions src/demandlib/bdew/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,20 @@
SPDX-License-Identifier: MIT
"""

from .elec_slp import ElecSlp # noqa: F401
from .heat_building import HeatBuilding # noqa: F401
from ._profiles25 import G25
from ._profiles25 import H25
from ._profiles25 import L25
from ._profiles25 import P25
from ._profiles25 import S25
from .elec_slp import ElecSlp
from .heat_building import HeatBuilding

__all__ = [
"ElecSlp",
"HeatBuilding",
"G25",
"H25",
"L25",
"P25",
"S25",
]
250 changes: 250 additions & 0 deletions src/demandlib/bdew/_profiles25.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
"""
Implementation of the 2025 BDEW standard load profiles:
* G25 (static), industrial profile based on 2022 and 2023 data
* H25 (dynamic), household profile based on 2018 and 2019 data
* L25 (static), farm profile based on previous L0 profile
* P25 (dynamic), profile for households with PV based on 2022 and 2023 data
* S25 (dynamic), profile for households with BES based on 2022 and 2023 data

SPDX-FileCopyrightText: Deutsches Zentrum für Luft- und Raumfahrt
SPDX-FileCopyrightText: Patrik Schönfeldt

SPDX-License-Identifier: MIT
"""

import os

import numpy as np
import pandas as pd

from demandlib.tools import set_holidays_in_df

_bdew_datapath = os.path.join(os.path.dirname(__file__), "bdew_data")


class BDEW25Profile(pd.Series):
"""
Electrical standard load profiles based on the BDEW method (2025 revision)

Parameters
----------
timeindex : pd.DatetimeIndex
Time index to produce the SLP for. Only constant step-size is
supported, i.e. timeindex.freq needs to be defined. If time
steps of 15 minutes are used, that are aligned with the full
hour, the original SLP is reproduced. Lower resolutions will
be downsampled, higher resolutions wil be padded. (Pading is
the correct handling because the SLP defines constant values
for 15 minutes.)

Optional Parameters
-------------------
holidays : dictionary or list
The keys of the dictionary or the items of the list should be datetime
objects of the days that are holidays.
"""

def __init__(
self,
timeindex: pd.DatetimeIndex,
holidays: dict | list | None = None,
):
original_timedelta = pd.Timedelta(timeindex.freq)
if original_timedelta == pd.Timedelta("00:15:00"):
timeindex15m = timeindex
else:
timeindex15m = pd.date_range(
start=timeindex[0],
end=timeindex[-1],
freq="15min",
)
new_df = pd.DataFrame(
data={
"month": timeindex15m.month,
"weekday": timeindex15m.day_of_week + 1,
"hour": timeindex15m.hour,
"minute": timeindex15m.minute,
},
index=timeindex15m,
)

set_holidays_in_df(new_df, holidays=holidays)

new_df.replace({"weekday": [1, 2, 3, 4, 5]}, "WT", inplace=True)
new_df.replace({"weekday": [6]}, "SA", inplace=True)
new_df.replace(
{"weekday": [0, 7]}, "FT", inplace=True
) # 0 for holiday

new_df = new_df.merge(
self.raw_profile_data,
on=["month", "weekday", "hour", "minute"],
how="inner",
)
new_df.set_index(timeindex15m, inplace=True)

values = new_df.value
if original_timedelta > pd.Timedelta("00:15:00"):
values = values.resample(original_timedelta).mean()
elif original_timedelta < pd.Timedelta("00:15:00"):
values = values.reindex(timeindex, method="ffill")

super().__init__(data=values, index=timeindex)

@property
def datafile(self):
raise NotImplementedError(
"BDEW25Profile needs to be implemented,"
" please use one of its children instead."
)

@property
def raw_profile_data(self):
profile_data = pd.read_csv(
_bdew_datapath + self.datafile,
header=[0, 1],
)

profile_data.rename(
columns={
"Januar": 1,
"Februar": 2,
"März": 3,
"April": 4,
"Mai": 5,
"Juni": 6,
"Juli": 7,
"August": 8,
"September": 9,
"Oktober": 10,
"November": 11,
"Dezember": 12,
},
inplace=True,
)
del profile_data[("Unnamed: 0_level_0", "[kWh]")]

hours = [h for h in range(24) for _ in range(4)]
minutes = [m for _ in range(24) for m in range(0, 60, 15)]
serialised_data = []
for column in profile_data.columns:
serialised_data.append(
pd.DataFrame(
data={
"month": column[0],
"weekday": column[1],
"hour": hours,
"minute": minutes,
"value": 4 * profile_data[column],
}
)
)

profile_data = pd.concat(serialised_data, ignore_index=True)
return profile_data


class DynamicBDEW25Profile(BDEW25Profile):
"""
BDEW25 SLP considering the dynamisation_function.
See BDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)
self.update(self * self.dynamisation_function(timeindex))

@staticmethod
def dynamisation_function(timeindex: pd.DatetimeIndex) -> pd.Series:

# cast to float hear because of miscalculations when using integers
day_of_year = np.array(timeindex.day_of_year, dtype=float)

return pd.Series(
-3.92 * 10**-10 * day_of_year**4
+ 3.2 * 10**-7 * day_of_year**3
- 7.02 * 10**-5 * day_of_year**2
+ 0.0021 * day_of_year
+ 1.24,
index=timeindex,
)


class G25(BDEW25Profile):
"""
SLP for industries, see BDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)

@property
def datafile(self):
return "/g25.csv"


class H25(DynamicBDEW25Profile):
"""
SLP for households, see DynamicBDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)

@property
def datafile(self):
return "/h25.csv"


class L25(BDEW25Profile):
"""
SLP for farms, see BDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)

@property
def datafile(self):
return "/l25.csv"


class P25(DynamicBDEW25Profile):
"""
SLP for housholds with generic PV system,
see DynamicBDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)

@property
def datafile(self):
return "/p25.csv"


class S25(DynamicBDEW25Profile):
"""
SLP for housholds with generic PV system and battery storage,
see DynamicBDEW25Profile for more details.
"""

def __init__(
self, timeindex: pd.DatetimeIndex, holidays: dict | list | None = None
):
super().__init__(timeindex=timeindex, holidays=holidays)

@property
def datafile(self):
return "/s25.csv"
Loading