Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
44 changes: 31 additions & 13 deletions docs/vdi4655.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ Here's a basic example of how to use the VDI 4655 module::
}
]

# Create climate object with weather data
climate = vdi.Climate().from_try_data(try_region=4)

# Create region
region = vdi.Region(
2017,
try_region=4,
climate=climate,
houses=houses,
resample_rule="1h"
)
Expand All @@ -53,41 +56,56 @@ Here's a basic example of how to use the VDI 4655 module::
House Parameters
----------------

The houses need to be defined as a list of dictionaries.
Required parameters for each house:

* ``name``: Unique identifier for the house
* ``house_type``: Either "EFH" (single-family) or "MFH" (multi-family)
* ``N_Pers``: Number of persons, up to 12 (relevant for EFH)
* ``N_WE``: Number of apartments, up to 40 (relevant for MFH)

Optional parameters for each house:

* ``Q_Heiz_a``: Annual heating demand in kWh
* ``Q_TWW_a``: Annual hot water demand in kWh
* ``W_a``: Annual electricity demand in kWh

Optional parameters:

* ``summer_temperature_limit``: Temperature threshold for summer season (default: 15°C)
* ``winter_temperature_limit``: Temperature threshold for winter season (default: 5°C)

(If any of the annual energy values are not provided, the respective time series
will be returned with all NaNs.)

Weather Data
------------

The module uses German test reference year (TRY) weather data by 'Deutscher Wetterdienst' (DWD)
for determining the daily temperature and cloud coverage. You can:
for determining the daily temperature and cloud coverage. Weather data is handled through the Climate class,
which offers several ways to initialize:

* Use built-in TRY weather data from 2010::

climate = vdi.Climate().from_try_data(try_region=4)

* Use the weather data from one of the 15 TRY regions by DWD from 2010
* Load data from a custom weather file::

* Specify a TRY region number (``try_region`` parameter), or
climate = vdi.Climate().from_file(fn_weather='path/to/weather.dat', try_region=4)

* Use geographical coordinates to determine the TRY region (requires geopandas)
* Initialize with your own data::

climate = vdi.Climate(
temperature=your_temp_data,
cloud_coverage=your_cloud_data,
energy_factors=your_energy_factors
)

