Skip to content

Commit

Permalink
Merge pull request #69 from oemof/dev
Browse files Browse the repository at this point in the history
Release v0.2.1
  • Loading branch information
p-snft authored Aug 6, 2024
2 parents 3a950f0 + a9a7b44 commit 5ab05bf
Show file tree
Hide file tree
Showing 7 changed files with 453 additions and 53 deletions.
10 changes: 5 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
Changelog
=========

v0.2.1 (yyyy-mm-dd)
v0.2.1 (2024-08-06)
+++++++++++++++++++++++++

New features
############
* Allow to have holidays in industrial profile

Bug fixes
#########

Other changes
#############

* simple_profile from IndustrialLoadProfile behaved differently in version
0.2.0 compared to 0.1.9 due to pandas masking functions. We are now back
to the old (design) values.


v0.2.0 (2024-06-27)
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Overview
:alt: Supported implementations
:target: https://pypi.org/project/demandlib

.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/demandlib/v0.1.8.svg
.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/demandlib/latest/dev
:alt: Commits since latest release
:target: https://github.com/oemof/demandlib/compare/v0.1.9...dev
:target: https://github.com/oemof/demandlib/compare/master...dev



Expand Down
32 changes: 31 additions & 1 deletion docs/further_profiles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,37 @@ Industrial Electrical Profile
Description
+++++++++++

The industrial electrical profile uses a step function.
The industrial electrical profile uses a step function synthesized using different
scaling factors for weekdays, weekend days and holidays as well as day time and night
time.

Usage
+++++
The industrial profile is explained in the example `electricity_demand_example.py`
located in the examples directory of the repository.

