From b9945b992c29394fd27c83af162be09afa90c09d Mon Sep 17 00:00:00 2001 From: martacki Date: Fri, 18 Oct 2024 11:10:35 +0200 Subject: [PATCH 1/6] initial dsm implementation for the heating sector --- config/config.default.yaml | 3 +++ rules/build_sector.smk | 7 +++++ scripts/build_hourly_heat_demand.py | 24 +++++++++++++++++ scripts/prepare_sector_network.py | 41 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index a475c6fdf..2ec73199f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -487,6 +487,9 @@ sector: rural: - air - ground + residential_heat_dsm: true + residential_heat_restriction_value: 0.27 + residential_heat_restriction_time: [10, 22] # 9am and 9pm cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 88e29dbe1..dfb3938ef 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -170,11 +170,15 @@ rule build_hourly_heat_demand: params: snapshots=config_provider("snapshots"), drop_leap_day=config_provider("enable", "drop_leap_day"), + sector=config_provider("sector"), input: heat_profile="data/heat_load_profile_BDEW.csv", heat_demand=resources("daily_heat_demand_total_base_s_{clusters}.nc"), output: heat_demand=resources("hourly_heat_demand_total_base_s_{clusters}.nc"), + heat_dsm_profile=resources( + "residential_heat_dsm_profile_total_base_s_{clusters}.csv" + ), resources: mem_mb=2000, threads: 8 @@ -1069,6 +1073,9 @@ rule prepare_sector_network: transport_data=resources("transport_data_s_{clusters}.csv"), avail_profile=resources("avail_profile_s_{clusters}.csv"), dsm_profile=resources("dsm_profile_s_{clusters}.csv"), + heat_dsm_profile=resources( + "residential_heat_dsm_profile_total_base_s_{clusters}.csv" + ), co2_totals_name=resources("co2_totals.csv"), co2="data/bundle/eea/UNFCCC_v23.csv", biomass_potentials=lambda w: ( diff --git a/scripts/build_hourly_heat_demand.py b/scripts/build_hourly_heat_demand.py index 9bb1f77ff..7c418a3af 100644 --- a/scripts/build_hourly_heat_demand.py +++ b/scripts/build_hourly_heat_demand.py @@ -32,10 +32,25 @@ from itertools import product +import numpy as np import pandas as pd import xarray as xr from _helpers import generate_periodic_profiles, get_snapshots, set_scenario_config +def heat_dsm_profile(nodes, options): + + weekly_profile = np.ones((24 * 7)) + for i in options["residential_heat_restriction_time"]: + weekly_profile[(np.arange(0, 7, 1) * 24 + int(i))] = 0 + + dsm_profile = generate_periodic_profiles( + dt_index=pd.date_range(freq="h", **snakemake.params.snapshots, tz="UTC"), + nodes=nodes, + weekly_profile=weekly_profile, + ) + + return dsm_profile + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -51,6 +66,8 @@ snakemake.params.snapshots, snakemake.params.drop_leap_day ) + options = snakemake.params.sector + daily_space_heat_demand = ( xr.open_dataarray(snakemake.input.heat_demand) .to_pandas() @@ -63,6 +80,7 @@ uses = ["water", "space"] heat_demand = {} + dsm_profile = {} for sector, use in product(sectors, uses): weekday = list(intraday_profiles[f"{sector} {use} weekday"]) weekend = list(intraday_profiles[f"{sector} {use} weekend"]) @@ -77,13 +95,19 @@ heat_demand[f"{sector} {use}"] = ( daily_space_heat_demand * intraday_year_profile ) + if sector == "residential": + dsm_profile[f"{sector} {use}"] = heat_dsm_profile( + daily_space_heat_demand.columns, options + ) else: heat_demand[f"{sector} {use}"] = intraday_year_profile heat_demand = pd.concat(heat_demand, axis=1, names=["sector use", "node"]) + dsm_profile = pd.concat(dsm_profile, axis=1, names=["sector use", "node"]) heat_demand.index.name = "snapshots" ds = heat_demand.stack(future_stack=True).to_xarray() ds.to_netcdf(snakemake.output.heat_demand) + dsm_profile.to_csv(snakemake.output.heat_dsm_profile) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a8ddce49d..6f9b3e684 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2143,6 +2143,47 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): p_set=heat_load.loc[n.snapshots], ) + if options["residential_heat_dsm"] and heat_system in [ + HeatSystem.RESIDENTIAL_RURAL, + HeatSystem.RESIDENTIAL_URBAN_DECENTRAL, + HeatSystem.URBAN_CENTRAL, + ]: + factor = heat_system.heat_demand_weighting( + urban_fraction=urban_fraction[nodes], dist_fraction=dist_fraction[nodes] + ) + + heat_dsm_profile = pd.read_csv( + snakemake.input.heat_dsm_profile, header=[1], index_col=[0] + )[nodes] + heat_dsm_profile.index = n.snapshots + + e_nom = ( + heat_demand[["residential space"]] + .T.groupby(level=1) + .sum() + .T[nodes] + .multiply(factor) + ) + + heat_dsm_profile = heat_dsm_profile * options["residential_heat_restriction_value"] + e_nom = e_nom.max() + + tes_time_constant_days = options["tes_tau"]["decentral"] + + n.madd( + "Store", + nodes, + suffix=f" {heat_system} heat flexibility", + bus=nodes + f" {heat_system} heat", + carrier="residential heating flexibility", + standing_loss=0, # 1 - np.exp(-1 / 24 / tes_time_constant_days), + e_cyclic=True, + e_nom=e_nom, + e_max_pu=heat_dsm_profile + ) + + logger.info(f"adding heat dsm in {heat_system} heating.") + ## Add heat pumps for heat_source in snakemake.params.heat_pump_sources[ heat_system.system_type.value From 5dc8eab1a19810f6a8446f5dedad7595bb89ce09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:11:49 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_hourly_heat_demand.py | 2 ++ scripts/prepare_sector_network.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/build_hourly_heat_demand.py b/scripts/build_hourly_heat_demand.py index 7c418a3af..9eb9dd166 100644 --- a/scripts/build_hourly_heat_demand.py +++ b/scripts/build_hourly_heat_demand.py @@ -37,6 +37,7 @@ import xarray as xr from _helpers import generate_periodic_profiles, get_snapshots, set_scenario_config + def heat_dsm_profile(nodes, options): weekly_profile = np.ones((24 * 7)) @@ -51,6 +52,7 @@ def heat_dsm_profile(nodes, options): return dsm_profile + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6f9b3e684..5a7cfd986 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2165,7 +2165,9 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): .multiply(factor) ) - heat_dsm_profile = heat_dsm_profile * options["residential_heat_restriction_value"] + heat_dsm_profile = ( + heat_dsm_profile * options["residential_heat_restriction_value"] + ) e_nom = e_nom.max() tes_time_constant_days = options["tes_tau"]["decentral"] @@ -2176,10 +2178,10 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): suffix=f" {heat_system} heat flexibility", bus=nodes + f" {heat_system} heat", carrier="residential heating flexibility", - standing_loss=0, # 1 - np.exp(-1 / 24 / tes_time_constant_days), + standing_loss=0, # 1 - np.exp(-1 / 24 / tes_time_constant_days), e_cyclic=True, e_nom=e_nom, - e_max_pu=heat_dsm_profile + e_max_pu=heat_dsm_profile, ) logger.info(f"adding heat dsm in {heat_system} heating.") From c483d3e6216e5d6bb9e789502fd6b4f0747a5455 Mon Sep 17 00:00:00 2001 From: martacki Date: Fri, 18 Oct 2024 14:25:18 +0200 Subject: [PATCH 3/6] assume thermal losses in buildings and remove heating dsm from default --- config/config.default.yaml | 2 +- scripts/prepare_sector_network.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 2ec73199f..5c4ebef31 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -487,7 +487,7 @@ sector: rural: - air - ground - residential_heat_dsm: true + residential_heat_dsm: false residential_heat_restriction_value: 0.27 residential_heat_restriction_time: [10, 22] # 9am and 9pm cluster_heat_buses: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5a7cfd986..ee26be004 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2170,6 +2170,7 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): ) e_nom = e_nom.max() + # Thermal (standing) losses of buildings assumed to be the same as decentralized water tanks tes_time_constant_days = options["tes_tau"]["decentral"] n.madd( @@ -2178,7 +2179,7 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): suffix=f" {heat_system} heat flexibility", bus=nodes + f" {heat_system} heat", carrier="residential heating flexibility", - standing_loss=0, # 1 - np.exp(-1 / 24 / tes_time_constant_days), + standing_loss=1-np.exp(-1/24/tes_time_constant_days), e_cyclic=True, e_nom=e_nom, e_max_pu=heat_dsm_profile, From 1c6ba98825bad2f3ab8aeac6487a327703010b58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:25:44 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ee26be004..7992391d9 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2179,7 +2179,7 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): suffix=f" {heat_system} heat flexibility", bus=nodes + f" {heat_system} heat", carrier="residential heating flexibility", - standing_loss=1-np.exp(-1/24/tes_time_constant_days), + standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days), e_cyclic=True, e_nom=e_nom, e_max_pu=heat_dsm_profile, From 0d1d1bb55e3f647de3259fa6fcc61f907dc5fe38 Mon Sep 17 00:00:00 2001 From: martacki Date: Fri, 18 Oct 2024 14:33:38 +0200 Subject: [PATCH 5/6] improve heat dsr documentation --- doc/configtables/sector.csv | 7 +++++-- doc/release_notes.rst | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index eeee192eb..02799ed90 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -27,10 +27,13 @@ district_heating,--,,`prepare_sector_network.py `_. Set to 0 for full restriction on HP DSM, default value is 0.27, +residential_heat_restriction_time,--,list of int, Time at which SOC of HPs has to be residential_heat_restriction_value. Defaults to [10, 22] corresponding to 9am and 9pm, cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. ,,, -bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM -bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value +bev_dsm_restriction_value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM, +bev_dsm_restriction_time,--,float,Time at which SOC of BEV has to be bev_dsm_restriction_value, transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." ,,, diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7bc693504..00b7459ef 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -78,6 +78,8 @@ Upcoming Release * Bugfix: demand for ammonia was double-counted at current/near-term planning horizons when ``sector['ammonia']`` was set to ``True``. +* Add demand-side-response (DSR) for the heating sector. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== From 49b34807d98a92099e1d9968954c305851bf0857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20R=C3=BCdt?= <117752024+daniel-rdt@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:08:30 +0100 Subject: [PATCH 6/6] fix for heat_dsm_profile for leap year (#1534) * fix for heat_dsm_profile for leap year * add fneum code suggestion * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/prepare_sector_network.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index cbeb3a33a..7e7df80fa 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2153,9 +2153,11 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): ) heat_dsm_profile = pd.read_csv( - snakemake.input.heat_dsm_profile, header=[1], index_col=[0] - )[nodes] - heat_dsm_profile.index = n.snapshots + snakemake.input.heat_dsm_profile, + header=[1], + index_col=[0], + parse_dates=True, + )[nodes].reindex(n.snapshots) e_nom = ( heat_demand[["residential space"]]