* Provide your own weather file (``file_weather`` parameter), adhering to the standard
of the TRY weather data published in 2016 by DWD (available at https://kunden.dwd.de/obt/)
The weather file must adhere to the standard of the TRY weather data published
in 2016 by DWD (available at https://kunden.dwd.de/obt/)

Further Reading
---------------

For more details about the VDI 4655 standard, refer to:

* VDI 4655: Reference load profiles of single-family and multi-family houses for the use of CHP systems
* May 2008 (ICS 91.140.01)
* Verein Deutscher Ingenieure e.V.
| VDI 4655: Reference load profiles of single-family and multi-family houses for the use of CHP systems
| May 2008 (ICS 91.140.01)
| Verein Deutscher Ingenieure e.V.
10 changes: 4 additions & 6 deletions examples/vdi_profile_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,11 @@
for n in range(2):
my_houses.append(
{
"N_Pers": 3,
"name": "EFH_{0}".format(n),
"house_type": "EFH",
"N_Pers": 3,
"N_WE": 1,
"Q_Heiz_a": 6000,
"copies": 24,
"house_type": "EFH",
"Q_TWW_a": 1500,
"W_a": 5250,
"summer_temperature_limit": 15,
Expand All @@ -60,12 +59,11 @@
)
my_houses.append(
{
"N_Pers": 45,
"name": "MFH_{0}".format(n),
"house_type": "MFH",
"N_Pers": 45,
"N_WE": 15,
"Q_Heiz_a": 60000,
"copies": 24,
"house_type": "MFH",
"Q_TWW_a": 15000,
"W_a": 45000,
"summer_temperature_limit": 15,
Expand Down
43 changes: 30 additions & 13 deletions src/demandlib/vdi/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Climate:
cloud_coverage : iterable of numbers
The cloud coverage in the area as daily mean values. The
number of values must equal 365 or 366 for a leap year.
energy_factors : pandas DataFrame
Factors for each house type, season type and energy type
for the appropriate TRY region, as provided by the VDI 4655.
"""

def __init__(
Expand All @@ -78,15 +81,20 @@ def __init__(
self.energy_factors = energy_factors

def from_try_data(self, try_region, hoy=8760):
if try_region not in list(range(1, 16)):
raise ValueError(
f">{try_region}< is not a valid number of a DWD TRY region."
)
self.check_try_region(try_region)

fn_weather = os.path.join(
os.path.dirname(__file__),
"resources_weather",
"TRY2010_{:02d}_Jahr.dat".format(try_region),
)
self.from_file(fn_weather, try_region, hoy)

return self

def from_file(self, fn_weather, try_region, hoy=8760):
self.check_try_region(try_region)

weather = dwd_try.read_dwd_weather_file(fn_weather)
weather = (
weather.set_index(
Expand Down Expand Up @@ -127,6 +135,12 @@ def check_attributes(self):
"\n* temperature\n* cloud_coverage\n* energy_factors"
)

def check_try_region(self, try_region):
if try_region not in list(range(1, 16)):
raise ValueError(
f">{try_region}< is not a valid number of a DWD TRY region."
)


class Region:
"""Define region-dependent boundary conditions for the load profiles.
Expand Down Expand Up @@ -426,17 +440,20 @@ def add_houses(self, houses):
"MFH" (multi-family)
* ``N_Pers``: Number of persons, up to 12 (relevant for EFH)
* ``N_WE``: Number of apartments, up to 40 (relevant for MFH)
* ``Q_Heiz_a``: Annual heating demand in kWh
* ``Q_TWW_a``: Annual hot water demand in kWh
* ``W_a``: Annual electricity demand in kWh

Optional:

* ``Q_Heiz_a``: Annual heating demand in kWh
* ``Q_TWW_a``: Annual hot water demand in kWh
* ``W_a``: Annual electricity demand in kWh
Comment on lines +500 to +502
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would always require these inputs. If you do not want to have TWW, you can simply give 0.

* ``summer_temperature_limit``: Temperature threshold for
summer season (default: 15°C)
* ``winter_temperature_limit``: Temperature threshold for
winter season (default: 5°C)

(If any of the annual energy values are not provided, the
respective time series will be returned with all NaNs.)

"""
houses_wrong = r"\n".join(
[str(h) for h in houses if h["house_type"] not in ["EFH", "MFH"]]
Expand Down Expand Up @@ -503,9 +520,9 @@ def get_daily_energy_demand_houses(self, tl):
n_we = house["N_WE"]

# Get yearly energy demands
q_heiz_a = house["Q_Heiz_a"]
w_a = house["W_a"]
q_tww_a = house["Q_TWW_a"]
q_heiz_a = house.get("Q_Heiz_a", float("NaN"))
w_a = house.get("W_a", float("NaN"))
q_tww_a = house.get("Q_TWW_a", float("NaN"))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not agree with this behaviour. This will also happen if there is a typo in the parameter. I think the best way to deal with this ist to introduce a Building() class where we can define what will happen with any of the parameters.

# (6.4) Do calculations according to VDI 4655 for each 'typtag'
for typtag in typtage_combinations:
Expand Down Expand Up @@ -603,8 +620,8 @@ def get_load_curve_houses(self):
for house in self.houses:
t_limit = namedtuple("temperature_limit", "summer winter")
tl = t_limit(
summer=house["summer_temperature_limit"],
winter=house["winter_temperature_limit"],
summer=house.get("summer_temperature_limit", 15),
winter=house.get("winter_temperature_limit", 5),
Comment on lines +703 to +704
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these temperatures in the norm? If so, I think it's okay to set them as defaults.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, these are the values used per default in the norm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default value is part of the norm. I agree.

)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See acomment above.

df_typ = (
self.type_days[tl]
Expand Down Expand Up @@ -632,7 +649,7 @@ def get_load_curve_houses(self):
# The typical day calculation inherently does not add up to the
# desired total energy demand of the full year. Here we fix that:
for column in load_curve_house.columns:
q_a = house[column.replace("TT", "a")]
q_a = house.get(column.replace("TT", "a"), float("NaN"))
sum_ = load_curve_house[column].sum()
if sum_ > 0: # Would produce NaN otherwise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

load_curve_house[column] = (
Expand Down
21 changes: 14 additions & 7 deletions tests/test_vdi4655.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def test_wrong_house_type(self, example_houses):
houses = example_houses + [
{
"N_Pers": 3,
"name": "Wrong_heouse_type",
"name": "Wrong_house_type",
"N_WE": 1,
"Q_Heiz_a": 6000,
"house_type": "wrong",
Expand All @@ -224,16 +224,23 @@ def test_wrong_house_type(self, example_houses):

def test_house_missing_energy_values(self, example_houses):
"""Test handling of houses with missing energy values."""
houses = example_houses.copy()
# Remove some energy values
del houses[0]["Q_Heiz_a"]
del houses[0]["W_a"]
houses = [
{
"name": "EFH",
"house_type": "EFH",
"N_Pers": 3,
"N_WE": 1,
}
]

region = Region(
2017, climate=Climate().from_try_data(4), houses=houses
)
with pytest.raises(KeyError, match="Q_Heiz_a"):
region.get_load_curve_houses()

load_curves = region.get_load_curve_houses()

# Check if load curves for house with NaN values are all zero
assert load_curves.isna().all(axis=None)

def test_invalid_try_region_warning(self, example_houses):
"""Test warning and skipping behavior for invalid TRY region."""
Expand Down