.. code-block:: python
import datetime
import demandlib.particular_profiles as profiles
import pandas as pd
holidays = {
datetime.date(2018, 1, 1): "New year",
}
# Set up IndustrialLoadProfile
ilp = profiles.IndustrialLoadProfile(
dt_index=pd.date_range("01-01-2018", "01-01-2019", freq="15min"),
holidays=holidays
)
# Get step load profile with own scaling factors and definition of
# beginning of workday
ind_elec_demand = ilp.simple_profile(
annual_demand=1e4,
am=datetime.time(9, 0, 0),
profile_factors={
"week": {"day": 1.0, "night": 0.8},
"weekend": {"day": 0.8, "night": 0.6},
"holiday": {"day": 0.2, "night": 0.2},
},
)
14 changes: 8 additions & 6 deletions examples/electricity_demand_example.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
"""
Creating power demand profiles using bdew profiles.
Creating power demand profiles using BDEW profiles for residential, commercial
and agricultural loads, as well as step load profiles for industrial loads.
Installation requirements
-------------------------
This example requires at least version v0.1.4 of the oemof demandlib. Install
by:
pip install 'demandlib>=0.1.4,<0.2'
Optional:
It further requires matplotlib for plotting:
pip install matplotlib
SPDX-FileCopyrightText: Birgit Schachler
Expand Down Expand Up @@ -81,20 +82,21 @@
profile_factors={
"week": {"day": 1.0, "night": 0.8},
"weekend": {"day": 0.8, "night": 0.6},
"holiday": {"day": 0.6, "night": 0.4},
},
)

print(
"Be aware that the values in the DataFrame are 15 minute values"
+ "with a power unit. If you sum up a table with 15min values"
+ "the result will be of the unit 'kW15minutes'."
"Be aware that the values in the DataFrame are 15 minute values "
"with a power unit. If you sum up a table with 15min values "
"the result will be of the unit 'kW15minutes'."
)
print(elec_demand.sum())

print("You will have to divide the result by 4 to get kWh.")
print(elec_demand.sum() / 4)

print("Or resample the DataFrame to hourly values using the mean() " "method.")
print("Or resample the DataFrame to hourly values using the mean() method.")

# Resample 15-minute values to hourly values.
elec_demand_resampled = elec_demand.resample("h").mean()
Expand Down
2 changes: 1 addition & 1 deletion src/demandlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.0"
__version__ = "0.2.1"
171 changes: 133 additions & 38 deletions src/demandlib/particular_profiles.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
"""
Implementation of the bdew standard load profiles for electric power.
Implementation of industrial step load profiles.
"""
import logging
from datetime import time as settime

import pandas as pd
Expand All @@ -13,74 +12,170 @@


class IndustrialLoadProfile:
"""Generate an industrial heat or electric load profile."""
"""Generate an industrial heat or electricity load profile."""

def __init__(self, dt_index, holidays=None):
def __init__(self, dt_index, holidays=None, holiday_is_sunday=False):
"""
Initialize the industrial load profile.
Parameters
----------
dt_index : pandas.DatetimeIndex
Datetime index for the load profile.
holidays : dict
Dictionary with keys of type datetime and values with holiday name
as str, e.g. {datetime.date(2010, 1, 1): "New year"}. See the
electricity_demand_example in the examples directory for
information on how to obtain holidays using workalender.
Per default, no holidays are used.
Default: None.
holiday_is_sunday : bool
If True, holidays are treated as Sundays. If False, holidays are
assigned weekday 0 and separate holiday scaling factors are applied
in method simple_profile().
Default: False.
"""
self.dataframe = pd.DataFrame(index=dt_index)
self.dataframe = add_weekdays2df(
self.dataframe, holiday_is_sunday=True, holidays=holidays
self.dataframe,
holiday_is_sunday=holiday_is_sunday,
holidays=holidays,
)

def simple_profile(self, annual_demand, **kwargs):
"""
Create industrial load profile
Create industrial step load profile.
Parameters
----------
annual_demand : float
Total demand.
Total demand over the period given upon initialisation
of IndustrialLoadProfile through parameter `dt_index`. This is
actually only the annual demand, if an entire year was given.
Other Parameters
----------------
am : datetime.time
beginning of workday
Defines the beginning of the workday. Times between `am` and `pm`,
including the start and end time defined by `am` and `pm`, are
assigned "day" factors from `profile_factors`. Other times are
assigned "night" factors.
Default: 7 a.m.
pm : datetime.time
end of workday
week : list
list of weekdays
weekend : list
list of weekend days
profile_factors : dictionary
dictionary with scaling factors for night and day of weekdays and
weekend days
Defines the end of the workday. Times between `am` and `pm`,
including the start and end time defined by `am` and `pm`, are
assigned "day" factors from `profile_factors`. Other times are
assigned "night" factors.
Default: 11:30 p.m.
week : list(int)
List of weekdays, where 1 corresponds to Monday, 2 to Tuesday, etc.
Weekdays are assigned "week" factors from `profile_factors`.
Default: [1, 2, 3, 4, 5].
weekend : list(int)
List of weekend days, where 1 corresponds to Monday, 2 to Tuesday,
etc. Weekend days are assigned "weekend" factors from
`profile_factors`.
Default: [6, 7].
holiday : list(int)
List of holiday days. Holidays given upon initialisation of the
IndustrialLoadProfile object are tagged with weekday 0 if
`holiday_is_sunday` is set to False, wherefore the default for this
parameter is [0].
Holidays are assigned "holiday" factors from `profile_factors`.
Default: [0].
profile_factors : dict
Dictionary with load profile scaling factors for night and day of
weekdays, weekend days and holidays. The dictionary must have the
same form as the dictionary given as the default value.
Default:
.. code:: python
{
"week": {"day": 0.8, "night": 0.6},
"weekend": {"day": 0.9, "night": 0.7},
"holiday": {"day": 0.9, "night": 0.7},
}
Returns
-------
pd.Series
Series with demand per time step (unit depends on the unit the
`annual_demand` was provided with). Index is a DatetimeIndex
containing all time steps the IndustrialLoadProfile was initialised
with.
"""

# Day(am to pm), night (pm to am), week day (week),
# weekend day (weekend)
# Define day (am to pm), night (pm to am), week day (week),
# weekend day (weekend) and holiday
am = kwargs.get("am", settime(7, 00, 0))
pm = kwargs.get("pm", settime(23, 30, 0))

week = kwargs.get("week", [1, 2, 3, 4, 5])
weekend = kwargs.get("weekend", [0, 6, 7])
weekend = kwargs.get("weekend", [6, 7])
holiday = kwargs.get("holiday", [0])

default_factors = {
"week": {"day": 0.8, "night": 0.6},
"weekend": {"day": 0.9, "night": 0.7},
"holiday": {"day": 0.9, "night": 0.7},
}

profile_factors = kwargs.get("profile_factors", default_factors)

self.dataframe["ind"] = 0

self.dataframe["ind"] = self.dataframe["ind"].mask(
cond=self.dataframe["weekday"].between_time(am, pm).isin(week),
other=profile_factors["week"]["day"],
# check profile factors
for key in ["week", "weekend", "holiday"]:
if key not in profile_factors.keys():
raise ValueError(
f"Missing entry for '{key}' in profile_factors."
)
else:
if "day" not in profile_factors[key].keys():
raise ValueError(
f"Missing entry for 'day' in profile_factors for "
f"'{key}'."
)
elif "night" not in profile_factors[key].keys():
raise ValueError(
f"Missing entry for 'night' in profile_factors for "
f"'{key}'."
)

self.dataframe["ind"] = 0.0

day_mask = self.dataframe.index.indexer_between_time(
am, pm, include_start=True, include_end=True
)
self.dataframe["ind"] = self.dataframe["ind"].mask(
cond=self.dataframe["weekday"].between_time(pm, am).isin(week),
other=profile_factors["week"]["night"],
day_filter = pd.Series(False, index=self.dataframe.index)
day_filter.iloc[day_mask] = True
# set up night filter as the reverse of the day filter
night_filter = pd.Series(True, index=self.dataframe.index)
night_filter.iloc[day_mask] = False

week_filter = self.dataframe["weekday"].isin(week)
weekend_filter = self.dataframe["weekday"].isin(weekend)
holiday_filter = self.dataframe["weekday"].isin(holiday)

# Update 'ind' column based on day/night filters and
# weekday/weekend/holiday conditions
self.dataframe.loc[day_filter & week_filter, "ind"] = profile_factors[
"week"
]["day"]
self.dataframe.loc[night_filter & week_filter, "ind"] = (
profile_factors["week"]["night"]
)
self.dataframe["ind"] = self.dataframe["ind"].mask(
cond=self.dataframe["weekday"].between_time(am, pm).isin(weekend),
other=profile_factors["weekend"]["day"],
self.dataframe.loc[day_filter & weekend_filter, "ind"] = (
profile_factors["weekend"]["day"]
)
self.dataframe["ind"] = self.dataframe["ind"].mask(
cond=self.dataframe["weekday"].between_time(pm, am).isin(weekend),
other=profile_factors["weekend"]["night"],
self.dataframe.loc[night_filter & weekend_filter, "ind"] = (
profile_factors["weekend"]["night"]
)
self.dataframe.loc[day_filter & holiday_filter, "ind"] = (
profile_factors["holiday"]["day"]
)
self.dataframe.loc[night_filter & holiday_filter, "ind"] = (
profile_factors["holiday"]["night"]
)

if self.dataframe["ind"].isnull().any(axis=0):
logging.error("NAN value found in industrial load profile")

time_interval = self.dataframe.index.freq.nanos / 3.6e12

Expand Down
Loading

0 comments on commit 5ab05bf

Please sign in to comment.