From 3ff8e0ba6d96a50771d34c63c3a392d4141cb2e0 Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Mon, 3 Mar 2025 13:12:21 +0100 Subject: [PATCH 1/8] Make temperature limit parameters actually optional --- src/demandlib/vdi/regions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index 66bb549..055858b 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -603,8 +603,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), ) df_typ = ( self.type_days[tl] From 21cbb0a55fc09bce0d460e9d7c0356e7ef190125 Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Mon, 3 Mar 2025 13:15:23 +0100 Subject: [PATCH 2/8] Allow skipping any energy type Heating, hot water and electrical demand are now optional parameters. If they are not provided, the respective time series will be returned with all NaNs. --- src/demandlib/vdi/regions.py | 17 ++++++++++------- tests/test_vdi4655.py | 21 ++++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index 055858b..80e7c3f 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -426,17 +426,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 * ``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"]] @@ -503,9 +506,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")) # (6.4) Do calculations according to VDI 4655 for each 'typtag' for typtag in typtage_combinations: @@ -632,7 +635,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 load_curve_house[column] = ( diff --git a/tests/test_vdi4655.py b/tests/test_vdi4655.py index e16c13e..70484e5 100644 --- a/tests/test_vdi4655.py +++ b/tests/test_vdi4655.py @@ -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", @@ -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.""" From 68bdc1a6b1050ae2694daed38757607a8017f008 Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Mon, 3 Mar 2025 13:21:47 +0100 Subject: [PATCH 3/8] Remove unused parameter 'copies' from example and reorder parameters --- examples/vdi_profile_example.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/vdi_profile_example.py b/examples/vdi_profile_example.py index e65a10d..dcc0036 100644 --- a/examples/vdi_profile_example.py +++ b/examples/vdi_profile_example.py @@ -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, @@ -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, From f6d0d6a0fbd919543b757a5393961cf7705a6e3e Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Mon, 3 Mar 2025 13:23:23 +0100 Subject: [PATCH 4/8] Allow custom DWD weather data by exposing new function 'from_file()' --- src/demandlib/vdi/regions.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index 80e7c3f..cb6bf9b 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -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__( @@ -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( @@ -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. From 05a51d215c8d88d1207c7929e227fd1ea0997b89 Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Mon, 3 Mar 2025 13:24:45 +0100 Subject: [PATCH 5/8] Adapt VDI documentation to latest changes --- docs/vdi4655.rst | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/vdi4655.rst b/docs/vdi4655.rst index a871457..ae86535 100644 --- a/docs/vdi4655.rst +++ b/docs/vdi4655.rst @@ -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" ) @@ -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. From 5ddbd67d24cb027643778feda5fa8d315d6fe25e Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Wed, 12 Mar 2025 08:56:23 +0100 Subject: [PATCH 6/8] Format and clean code --- src/demandlib/vdi/regions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index cb6bf9b..8c0adf8 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -29,6 +29,7 @@ For a given year, the typical days can be matched to the actual calendar days, based on the following conditions: + - Season: summer, winter or transition - Day: weekday or sunday (Or holiday, which counts as sunday) - Cloud coverage: cloudy or not cloudy @@ -487,8 +488,6 @@ def get_daily_energy_demand_houses(self, tl): """ if tl not in self.type_days: self.type_days[tl] = self._get_typical_days(self._holidays, tl) - # typtage_combinations = settings["typtage_combinations"] - # houses_list = settings["houses_list_VDI"] if self.zero_summer_heat_demand: # Reduze the value of 'F_Heiz_TT' to zero. From a947fa2fb59bf6a28d780cae14b53566d393f2ce Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Wed, 12 Mar 2025 08:58:06 +0100 Subject: [PATCH 7/8] Add checks for required and unsupported parameters to add_houses() --- src/demandlib/vdi/regions.py | 28 ++++++++++++++++++++++++++++ tests/test_vdi4655.py | 11 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index 8c0adf8..aba2687 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -456,6 +456,34 @@ def add_houses(self, houses): respective time series will be returned with all NaNs.) """ + param_req = ["name", "house_type", "N_Pers", "N_WE"] + param_opt = [ + "Q_Heiz_a", + "Q_TWW_a", + "W_a", + "summer_temperature_limit", + "winter_temperature_limit", + ] + for i, h in enumerate(houses): + param_missing = [p for p in param_req if p not in h.keys()] + if len(param_missing) > 0: + msg = ( + f"House {i} is missing the following required " + f"parameters: {param_missing}" + ) + raise AttributeError(msg) + + for h in houses: + param_wrong = [ + k for k in h.keys() if k not in param_req + param_opt + ] + if len(param_wrong) > 0: + msg = ( + f"The following parameters for house {h['name']} " + f"are not supported: {param_wrong}" + ) + raise AttributeError(msg) + houses_wrong = r"\n".join( [str(h) for h in houses if h["house_type"] not in ["EFH", "MFH"]] ) diff --git a/tests/test_vdi4655.py b/tests/test_vdi4655.py index 70484e5..2a54216 100644 --- a/tests/test_vdi4655.py +++ b/tests/test_vdi4655.py @@ -205,6 +205,17 @@ def test_dwd_weather_file_missing_header(self): finally: os.remove(temp_filepath) + def test_house_parameters_missing(self): + houses = [{"name": "House with missing parameters"}] + with pytest.raises(AttributeError, match="required parameters"): + Region(2017, climate=Climate().from_try_data(4), houses=houses) + + def test_house_parameters_unsupported(self, example_houses): + houses = example_houses.copy() + houses[0]["unsupported"] = "unsupported paramter" + with pytest.raises(AttributeError, match="not supported"): + Region(2017, climate=Climate().from_try_data(4), houses=houses) + def test_wrong_house_type(self, example_houses): houses = example_houses + [ { From 23f6b9c77ede7d32fe4d84f0eb77b2f7a235812b Mon Sep 17 00:00:00 2001 From: Joris Nettelstroth Date: Wed, 12 Mar 2025 09:07:01 +0100 Subject: [PATCH 8/8] Rename from_file() to from_dwd_weather_file() and improve docs --- docs/vdi4655.rst | 38 ++++++++++++++++-------- src/demandlib/vdi/regions.py | 57 ++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/docs/vdi4655.rst b/docs/vdi4655.rst index ae86535..2168088 100644 --- a/docs/vdi4655.rst +++ b/docs/vdi4655.rst @@ -82,24 +82,38 @@ The module uses German test reference year (TRY) weather data by 'Deutscher Wett 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:: +* Use built-in TRY weather data from 2010: - climate = vdi.Climate().from_try_data(try_region=4) + :: -* Load data from a custom weather file:: + climate = vdi.Climate().from_try_data(try_region=4) - climate = vdi.Climate().from_file(fn_weather='path/to/weather.dat', try_region=4) +* Load data from a custom weather file: -* Initialize with your own data:: + :: - climate = vdi.Climate( - temperature=your_temp_data, - cloud_coverage=your_cloud_data, - energy_factors=your_energy_factors - ) + climate = vdi.Climate().from_dwd_weather_file(fn_weather='path/to/weather.dat', try_region=4) + + 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/) + + Please refer to the function documentation for the caveats of using custom weather data. + In short, the function should be used with caution, since this is not the usage intended + by the norm and the profiles are supposed to be generated with the 2010 TRY weather data. + +* Initialize with your own data: + + :: + + climate = vdi.Climate( + temperature=your_temp_data, + cloud_coverage=your_cloud_data, + energy_factors=your_energy_factors + ) -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/) + To use other sources of weather data, a custom ``Climate()`` object can be created + by providing daily average temperature and cloud coverage time series, as well as + matching energy factors, which scale the typical days relative to each other. Further Reading --------------- diff --git a/src/demandlib/vdi/regions.py b/src/demandlib/vdi/regions.py index aba2687..f06b09c 100644 --- a/src/demandlib/vdi/regions.py +++ b/src/demandlib/vdi/regions.py @@ -82,6 +82,19 @@ def __init__( self.energy_factors = energy_factors def from_try_data(self, try_region, hoy=8760): + """ + Create a climate object from test-reference-year data. + + Parameters + ---------- + try_region : int + Number of the test-reference-year region where the building + is located, as defined by the german weather service DWD. + The module dwd_try provides the function find_try_region() to find + the correct region for given coordinates. + hoy : int, optional + Number of hours of the year. The default is 8760. + """ self.check_try_region(try_region) fn_weather = os.path.join( @@ -89,11 +102,51 @@ def from_try_data(self, try_region, hoy=8760): "resources_weather", "TRY2010_{:02d}_Jahr.dat".format(try_region), ) - self.from_file(fn_weather, try_region, hoy) + self.from_dwd_weather_file(fn_weather, try_region, hoy) return self - def from_file(self, fn_weather, try_region, hoy=8760): + def from_dwd_weather_file(self, fn_weather, try_region, hoy=8760): + """ + Create a climate object from a DWD weather file. + + The weather file must adhere to the standard of the TRY weather + data published in 2016 by the German weather service DWD, + available at https://kunden.dwd.de/obt/. + + .. note:: + + The function ``from_try_data()`` is the implementation for using + weather data as intended by the VDI 4655, because it loads + the original weather data. Using different weather data is + not supported by the norm. + + However, ``from_dwd_weather_file()`` enables users to load the + most recent DWD test reference year weather files **at their + own risk**. They still need to provide a TRY region number, + which is required for loading the energy factors. + These are used for scaling the typical days relative to each + other, depending on the TRY region. But users need to be aware + that they do not have the originally intended effect when + used with different weather data. + + Other file types are currently not supported. Instead, users + need to create a ``Climate()`` object and provide temperature + and cloud coverage time series, as well as matching energy + factors. + + Parameters + ---------- + fn_weather : str + Name of the weather data file to load. + try_region : int + Number of the test-reference-year region where the building + is located, as defined by the German weather service DWD. + The module ``dwd_try`` provides the function ``find_try_region()`` + to find the correct region for given coordinates. + hoy : int, optional + Number of hours of the year. The default is 8760. + """ self.check_try_region(try_region) weather = dwd_try.read_dwd_weather_file(fn_weather)