From 083e6abc9810ebf625f5a6cba8cba8aa2de6522d Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 12 Jun 2024 18:00:47 +0200 Subject: [PATCH 01/30] added new script for DH modifications --- workflow/Snakefile | 20 +++- workflow/scripts/modify_dh_systems.py | 166 ++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 workflow/scripts/modify_dh_systems.py diff --git a/workflow/Snakefile b/workflow/Snakefile index 7e12cf5e..e2957e73 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -146,6 +146,24 @@ rule build_mobility_demand: script: "scripts/build_mobility_demand.py" +rule modify_dh_systems: + params: + enable_subnodes_de=config_provider("district_heating", "enable_subnodes_de"), + input: + network=RESULTS + + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + fn="data/demandregio_spatial.json", + fn_map="data/mapping_from_4_to_38.json", + nuts3=resources("nuts3_shapes.geojson"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + triebs="data/Gesamtdaten_Triebs.xlsx", + output: + network=RESULTS + + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" + resources: + mem_mb=1000 + script: + "scripts/modify_prenetwork.py" rule modify_prenetwork: params: @@ -166,7 +184,7 @@ rule modify_prenetwork: clustering=config_provider("clustering", "temporal", "resolution_sector"), input: network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", wkn=resources("wasserstoff_kernnetz_elec_s{simpl}_{clusters}.csv") if config_provider("wasserstoff_kernnetz", "enable") else [], costs=resources("costs_{planning_horizons}.csv"), aladin_demand=resources("mobility_demand_aladin_{simpl}_{clusters}_{planning_horizons}.csv"), diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py new file mode 100644 index 00000000..d523e223 --- /dev/null +++ b/workflow/scripts/modify_dh_systems.py @@ -0,0 +1,166 @@ +import logging + +logger = logging.getLogger(__name__) +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import geopandas as gpd +from shapely.geometry import Point +import json +import pypsa + + +def load_egon(): + """ + Load and prepares the egon data about district heating in Germany on NUTS3 level. + + Returns: + GeoDataFrame: A GeoDataFrame containing the processed egon data. + """ + + nuts3 = gpd.read_file(snakemake.input.nuts3)[ + ["index", "pop", "geometry"] + ] # Keep only necessary columns + + internal_id = { + 71: "District heating", + } + + df = pd.read_json(snakemake.input.fn) + id_region = pd.read_json(snakemake.input.fn_map) + + df["internal_id"] = df["internal_id"].apply(lambda x: x[0]) + df = df[df["internal_id"] == 71] # Keep only rows with district heating + + df["nuts3"] = df.id_region.map( + id_region.set_index(id_region.id_region_from).kuerzel_to + ) + + heat_tech_per_region = df.groupby([df.nuts3, df.internal_id]).sum().value.unstack() + heat_tech_per_region.rename(columns=internal_id, inplace=True) + + egon_df = heat_tech_per_region.merge(nuts3, left_on="nuts3", right_on="index") + egon_gdf = gpd.GeoDataFrame(egon_df) # Convert merged DataFrame to GeoDataFrame + + return egon_gdf + + +def update_dist_shares(egon_gdf, n_pre): + """ + Update district heating shares of clusters according to egon data on NUTS3 level. + """ + + n = n_pre.copy() + regions_onshore = gpd.read_file( + snakemake.input.regions_onshore + ) # shared resources true + regions_onshore.set_index("name", inplace=True) + # Map NUTS3 regions of egon data to corresponding clusters according to maximum overlap + + egon_gdf["cluster"] = egon_gdf.apply( + lambda x: regions_onshore.geometry.intersection(x.geometry).area.idxmax(), + axis=1, + ) + + # Calculate DH demand shares + dh_shares = ( + egon_gdf.groupby("cluster")["District heating"].sum() + / egon_gdf["District heating"].sum() + ) + + # Current DH demands by cluster + dh_demand = n.loads_t.p_set.filter(regex="DE\d.*urban central heat") + + return n + + +def prepare_subnodes_de(egon_gdf): + # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities + + # Load and prepare Triebs data + dh_areas_triebs = pd.read_excel( + snakemake.input.triebs, + sheet_name="Staedte", + ) + # convert dataframe dh_areas_triebs to geopandas dataframe using the Latirude and Longitude columns for geometry column as point coordinates + dh_areas_triebs["geometry"] = gpd.points_from_xy( + dh_areas_triebs["Longitude"], dh_areas_triebs["Latitude"] + ) + dh_areas_triebs = gpd.GeoDataFrame(dh_areas_triebs, geometry="geometry") + + # Merge merged_gdf with dh_areas_triebs using the nuts3 id + merged_gdf = egon_gdf.merge( + dh_areas_triebs, left_on="index", right_on="NUTS3", how="right" + ) + # Create additional column nuts_3_matchshape that contains the value of the index column in merged_gdf of the row where the geometry column of dh_areas_triebs intersects with the geometry column of nuts3_shapes + + merged_gdf.loc[merged_gdf.geometry_x.isna(), "geometry_x"] = merged_gdf.loc[ + merged_gdf.geometry_x.isna() + ].apply( + lambda x: egon_gdf.loc[ + egon_gdf.geometry.contains(x.geometry_y), "geometry" + ].item(), + axis=1, + ) + merged_gdf.loc[merged_gdf["index"].isna(), "index"] = merged_gdf.loc[ + merged_gdf["index"].isna() + ].apply( + lambda x: egon_gdf.loc[ + egon_gdf.geometry.contains(x.geometry_y), "index" + ].item(), + axis=1, + ) + merged_gdf.loc[ + merged_gdf["District heating"].isna(), "District heating" + ] = merged_gdf.loc[merged_gdf["District heating"].isna()].apply( + lambda x: egon_gdf.loc[ + egon_gdf.geometry.contains(x.geometry_y), "District heating" + ].item(), + axis=1, + ) + + # Intraregional distribution key according to population + merged_gdf["intrareg_dist_key"] = merged_gdf.apply( + lambda reg: reg["Einwohnerzahl [-]"] + / merged_gdf.loc[ + merged_gdf["index"] == reg["index"], "Einwohnerzahl [-]" + ].sum(), + axis=1, + ).sort_values() + # Multiply column District heating with distribution key + merged_gdf["demand_dh_subnode"] = ( + merged_gdf["District heating"] * merged_gdf["intrareg_dist_key"] + ) + + return merged_gdf + + +if __name__ == "__main__": + if "snakemake" not in globals(): + import os + import sys + + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + path = "../submodules/pypsa-eur/scripts" + sys.path.insert(0, os.path.abspath(path)) + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "modify_dh_systems", + simpl="", + clusters=22, + opts="", + ll="vopt", + sector_opts="none", + planning_horizons="2020", + run="KN2045_Bal_v4", + ) + logger.info("Adding SysGF-specific functionality") + + n = pypsa.Network(snakemake.input.network) + egon_gdf = load_egon() + update_dist_shares(egon_gdf, n) + + if snakemake.params.enable_subnodes_de: + prepare_subnodes_de(egon_gdf) From 6a9286c94bc5a790cef379285fe097a592f8c885 Mon Sep 17 00:00:00 2001 From: cpschau Date: Fri, 14 Jun 2024 10:56:29 +0200 Subject: [PATCH 02/30] function for update of urban heat loads --- workflow/scripts/modify_dh_systems.py | 85 +++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py index d523e223..ccd12e97 100644 --- a/workflow/scripts/modify_dh_systems.py +++ b/workflow/scripts/modify_dh_systems.py @@ -23,14 +23,23 @@ def load_egon(): ] # Keep only necessary columns internal_id = { + 9: "Hard coal", + 10: "Brown coal", + 11: "Natural gas", + 34: "Heating oil", + 35: "Biomass (solid)", + 68: "Ambient heating", + 69: "Solar heat", 71: "District heating", + 72: "Electrical energy", + 218: "Biomass (excluding wood, biogas)", } df = pd.read_json(snakemake.input.fn) id_region = pd.read_json(snakemake.input.fn_map) df["internal_id"] = df["internal_id"].apply(lambda x: x[0]) - df = df[df["internal_id"] == 71] # Keep only rows with district heating + # df = df[df["internal_id"] == 71] # Keep only rows with district heating df["nuts3"] = df.id_region.map( id_region.set_index(id_region.id_region_from).kuerzel_to @@ -41,13 +50,15 @@ def load_egon(): egon_df = heat_tech_per_region.merge(nuts3, left_on="nuts3", right_on="index") egon_gdf = gpd.GeoDataFrame(egon_df) # Convert merged DataFrame to GeoDataFrame + egon_gdf = egon_gdf.to_crs("EPSG:4326") return egon_gdf -def update_dist_shares(egon_gdf, n_pre): +def update_urban_loads_de(egon_gdf, n_pre): """ - Update district heating shares of clusters according to egon data on NUTS3 level. + Update district heating demands of clusters according to shares in egon data on NUTS3 level for Germany. + Other heat loads are adjusted accodingly to ensure consistency of the nodal heat demand. """ n = n_pre.copy() @@ -62,16 +73,68 @@ def update_dist_shares(egon_gdf, n_pre): axis=1, ) - # Calculate DH demand shares - dh_shares = ( - egon_gdf.groupby("cluster")["District heating"].sum() - / egon_gdf["District heating"].sum() + # Calculate nodel DH shares according to households and modify index + egon_gdf_clustered = egon_gdf.groupby("cluster").sum(numeric_only=True) + nodal_dh_shares = egon_gdf_clustered["District heating"] / egon_gdf_clustered.drop( + "pop", axis=1 + ).sum(axis=1) + + nodal_dh_shares.index += " urban central heat" + + # District heating demands by cluster in German nodes before heat distribution + nodal_uc_demand = ( + n.loads_t.p_set.filter(regex="DE.*urban central heat") + .apply(lambda c: c * n.snapshot_weightings.generators) + .sum() + .div( + 1 + snakemake.config["sector"]["district_heating"]["district_heating_loss"] + ) + ) + + nodal_uc_losses = ( + nodal_uc_demand + - n.loads_t.p_set.filter(regex="DE.*urban central heat") + .apply(lambda c: c * n.snapshot_weightings.generators) + .sum() + ) + + # Sum of rural and urban heat demand + nodal_heat_demand = ( + n.loads_t.p_set.filter(regex="DE.*heat$") + .apply(lambda c: c * n.snapshot_weightings.generators) + .sum() + .sub(nodal_uc_losses, fill_value=0) + ) + + # Modify index of nodal_heat demand to align with urban central loads and aggregatze loads + nodal_heat_demand.index = nodal_heat_demand.index.str.replace( + "decentral", "central" + ).str.replace("rural", "urban central") + + nodal_heat_demand = nodal_heat_demand.groupby(nodal_heat_demand.index).sum() + + # Old district heating share + nodal_uc_shares = nodal_uc_demand / nodal_heat_demand + + # Scaling factor for update of urban central heat loads + + scaling_factor = nodal_dh_shares / nodal_uc_shares + scaling_factor.dropna( + inplace=True + ) # To deal with shape anomaly described in https://github.com/PyPSA/pypsa-eur/issues/1100 + + # Update urban heat loads + old_uc_loads = n.loads_t.p_set.filter(regex="DE.*urban central heat") + new_uc_loads = ( + n.loads_t.p_set.filter(regex="DE.*urban central heat") * scaling_factor ) + diff_update = new_uc_loads - old_uc_loads + diff_update.columns = diff_update.columns.str.replace("central", "decentral") - # Current DH demands by cluster - dh_demand = n.loads_t.p_set.filter(regex="DE\d.*urban central heat") + n_pre.loads_t.p_set[new_uc_loads.columns] = new_uc_loads + n_pre.loads_t.p_set[diff_update.columns] -= diff_update - return n + return n_pre def prepare_subnodes_de(egon_gdf): @@ -160,7 +223,7 @@ def prepare_subnodes_de(egon_gdf): n = pypsa.Network(snakemake.input.network) egon_gdf = load_egon() - update_dist_shares(egon_gdf, n) + update_urban_loads_de(egon_gdf, n) if snakemake.params.enable_subnodes_de: prepare_subnodes_de(egon_gdf) From dbc2c37e2985066bd6034fdf0bb18758444b809e Mon Sep 17 00:00:00 2001 From: cpschau Date: Mon, 17 Jun 2024 10:30:55 +0200 Subject: [PATCH 03/30] Preserve aggregate DH demand --- workflow/scripts/modify_dh_systems.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py index ccd12e97..ce290c42 100644 --- a/workflow/scripts/modify_dh_systems.py +++ b/workflow/scripts/modify_dh_systems.py @@ -73,7 +73,7 @@ def update_urban_loads_de(egon_gdf, n_pre): axis=1, ) - # Calculate nodel DH shares according to households and modify index + # Calculate nodal DH shares according to households and modify index egon_gdf_clustered = egon_gdf.groupby("cluster").sum(numeric_only=True) nodal_dh_shares = egon_gdf_clustered["District heating"] / egon_gdf_clustered.drop( "pop", axis=1 @@ -123,18 +123,20 @@ def update_urban_loads_de(egon_gdf, n_pre): inplace=True ) # To deal with shape anomaly described in https://github.com/PyPSA/pypsa-eur/issues/1100 - # Update urban heat loads + # Update urban heat loads changing distribution and restoring old scale old_uc_loads = n.loads_t.p_set.filter(regex="DE.*urban central heat") new_uc_loads = ( n.loads_t.p_set.filter(regex="DE.*urban central heat") * scaling_factor ) + restore_scalar = new_uc_loads.sum().sum() / old_uc_loads.sum().sum() + new_uc_loads = new_uc_loads / restore_scalar diff_update = new_uc_loads - old_uc_loads diff_update.columns = diff_update.columns.str.replace("central", "decentral") n_pre.loads_t.p_set[new_uc_loads.columns] = new_uc_loads n_pre.loads_t.p_set[diff_update.columns] -= diff_update - return n_pre + return def prepare_subnodes_de(egon_gdf): From 8f3d74351684e47117308065ea555ebdf0f3aad4 Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 31 Jul 2024 11:34:46 +0200 Subject: [PATCH 04/30] add subnodes and loads --- workflow/scripts/modify_dh_systems.py | 113 ++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py index ce290c42..89d36aec 100644 --- a/workflow/scripts/modify_dh_systems.py +++ b/workflow/scripts/modify_dh_systems.py @@ -55,10 +55,10 @@ def load_egon(): return egon_gdf -def update_urban_loads_de(egon_gdf, n_pre): +def update_urban_loads(egon_gdf, n_pre): """ Update district heating demands of clusters according to shares in egon data on NUTS3 level for Germany. - Other heat loads are adjusted accodingly to ensure consistency of the nodal heat demand. + Other heat loads are adjusted accordingly to ensure consistency of the nodal heat demand. """ n = n_pre.copy() @@ -106,7 +106,7 @@ def update_urban_loads_de(egon_gdf, n_pre): .sub(nodal_uc_losses, fill_value=0) ) - # Modify index of nodal_heat demand to align with urban central loads and aggregatze loads + # Modify index of nodal_heat demand to align with urban central loads and aggregate loads nodal_heat_demand.index = nodal_heat_demand.index.str.replace( "decentral", "central" ).str.replace("rural", "urban central") @@ -139,7 +139,7 @@ def update_urban_loads_de(egon_gdf, n_pre): return -def prepare_subnodes_de(egon_gdf): +def prepare_subnodes(egon_gdf, head=40): # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities # Load and prepare Triebs data @@ -153,17 +153,23 @@ def prepare_subnodes_de(egon_gdf): ) dh_areas_triebs = gpd.GeoDataFrame(dh_areas_triebs, geometry="geometry") + # Keep only n largest district heating networks according to head parameter + to_keep = ["Stadtname", "NUTS3", "Einwohnerzahl [-]", "geometry"] + + dh_areas_triebs = dh_areas_triebs.sort_values( + by="Einwohnerzahl [-]", ascending=False + ).head(head)[to_keep] + # Merge merged_gdf with dh_areas_triebs using the nuts3 id - merged_gdf = egon_gdf.merge( - dh_areas_triebs, left_on="index", right_on="NUTS3", how="right" + merged_gdf = dh_areas_triebs.merge( + egon_gdf, left_on="NUTS3", right_on="index", how="left" ) - # Create additional column nuts_3_matchshape that contains the value of the index column in merged_gdf of the row where the geometry column of dh_areas_triebs intersects with the geometry column of nuts3_shapes - - merged_gdf.loc[merged_gdf.geometry_x.isna(), "geometry_x"] = merged_gdf.loc[ - merged_gdf.geometry_x.isna() + # DH systems without matching NUTS3 id the surrounding region is assigned using the geometries + merged_gdf.loc[merged_gdf.geometry_y.isna(), "geometry_y"] = merged_gdf.loc[ + merged_gdf.geometry_y.isna() ].apply( lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_y), "geometry" + egon_gdf.geometry.contains(x.geometry_x), "geometry" ].item(), axis=1, ) @@ -171,7 +177,15 @@ def prepare_subnodes_de(egon_gdf): merged_gdf["index"].isna() ].apply( lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_y), "index" + egon_gdf.geometry.contains(x.geometry_x), "index" + ].item(), + axis=1, + ) + merged_gdf.loc[merged_gdf["cluster"].isna(), "cluster"] = merged_gdf.loc[ + merged_gdf["cluster"].isna() + ].apply( + lambda x: egon_gdf.loc[ + egon_gdf.geometry.contains(x.geometry_x), "cluster" ].item(), axis=1, ) @@ -179,12 +193,12 @@ def prepare_subnodes_de(egon_gdf): merged_gdf["District heating"].isna(), "District heating" ] = merged_gdf.loc[merged_gdf["District heating"].isna()].apply( lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_y), "District heating" + egon_gdf.geometry.contains(x.geometry_x), "District heating" ].item(), axis=1, ) - # Intraregional distribution key according to population + # Intraregional distribution key according to population for NUTS3 regions with multiple DH systems merged_gdf["intrareg_dist_key"] = merged_gdf.apply( lambda reg: reg["Einwohnerzahl [-]"] / merged_gdf.loc[ @@ -193,13 +207,75 @@ def prepare_subnodes_de(egon_gdf): axis=1, ).sort_values() # Multiply column District heating with distribution key - merged_gdf["demand_dh_subnode"] = ( + merged_gdf["dh_hh_subnode"] = ( merged_gdf["District heating"] * merged_gdf["intrareg_dist_key"] ) + # Convert district heating counts to percentage of cluster demand + + merged_gdf["demand_share_subnode"] = merged_gdf.apply( + lambda x: x["dh_hh_subnode"] + / egon_gdf.loc[egon_gdf.cluster == x.cluster, "District heating"].sum(), + axis=1, + ) + + # TODO: Function to assign CHPs to subnodes return merged_gdf +def add_subnodes(n, subnodes, head=40): + """ + Add subnodes to the network and adjust loads and capacities accordingly. + """ + + n_sub = n.copy() + + # Add subnodes to network + for idx, row in subnodes.iterrows(): + name = f'{row["cluster"]} {row["Stadtname"]} urban central heat' + + # Add buses + n.madd( + "Bus", + [name], + y=row.geometry_x.y, + x=row.geometry_x.x, + country="DE", + location=row["cluster"], + carrier="urban central heat", + unit="MWh_th", + ) + + # Add heat loads + heat_load = row["demand_share_subnode"] * n.loads_t.p_set.filter( + regex=f"{row['cluster']} urban central heat" + ).rename( + { + f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadtname']} urban central heat" + }, + axis=1, + ) + n.madd( + "Load", + [name], + bus=name, + p_set=heat_load, + carrier="urban central heat", + location=row["cluster"], + profile=row["cluster"], + ) + + # Adjust loads of cluster buses + for idx, row in ( + subnodes.groupby("cluster", as_index=False).sum(numeric_only=True).iterrows() + ): + n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= ( + 1 - row["demand_share_subnode"] + ) + + return + + if __name__ == "__main__": if "snakemake" not in globals(): import os @@ -225,7 +301,8 @@ def prepare_subnodes_de(egon_gdf): n = pypsa.Network(snakemake.input.network) egon_gdf = load_egon() - update_urban_loads_de(egon_gdf, n) + update_urban_loads(egon_gdf, n) - if snakemake.params.enable_subnodes_de: - prepare_subnodes_de(egon_gdf) + if snakemake.params.add_subnodes_de: + subnodes = prepare_subnodes(egon_gdf, snakemake.params.add_subnodes_de) + add_subnodes(n, subnodes) From 1778dc14b120121304340eb05b23be09606f749b Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 6 Aug 2024 09:03:10 +0200 Subject: [PATCH 05/30] load chps, change function header --- workflow/scripts/modify_dh_systems.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py index 89d36aec..7585a799 100644 --- a/workflow/scripts/modify_dh_systems.py +++ b/workflow/scripts/modify_dh_systems.py @@ -55,7 +55,7 @@ def load_egon(): return egon_gdf -def update_urban_loads(egon_gdf, n_pre): +def update_urban_loads(n_pre, egon_gdf): """ Update district heating demands of clusters according to shares in egon data on NUTS3 level for Germany. Other heat loads are adjusted accordingly to ensure consistency of the nodal heat demand. @@ -140,6 +140,9 @@ def update_urban_loads(egon_gdf, n_pre): def prepare_subnodes(egon_gdf, head=40): + """ + Prepare subnodes for the network based on the Triebs data and the egon data. + """ # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities # Load and prepare Triebs data @@ -219,7 +222,6 @@ def prepare_subnodes(egon_gdf, head=40): axis=1, ) - # TODO: Function to assign CHPs to subnodes return merged_gdf @@ -276,6 +278,20 @@ def add_subnodes(n, subnodes, head=40): return +def modify_chps(chps): + """ + Modify the CHP dataframe to include the subnodes + """ + + chps["subnode"] = chps.apply( + lambda x: f'{x["cluster"]} {x["Stadtname"]} urban central heat', axis=1 + ) + + chps["capacity"] = chps["capacity"] * chps["demand_share_subnode"] + + return chps + + if __name__ == "__main__": if "snakemake" not in globals(): import os @@ -301,8 +317,13 @@ def add_subnodes(n, subnodes, head=40): n = pypsa.Network(snakemake.input.network) egon_gdf = load_egon() - update_urban_loads(egon_gdf, n) + update_urban_loads(n, egon_gdf) + chps = pd.read_csv(snakemake.input.german_chp) if snakemake.params.add_subnodes_de: subnodes = prepare_subnodes(egon_gdf, snakemake.params.add_subnodes_de) add_subnodes(n, subnodes) + modify_chps(chps, subnodes) + + n.export_to_netcdf(snakemake.output.network) + chps.to_csv(snakemake.output.german_chp, index=False) From 77f0bddc38054718b8b09bc4e7de03aff3e3e5af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:52:08 +0000 Subject: [PATCH 06/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- workflow/scripts/modify_dh_systems.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py index 7585a799..dc379255 100644 --- a/workflow/scripts/modify_dh_systems.py +++ b/workflow/scripts/modify_dh_systems.py @@ -1,18 +1,21 @@ +# -*- coding: utf-8 -*- import logging logger = logging.getLogger(__name__) -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import geopandas as gpd -from shapely.geometry import Point import json + +import geopandas as gpd +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd import pypsa +from shapely.geometry import Point def load_egon(): """ - Load and prepares the egon data about district heating in Germany on NUTS3 level. + Load and prepares the egon data about district heating in Germany on NUTS3 + level. Returns: GeoDataFrame: A GeoDataFrame containing the processed egon data. @@ -57,8 +60,11 @@ def load_egon(): def update_urban_loads(n_pre, egon_gdf): """ - Update district heating demands of clusters according to shares in egon data on NUTS3 level for Germany. - Other heat loads are adjusted accordingly to ensure consistency of the nodal heat demand. + Update district heating demands of clusters according to shares in egon + data on NUTS3 level for Germany. + + Other heat loads are adjusted accordingly to ensure consistency of + the nodal heat demand. """ n = n_pre.copy() @@ -141,7 +147,8 @@ def update_urban_loads(n_pre, egon_gdf): def prepare_subnodes(egon_gdf, head=40): """ - Prepare subnodes for the network based on the Triebs data and the egon data. + Prepare subnodes for the network based on the Triebs data and the egon + data. """ # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities @@ -280,7 +287,7 @@ def add_subnodes(n, subnodes, head=40): def modify_chps(chps): """ - Modify the CHP dataframe to include the subnodes + Modify the CHP dataframe to include the subnodes. """ chps["subnode"] = chps.apply( From 7bc57492e980846913b7884d7dacb1cc31cd98e1 Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 20 Aug 2024 16:59:01 +0200 Subject: [PATCH 07/30] =?UTF-8?q?changed=20data=20source=20to=20Fernw?= =?UTF-8?q?=C3=A4rmeatlas;=20changed=20script=20structure;=20included=20CH?= =?UTF-8?q?Ps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yaml | 3 +- workflow/Snakefile | 66 ++-- .../scripts/add_district_heating_subnodes.py | 194 ++++++++++ workflow/scripts/build_existing_chp_de.py | 48 ++- workflow/scripts/modify_dh_systems.py | 336 ------------------ 5 files changed, 286 insertions(+), 361 deletions(-) create mode 100644 workflow/scripts/add_district_heating_subnodes.py delete mode 100644 workflow/scripts/modify_dh_systems.py diff --git a/config/config.yaml b/config/config.yaml index 619f308a..1d895fe8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,7 +4,7 @@ # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run run: - prefix: 20240814limitseq + prefix: 20242008_dh_subnodes_off name: # - CurrentPolicies - KN2045_Bal_v4 @@ -222,6 +222,7 @@ sector: 2040: 0.6 2045: 0.8 2050: 1.0 + add_subnodes: false central_heat_vent: true co2_spatial: true biomass_spatial: true diff --git a/workflow/Snakefile b/workflow/Snakefile index 4a0b4530..e0f63abd 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -181,26 +181,6 @@ rule build_mobility_demand: "scripts/build_mobility_demand.py" -rule modify_dh_systems: - params: - enable_subnodes_de=config_provider("district_heating", "enable_subnodes_de"), - input: - network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - fn="data/demandregio_spatial.json", - fn_map="data/mapping_from_4_to_38.json", - nuts3=resources("nuts3_shapes.geojson"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - triebs="data/Gesamtdaten_Triebs.xlsx", - output: - network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", - resources: - mem_mb=1000, - script: - "scripts/modify_prenetwork.py" - - rule build_egon_data: input: demandregio_spatial="data/egon/demandregio_spatial_2018.json", @@ -216,6 +196,28 @@ rule build_egon_data: "scripts/build_egon_data.py" +rule add_district_heating_subnodes: + params: + district_heating=config_provider("sector", "district_heating"), + input: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), + nuts3=resources("nuts3_shapes.geojson"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + fernwaermeatlas="data/fernwaermeatlas/Fernwärmeatlas_öffentlich.xlsx", + output: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", + district_heating_subnodes=resources( + "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + ), + resources: + mem_mb=1000, + script: + "scripts/add_district_heating_subnodes.py" + + ruleorder: modify_district_heat_share > build_district_heat_share @@ -348,6 +350,10 @@ rule retrieve_mastr: rule build_existing_chp_de: + params: + add_district_heating_subnodes=config_provider( + "sector", "district_heating", "add_subnodes" + ), input: mastr_biomass="data/mastr/bnetza_open_mastr_2023-08-08_B_biomass.csv", mastr_combustion="data/mastr/bnetza_open_mastr_2023-08-08_B_combustion.csv", @@ -356,16 +362,30 @@ rule build_existing_chp_de: keep_local=True, ), busmap=resources("networks/base.nc"), + district_heating_subnodes=resources( + "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + ), output: - german_chp=resources("german_chp.csv"), + german_chp=resources( + "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" + ), script: "scripts/build_existing_chp_de.py" use rule add_existing_baseyear from pypsaeur with: + params: + **{k: v for k, v in rules.add_existing_baseyear.params.items()}, + add_district_heating_subnodes=config_provider( + "sector", "district_heating", "add_subnodes" + ), input: - **rules.add_existing_baseyear.input, - custom_powerplants=resources("german_chp.csv"), + **{k: v for k, v in rules.add_existing_baseyear.input.items() if k != "network"}, + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", + custom_powerplants=resources( + "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" + ), use rule build_existing_heating_distribution from pypsaeur with: diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py new file mode 100644 index 00000000..50563287 --- /dev/null +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +import logging + +logger = logging.getLogger(__name__) +from random import randint +from time import sleep + +import geopandas as gpd +import numpy as np +import pandas as pd +import pypsa +from geopy.geocoders import Nominatim + + +# Function to encode city names in UTF-8 +def encode_utf8(city_name): + return city_name.encode("utf-8") + + +def prepare_subnodes(subnodes, regions_onshore, heat_techs, head=40): + # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities + + # Keep only n largest district heating networks according to head parameter + subnodes = subnodes.sort_values( + by="Wärmeeinspeisung in GWh/a", ascending=False + ).head(head) + + # Create a Nominatim object + nominatim = Nominatim(user_agent="cityEncoder") + + subnodes["lat"] = np.nan + subnodes["lon"] = np.nan + subnodes["Stadt"] = subnodes["Stadt"].str.split("_").str[0] + + # Drop duplicates if Gelsenkirchen, Kiel, or Flensburg is included and keep the one with higher Wärmeeinspeisung in GWh/a + subnodes = subnodes.drop_duplicates(subset="Stadt", keep="first") + + # Get the location of all cities in the dataset (Stadt column) and write them to column "location" do it as try ecxept to avoid errors + for i, row in subnodes.iterrows(): + try: + location = nominatim.geocode(encode_utf8(row["Stadt"]), country_codes="DE") + # Extract the latitude and longitude from the location column + subnodes.at[i, "lat"] = location.latitude + subnodes.at[i, "lon"] = location.longitude + sleep_sec = 1 + sleep(randint(1 * 100, sleep_sec * 100) / 100) + except: + logger.info(f"Location not found for {row['Stadt']}") + pass + + # Make a shapely point object from the lat and lon columns + subnodes["geometry"] = gpd.points_from_xy(subnodes["lon"], subnodes["lat"]) + # Drop rows with missing geometry + logger.info("Cities without locations are dropped.") + subnodes = subnodes.dropna(subset=["geometry"]) + # Convert the DataFrame to a GeoDataFrame + subnodes = gpd.GeoDataFrame(subnodes, crs="EPSG:4326") + + # Assign cluster to subnodes according to onshore regions + subnodes["cluster"] = subnodes.apply( + lambda x: regions_onshore.geometry.contains(x.geometry).idxmax(), axis=1 + ) + subnodes["nuts3"] = subnodes.apply( + lambda x: heat_techs.geometry.contains(x.geometry).idxmax(), + axis=1, + ) + subnodes["nuts3_shape"] = subnodes.apply( + lambda x: heat_techs.loc[ + heat_techs.geometry.contains(x.geometry).idxmax(), "geometry" + ].wkt, + axis=1, + ) + + return subnodes + + +def add_subnodes(n, subnodes): + """ + Add subnodes to the network and adjust loads and capacities accordingly. + """ + + # Add subnodes to network + for idx, row in subnodes.iterrows(): + name = f'{row["cluster"]} {row["Stadt"]} urban central heat' + + # Add buses + n.madd( + "Bus", + [name], + y=row.geometry.y, + x=row.geometry.x, + country="DE", + location=row["cluster"], + carrier="urban central heat", + unit="MWh_th", + ) + + # Add heat loads + scalar = min( + 1, + ( + row["Wärmeeinspeisung in GWh/a"] + * 1e3 + / n.loads_t.p_set.filter(regex=f"{row['cluster']} urban central heat") + .sum(axis=1) + .mul(n.snapshot_weightings.generators) + .sum() + ), + ) + lost_load = ( + row["Wärmeeinspeisung in GWh/a"] * 1e3 + - n.loads_t.p_set.filter(regex=f"{row['cluster']} urban central heat") + .sum(axis=1) + .mul(n.snapshot_weightings.generators) + .sum() + ) + if scalar == 1: + logger.info( + f"District heating load of {row['Stadt']} exceeds load of its assigned cluster {row['cluster']}. {lost_load} MWh/a are disregarded." + ) + heat_load = scalar * n.loads_t.p_set.filter( + regex=f"{row['cluster']} urban central heat" + ).rename( + { + f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadt']} urban central heat" + }, + axis=1, + ) + n.madd( + "Load", + [name], + bus=name, + p_set=heat_load, + carrier="urban central heat", + location=row["cluster"], + profile=row["cluster"], + ) + + # Adjust loads of cluster buses + n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar + + return + + +if __name__ == "__main__": + if "snakemake" not in globals(): + import os + import sys + + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + path = "../submodules/pypsa-eur/scripts" + sys.path.insert(0, os.path.abspath(path)) + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "add_district_heating_subnodes", + simpl="", + clusters=44, + opts="", + ll="vopt", + sector_opts="none", + planning_horizons="2020", + run="KN2045_Bal_v4", + ) + + logger.info("Adding SysGF-specific functionality") + + n = pypsa.Network(snakemake.input.network) + heat_techs = gpd.read_file(snakemake.input.heating_technologies_nuts3).set_index( + "index" + ) + fernwaermeatlas = pd.read_excel( + snakemake.input.fernwaermeatlas, + sheet_name="Fernwärmeatlas_öffentlich", + ) + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + # Assign onshore region to heat techs based on geometry + heat_techs["cluster"] = heat_techs.apply( + lambda x: regions_onshore.geometry.contains(x.geometry).idxmax(), + axis=1, + ) + + subnodes = prepare_subnodes( + fernwaermeatlas, + regions_onshore, + heat_techs, + head=snakemake.params.district_heating["add_subnodes"], + ) + subnodes.to_file(snakemake.output.district_heating_subnodes, driver="GeoJSON") + + add_subnodes(n, subnodes) + + n.export_to_netcdf(snakemake.output.network) diff --git a/workflow/scripts/build_existing_chp_de.py b/workflow/scripts/build_existing_chp_de.py index 5c0274ad..c48d3caf 100644 --- a/workflow/scripts/build_existing_chp_de.py +++ b/workflow/scripts/build_existing_chp_de.py @@ -15,6 +15,7 @@ import os import sys +import geopandas as gpd import pandas as pd import pypsa from powerplantmatching.export import map_country_bus @@ -203,13 +204,51 @@ def BP(cap, year): return CHP_de +def assign_subnode(CHP_de, subnodes): + """ + Assign subnodes to the CHP plants based on their location. + """ + + # Make a geodataframe from CHP_de using the lat and lon columns + CHP_de = gpd.GeoDataFrame( + CHP_de, geometry=gpd.points_from_xy(CHP_de.lon, CHP_de.lat) + ) + CHP_de.crs = subnodes.crs + # Set nuts_3 shape wkt column as geometry + subnodes["geometry"] = gpd.GeoSeries.from_wkt(subnodes["nuts3_shape"]) + subnodes.drop("nuts3_shape", axis=1, inplace=True) + subnodes.index.rename("city", inplace=True) + + # Assign subnode to CHP plants based on the nuts3 region + CHP_de = CHP_de.sjoin(subnodes, how="left", predicate="within") + CHP_de["subnode"] = CHP_de["cluster"] + " " + CHP_de["city"] + CHP_de.drop(["city", "cluster"], axis=1, inplace=True) + + return CHP_de + + if __name__ == "__main__": if "snakemake" not in globals(): + import os + + # Change directory to current script + os.chdir(os.path.dirname(os.path.abspath(__file__))) + path = "../submodules/pypsa-eur/scripts" sys.path.insert(0, os.path.abspath(path)) from _helpers import mock_snakemake - snakemake = mock_snakemake("build_existing_chp_de") + snakemake = mock_snakemake( + "build_existing_chp_de", + simpl="", + clusters=44, + opts="", + ll="vopt", + sector_opts="none", + planning_horizons="2020", + run="KN2045_Bal_v4", + ) + # snakemake = mock_snakemake("build_existing_chp_de") logging.basicConfig(level=snakemake.config["logging"]["level"]) @@ -233,4 +272,11 @@ def BP(cap, year): substations = bn.buses.query("substation_lv") CHP_de = map_country_bus(CHP_de, substations) + if snakemake.params.add_district_heating_subnodes: + subnodes = gpd.read_file( + snakemake.input.district_heating_subnodes, + columns=["Stadt", "cluster", "nuts3_shape"], + ).set_index("Stadt") + CHP_de = assign_subnode(CHP_de, subnodes) + CHP_de.to_csv(snakemake.output.german_chp, index=False) diff --git a/workflow/scripts/modify_dh_systems.py b/workflow/scripts/modify_dh_systems.py deleted file mode 100644 index dc379255..00000000 --- a/workflow/scripts/modify_dh_systems.py +++ /dev/null @@ -1,336 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -logger = logging.getLogger(__name__) -import json - -import geopandas as gpd -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import pypsa -from shapely.geometry import Point - - -def load_egon(): - """ - Load and prepares the egon data about district heating in Germany on NUTS3 - level. - - Returns: - GeoDataFrame: A GeoDataFrame containing the processed egon data. - """ - - nuts3 = gpd.read_file(snakemake.input.nuts3)[ - ["index", "pop", "geometry"] - ] # Keep only necessary columns - - internal_id = { - 9: "Hard coal", - 10: "Brown coal", - 11: "Natural gas", - 34: "Heating oil", - 35: "Biomass (solid)", - 68: "Ambient heating", - 69: "Solar heat", - 71: "District heating", - 72: "Electrical energy", - 218: "Biomass (excluding wood, biogas)", - } - - df = pd.read_json(snakemake.input.fn) - id_region = pd.read_json(snakemake.input.fn_map) - - df["internal_id"] = df["internal_id"].apply(lambda x: x[0]) - # df = df[df["internal_id"] == 71] # Keep only rows with district heating - - df["nuts3"] = df.id_region.map( - id_region.set_index(id_region.id_region_from).kuerzel_to - ) - - heat_tech_per_region = df.groupby([df.nuts3, df.internal_id]).sum().value.unstack() - heat_tech_per_region.rename(columns=internal_id, inplace=True) - - egon_df = heat_tech_per_region.merge(nuts3, left_on="nuts3", right_on="index") - egon_gdf = gpd.GeoDataFrame(egon_df) # Convert merged DataFrame to GeoDataFrame - egon_gdf = egon_gdf.to_crs("EPSG:4326") - - return egon_gdf - - -def update_urban_loads(n_pre, egon_gdf): - """ - Update district heating demands of clusters according to shares in egon - data on NUTS3 level for Germany. - - Other heat loads are adjusted accordingly to ensure consistency of - the nodal heat demand. - """ - - n = n_pre.copy() - regions_onshore = gpd.read_file( - snakemake.input.regions_onshore - ) # shared resources true - regions_onshore.set_index("name", inplace=True) - # Map NUTS3 regions of egon data to corresponding clusters according to maximum overlap - - egon_gdf["cluster"] = egon_gdf.apply( - lambda x: regions_onshore.geometry.intersection(x.geometry).area.idxmax(), - axis=1, - ) - - # Calculate nodal DH shares according to households and modify index - egon_gdf_clustered = egon_gdf.groupby("cluster").sum(numeric_only=True) - nodal_dh_shares = egon_gdf_clustered["District heating"] / egon_gdf_clustered.drop( - "pop", axis=1 - ).sum(axis=1) - - nodal_dh_shares.index += " urban central heat" - - # District heating demands by cluster in German nodes before heat distribution - nodal_uc_demand = ( - n.loads_t.p_set.filter(regex="DE.*urban central heat") - .apply(lambda c: c * n.snapshot_weightings.generators) - .sum() - .div( - 1 + snakemake.config["sector"]["district_heating"]["district_heating_loss"] - ) - ) - - nodal_uc_losses = ( - nodal_uc_demand - - n.loads_t.p_set.filter(regex="DE.*urban central heat") - .apply(lambda c: c * n.snapshot_weightings.generators) - .sum() - ) - - # Sum of rural and urban heat demand - nodal_heat_demand = ( - n.loads_t.p_set.filter(regex="DE.*heat$") - .apply(lambda c: c * n.snapshot_weightings.generators) - .sum() - .sub(nodal_uc_losses, fill_value=0) - ) - - # Modify index of nodal_heat demand to align with urban central loads and aggregate loads - nodal_heat_demand.index = nodal_heat_demand.index.str.replace( - "decentral", "central" - ).str.replace("rural", "urban central") - - nodal_heat_demand = nodal_heat_demand.groupby(nodal_heat_demand.index).sum() - - # Old district heating share - nodal_uc_shares = nodal_uc_demand / nodal_heat_demand - - # Scaling factor for update of urban central heat loads - - scaling_factor = nodal_dh_shares / nodal_uc_shares - scaling_factor.dropna( - inplace=True - ) # To deal with shape anomaly described in https://github.com/PyPSA/pypsa-eur/issues/1100 - - # Update urban heat loads changing distribution and restoring old scale - old_uc_loads = n.loads_t.p_set.filter(regex="DE.*urban central heat") - new_uc_loads = ( - n.loads_t.p_set.filter(regex="DE.*urban central heat") * scaling_factor - ) - restore_scalar = new_uc_loads.sum().sum() / old_uc_loads.sum().sum() - new_uc_loads = new_uc_loads / restore_scalar - diff_update = new_uc_loads - old_uc_loads - diff_update.columns = diff_update.columns.str.replace("central", "decentral") - - n_pre.loads_t.p_set[new_uc_loads.columns] = new_uc_loads - n_pre.loads_t.p_set[diff_update.columns] -= diff_update - - return - - -def prepare_subnodes(egon_gdf, head=40): - """ - Prepare subnodes for the network based on the Triebs data and the egon - data. - """ - # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities - - # Load and prepare Triebs data - dh_areas_triebs = pd.read_excel( - snakemake.input.triebs, - sheet_name="Staedte", - ) - # convert dataframe dh_areas_triebs to geopandas dataframe using the Latirude and Longitude columns for geometry column as point coordinates - dh_areas_triebs["geometry"] = gpd.points_from_xy( - dh_areas_triebs["Longitude"], dh_areas_triebs["Latitude"] - ) - dh_areas_triebs = gpd.GeoDataFrame(dh_areas_triebs, geometry="geometry") - - # Keep only n largest district heating networks according to head parameter - to_keep = ["Stadtname", "NUTS3", "Einwohnerzahl [-]", "geometry"] - - dh_areas_triebs = dh_areas_triebs.sort_values( - by="Einwohnerzahl [-]", ascending=False - ).head(head)[to_keep] - - # Merge merged_gdf with dh_areas_triebs using the nuts3 id - merged_gdf = dh_areas_triebs.merge( - egon_gdf, left_on="NUTS3", right_on="index", how="left" - ) - # DH systems without matching NUTS3 id the surrounding region is assigned using the geometries - merged_gdf.loc[merged_gdf.geometry_y.isna(), "geometry_y"] = merged_gdf.loc[ - merged_gdf.geometry_y.isna() - ].apply( - lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_x), "geometry" - ].item(), - axis=1, - ) - merged_gdf.loc[merged_gdf["index"].isna(), "index"] = merged_gdf.loc[ - merged_gdf["index"].isna() - ].apply( - lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_x), "index" - ].item(), - axis=1, - ) - merged_gdf.loc[merged_gdf["cluster"].isna(), "cluster"] = merged_gdf.loc[ - merged_gdf["cluster"].isna() - ].apply( - lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_x), "cluster" - ].item(), - axis=1, - ) - merged_gdf.loc[ - merged_gdf["District heating"].isna(), "District heating" - ] = merged_gdf.loc[merged_gdf["District heating"].isna()].apply( - lambda x: egon_gdf.loc[ - egon_gdf.geometry.contains(x.geometry_x), "District heating" - ].item(), - axis=1, - ) - - # Intraregional distribution key according to population for NUTS3 regions with multiple DH systems - merged_gdf["intrareg_dist_key"] = merged_gdf.apply( - lambda reg: reg["Einwohnerzahl [-]"] - / merged_gdf.loc[ - merged_gdf["index"] == reg["index"], "Einwohnerzahl [-]" - ].sum(), - axis=1, - ).sort_values() - # Multiply column District heating with distribution key - merged_gdf["dh_hh_subnode"] = ( - merged_gdf["District heating"] * merged_gdf["intrareg_dist_key"] - ) - - # Convert district heating counts to percentage of cluster demand - - merged_gdf["demand_share_subnode"] = merged_gdf.apply( - lambda x: x["dh_hh_subnode"] - / egon_gdf.loc[egon_gdf.cluster == x.cluster, "District heating"].sum(), - axis=1, - ) - - return merged_gdf - - -def add_subnodes(n, subnodes, head=40): - """ - Add subnodes to the network and adjust loads and capacities accordingly. - """ - - n_sub = n.copy() - - # Add subnodes to network - for idx, row in subnodes.iterrows(): - name = f'{row["cluster"]} {row["Stadtname"]} urban central heat' - - # Add buses - n.madd( - "Bus", - [name], - y=row.geometry_x.y, - x=row.geometry_x.x, - country="DE", - location=row["cluster"], - carrier="urban central heat", - unit="MWh_th", - ) - - # Add heat loads - heat_load = row["demand_share_subnode"] * n.loads_t.p_set.filter( - regex=f"{row['cluster']} urban central heat" - ).rename( - { - f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadtname']} urban central heat" - }, - axis=1, - ) - n.madd( - "Load", - [name], - bus=name, - p_set=heat_load, - carrier="urban central heat", - location=row["cluster"], - profile=row["cluster"], - ) - - # Adjust loads of cluster buses - for idx, row in ( - subnodes.groupby("cluster", as_index=False).sum(numeric_only=True).iterrows() - ): - n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= ( - 1 - row["demand_share_subnode"] - ) - - return - - -def modify_chps(chps): - """ - Modify the CHP dataframe to include the subnodes. - """ - - chps["subnode"] = chps.apply( - lambda x: f'{x["cluster"]} {x["Stadtname"]} urban central heat', axis=1 - ) - - chps["capacity"] = chps["capacity"] * chps["demand_share_subnode"] - - return chps - - -if __name__ == "__main__": - if "snakemake" not in globals(): - import os - import sys - - os.chdir(os.path.dirname(os.path.abspath(__file__))) - - path = "../submodules/pypsa-eur/scripts" - sys.path.insert(0, os.path.abspath(path)) - from _helpers import mock_snakemake - - snakemake = mock_snakemake( - "modify_dh_systems", - simpl="", - clusters=22, - opts="", - ll="vopt", - sector_opts="none", - planning_horizons="2020", - run="KN2045_Bal_v4", - ) - logger.info("Adding SysGF-specific functionality") - - n = pypsa.Network(snakemake.input.network) - egon_gdf = load_egon() - update_urban_loads(n, egon_gdf) - chps = pd.read_csv(snakemake.input.german_chp) - - if snakemake.params.add_subnodes_de: - subnodes = prepare_subnodes(egon_gdf, snakemake.params.add_subnodes_de) - add_subnodes(n, subnodes) - modify_chps(chps, subnodes) - - n.export_to_netcdf(snakemake.output.network) - chps.to_csv(snakemake.output.german_chp, index=False) From 99448783cdf0ce0f8a228f695b8b548bbf00bb41 Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 21 Aug 2024 10:18:31 +0200 Subject: [PATCH 08/30] fixed nodal EB with gas boiler; fixed snakemake issues --- workflow/Snakefile | 63 ++++++++++++------- .../scripts/add_district_heating_subnodes.py | 16 ++++- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index e0f63abd..25596e16 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -196,26 +196,32 @@ rule build_egon_data: "scripts/build_egon_data.py" -rule add_district_heating_subnodes: - params: - district_heating=config_provider("sector", "district_heating"), - input: - network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), - nuts3=resources("nuts3_shapes.geojson"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - fernwaermeatlas="data/fernwaermeatlas/Fernwärmeatlas_öffentlich.xlsx", - output: - network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", - district_heating_subnodes=resources( - "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" - ), - resources: - mem_mb=1000, - script: - "scripts/add_district_heating_subnodes.py" +if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ + "district_heating" +].get("add_subnodes", True): + + rule add_district_heating_subnodes: + params: + district_heating=config_provider("sector", "district_heating"), + input: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), + nuts3=resources("nuts3_shapes.geojson"), + regions_onshore=resources( + "regions_onshore_elec_s{simpl}_{clusters}.geojson" + ), + fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", + output: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", + district_heating_subnodes=resources( + "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + ), + resources: + mem_mb=1000, + script: + "scripts/add_district_heating_subnodes.py" ruleorder: modify_district_heat_share > build_district_heat_share @@ -362,8 +368,12 @@ rule build_existing_chp_de: keep_local=True, ), busmap=resources("networks/base.nc"), - district_heating_subnodes=resources( - "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + district_heating_subnodes=( + resources( + "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + ) + if config["sector"]["district_heating"].get("add_subnodes", True) + else [] ), output: german_chp=resources( @@ -381,8 +391,13 @@ use rule add_existing_baseyear from pypsaeur with: ), input: **{k: v for k, v in rules.add_existing_baseyear.input.items() if k != "network"}, - network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", + network=( + RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" + if config["sector"]["district_heating"].get("add_subnodes", True) + else RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + ), custom_powerplants=resources( "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 50563287..b1aa068d 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -19,7 +19,9 @@ def encode_utf8(city_name): def prepare_subnodes(subnodes, regions_onshore, heat_techs, head=40): # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities - + # If head is boolean set it to 40 for default behavior + if isinstance(head, bool): + head = 40 # Keep only n largest district heating networks according to head parameter subnodes = subnodes.sort_values( by="Wärmeeinspeisung in GWh/a", ascending=False @@ -136,6 +138,18 @@ def add_subnodes(n, subnodes): profile=row["cluster"], ) + n.madd( + "Generator", + [f"{name} gas boiler"], + bus=name, + carrier="gas", + p_nom_extendable=True, + p_nom_max=1e6, + capital_cost=10000, + marginal_cost=25, + efficiency=1, + ) + # Adjust loads of cluster buses n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar From c7ec368c660ff4b9d8393a2e38e46a34afdf3c42 Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 21 Aug 2024 11:41:30 +0200 Subject: [PATCH 09/30] changed input for add_brownfield --- workflow/Snakefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/workflow/Snakefile b/workflow/Snakefile index 25596e16..abce09e9 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -403,6 +403,18 @@ use rule add_existing_baseyear from pypsaeur with: ), +use rule add_brownfield from pypsaeur with: + input: + **{k: v for k, v in rules.add_brownfield.input.items() if k != "network"}, + network=( + RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" + if config["sector"]["district_heating"].get("add_subnodes", True) + else RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + ), + + use rule build_existing_heating_distribution from pypsaeur with: input: **{ From 1fbecbcae9ddeb229484395834303bb9cc89d37a Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 21 Aug 2024 16:41:05 +0200 Subject: [PATCH 10/30] geolocations from file instead of geocoding --- workflow/Snakefile | 1 + .../scripts/add_district_heating_subnodes.py | 41 ++++++------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index abce09e9..f88b24e0 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -212,6 +212,7 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ "regions_onshore_elec_s{simpl}_{clusters}.geojson" ), fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", + cities="data/fernwaermeatlas/cities_geolocations.geojson", output: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index b1aa068d..c6908691 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -17,43 +17,26 @@ def encode_utf8(city_name): return city_name.encode("utf-8") -def prepare_subnodes(subnodes, regions_onshore, heat_techs, head=40): +def prepare_subnodes(subnodes, cities, regions_onshore, heat_techs, head=40): # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities # If head is boolean set it to 40 for default behavior if isinstance(head, bool): head = 40 - # Keep only n largest district heating networks according to head parameter - subnodes = subnodes.sort_values( - by="Wärmeeinspeisung in GWh/a", ascending=False - ).head(head) - - # Create a Nominatim object - nominatim = Nominatim(user_agent="cityEncoder") - subnodes["lat"] = np.nan - subnodes["lon"] = np.nan subnodes["Stadt"] = subnodes["Stadt"].str.split("_").str[0] # Drop duplicates if Gelsenkirchen, Kiel, or Flensburg is included and keep the one with higher Wärmeeinspeisung in GWh/a subnodes = subnodes.drop_duplicates(subset="Stadt", keep="first") - # Get the location of all cities in the dataset (Stadt column) and write them to column "location" do it as try ecxept to avoid errors - for i, row in subnodes.iterrows(): - try: - location = nominatim.geocode(encode_utf8(row["Stadt"]), country_codes="DE") - # Extract the latitude and longitude from the location column - subnodes.at[i, "lat"] = location.latitude - subnodes.at[i, "lon"] = location.longitude - sleep_sec = 1 - sleep(randint(1 * 100, sleep_sec * 100) / 100) - except: - logger.info(f"Location not found for {row['Stadt']}") - pass - - # Make a shapely point object from the lat and lon columns - subnodes["geometry"] = gpd.points_from_xy(subnodes["lon"], subnodes["lat"]) - # Drop rows with missing geometry - logger.info("Cities without locations are dropped.") + # Keep only n largest district heating networks according to head parameter + subnodes = subnodes.sort_values( + by="Wärmeeinspeisung in GWh/a", ascending=False + ).head(head) + + subnodes["geometry"] = subnodes["Stadt"].apply( + lambda s: cities.loc[cities["Stadt"] == s, "geometry"].values[0] + ) + subnodes = subnodes.dropna(subset=["geometry"]) # Convert the DataFrame to a GeoDataFrame subnodes = gpd.GeoDataFrame(subnodes, crs="EPSG:4326") @@ -170,7 +153,7 @@ def add_subnodes(n, subnodes): snakemake = mock_snakemake( "add_district_heating_subnodes", simpl="", - clusters=44, + clusters=22, opts="", ll="vopt", sector_opts="none", @@ -188,6 +171,7 @@ def add_subnodes(n, subnodes): snakemake.input.fernwaermeatlas, sheet_name="Fernwärmeatlas_öffentlich", ) + cities = gpd.read_file(snakemake.input.cities) regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") # Assign onshore region to heat techs based on geometry heat_techs["cluster"] = heat_techs.apply( @@ -197,6 +181,7 @@ def add_subnodes(n, subnodes): subnodes = prepare_subnodes( fernwaermeatlas, + cities, regions_onshore, heat_techs, head=snakemake.params.district_heating["add_subnodes"], From c97e56e7f888353dbe8accab5b869b3bbcd53e15 Mon Sep 17 00:00:00 2001 From: cpschau Date: Fri, 23 Aug 2024 15:05:07 +0200 Subject: [PATCH 11/30] added links and stores except from heat pumps; adapted snakemake inputs for add_brownfield --- workflow/Snakefile | 9 ++++ .../scripts/add_district_heating_subnodes.py | 50 ++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index f88b24e0..81b138bb 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -404,8 +404,17 @@ use rule add_existing_baseyear from pypsaeur with: ), +def input_profile_tech_brownfield(w): + return { + f"profile_{tech}": resources(f"profile_{tech}.nc") + for tech in config_provider("electricity", "renewable_carriers")(w) + if tech != "hydro" + } + + use rule add_brownfield from pypsaeur with: input: + unpack(input_profile_tech_brownfield), **{k: v for k, v in rules.add_brownfield.input.items() if k != "network"}, network=( RESULTS diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index c6908691..04bc00c9 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -118,24 +118,62 @@ def add_subnodes(n, subnodes): p_set=heat_load, carrier="urban central heat", location=row["cluster"], - profile=row["cluster"], ) + # Adjust loads of cluster buses + n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar + + # Replicate district heating stores and links of mother node for subnodes + # TODO: Add heat pump links + + n.madd( + "Bus", + [f"{row['cluster']} {row['Stadt']} urban central water tanks"], + location=row["cluster"], + carrier="urban central water tanks", + unit="MWh_th", + ) + + stores = ( + n.stores.filter(like=f"{row['cluster']} urban central", axis=0) + .reset_index() + .replace( + { + f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + }, + regex=True, + ) + .set_index("Store") + ) + n.madd("Store", stores.index, **stores) + + links = ( + n.links.loc[~n.links.carrier.str.contains("heat pump")] + .filter(like=f"{row['cluster']} urban central", axis=0) + .reset_index() + .replace( + { + f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + }, + regex=True, + ) + .set_index("Link") + ) + n.madd("Link", links.index, **links) + + # Add artificial gas boiler to subnode n.madd( "Generator", - [f"{name} gas boiler"], + [f"{name} load shedding"], bus=name, carrier="gas", p_nom_extendable=True, p_nom_max=1e6, capital_cost=10000, - marginal_cost=25, + marginal_cost=200, efficiency=1, ) - # Adjust loads of cluster buses - n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar - return From fe47933db568852d7135570680b5729ed3292f43 Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 27 Aug 2024 10:11:02 +0200 Subject: [PATCH 12/30] add ASHP --- workflow/Snakefile | 15 +++++- .../scripts/add_district_heating_subnodes.py | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 81b138bb..d60f2d29 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -213,12 +213,16 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ ), fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", cities="data/fernwaermeatlas/cities_geolocations.geojson", + cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), output: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", district_heating_subnodes=resources( "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), + cop_air_total_extended=resources( + "cop_air_total_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + ), resources: mem_mb=1000, script: @@ -391,7 +395,11 @@ use rule add_existing_baseyear from pypsaeur with: "sector", "district_heating", "add_subnodes" ), input: - **{k: v for k, v in rules.add_existing_baseyear.input.items() if k != "network"}, + **{ + k: v + for k, v in rules.add_existing_baseyear.input.items() + if k != "network" and k != "cop_air_total" + }, network=( RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" @@ -399,6 +407,11 @@ use rule add_existing_baseyear from pypsaeur with: else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), + cop_air_total=( + resources("cop_air_total_elec_s{simpl}_{clusters}-extended.nc") + if config["sector"]["district_heating"].get("add_subnodes", True) + else resources("cop_air_total_elec_s{simpl}_{clusters}.nc") + ), custom_powerplants=resources( "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 04bc00c9..2e6642db 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -9,6 +9,7 @@ import numpy as np import pandas as pd import pypsa +import xarray as xr from geopy.geocoders import Nominatim @@ -161,6 +162,28 @@ def add_subnodes(n, subnodes): ) n.madd("Link", links.index, **links) + # Add heat pumps to subnode + heat_pumps = ( + n.links.loc[n.links.carrier.str.contains("heat pump")] + .reset_index() + .replace( + { + f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + }, + regex=True, + ) + .set_index("Link") + ).drop("efficiency", axis=1) + heat_pumps_t = n.links_t.efficiency.filter( + regex=f"{row['cluster']} urban central.*heat pump" + ).rename( + { + f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + }, + axis=1, + ) + n.madd("Link", heat_pumps.index, efficiency=heat_pumps_t, **heat_pumps) + # Add artificial gas boiler to subnode n.madd( "Generator", @@ -177,6 +200,30 @@ def add_subnodes(n, subnodes): return +def extend_cops(cops, subnodes): + """ + Extend COPs by subnodes mirroring the timeseries of the corresponding + mother node. + """ + cops_extended = cops.copy() + + # Iterate over the DataFrame rows + for _, row in subnodes.iterrows(): + cluster_name = row["cluster"] + city_name = row["city"] + + # Select the xarray entry where name matches the cluster + selected_entry = cops.sel(name=cluster_name) + + # Rename the selected entry + renamed_entry = selected_entry.assign_coords(name=f"{cluster_name}_{city_name}") + + # Combine the renamed entry with the extended dataset + cops_extended = xr.concat([cops_extended, renamed_entry], dim="name") + + return cops_extended + + if __name__ == "__main__": if "snakemake" not in globals(): import os @@ -228,4 +275,8 @@ def add_subnodes(n, subnodes): add_subnodes(n, subnodes) + if snakemake.config["foresight"] == "myopic": + cops = xr.open_dataarray(snakemake.input.cop_air_total) + cops_extended = extend_cops(cops, subnodes) + cops_extended.to_netcdf(snakemake.output.cop_air_total_extended) n.export_to_netcdf(snakemake.output.network) From c081ff4099b767a76b276cfb0c31207a1b053ef0 Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 27 Aug 2024 11:06:08 +0200 Subject: [PATCH 13/30] fix bugs for heat pump integration --- workflow/Snakefile | 4 +++- workflow/scripts/add_district_heating_subnodes.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index d60f2d29..7ab12ed7 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -408,7 +408,9 @@ use rule add_existing_baseyear from pypsaeur with: + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), cop_air_total=( - resources("cop_air_total_elec_s{simpl}_{clusters}-extended.nc") + resources( + "cop_air_total_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + ) if config["sector"]["district_heating"].get("add_subnodes", True) else resources("cop_air_total_elec_s{simpl}_{clusters}.nc") ), diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 2e6642db..49d66b61 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -164,7 +164,7 @@ def add_subnodes(n, subnodes): # Add heat pumps to subnode heat_pumps = ( - n.links.loc[n.links.carrier.str.contains("heat pump")] + n.links.filter(regex=f"{row['cluster']} urban central.*heat pump", axis=0) .reset_index() .replace( { @@ -176,11 +176,10 @@ def add_subnodes(n, subnodes): ).drop("efficiency", axis=1) heat_pumps_t = n.links_t.efficiency.filter( regex=f"{row['cluster']} urban central.*heat pump" - ).rename( - { - f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" - }, - axis=1, + ) + heat_pumps_t.columns = heat_pumps_t.columns.str.replace( + f"{row['cluster']} urban central", + f"{row['cluster']} {row['Stadt']} urban central", ) n.madd("Link", heat_pumps.index, efficiency=heat_pumps_t, **heat_pumps) @@ -210,13 +209,13 @@ def extend_cops(cops, subnodes): # Iterate over the DataFrame rows for _, row in subnodes.iterrows(): cluster_name = row["cluster"] - city_name = row["city"] + city_name = row["Stadt"] # Select the xarray entry where name matches the cluster selected_entry = cops.sel(name=cluster_name) # Rename the selected entry - renamed_entry = selected_entry.assign_coords(name=f"{cluster_name}_{city_name}") + renamed_entry = selected_entry.assign_coords(name=f"{cluster_name} {city_name}") # Combine the renamed entry with the extended dataset cops_extended = xr.concat([cops_extended, renamed_entry], dim="name") From f5e8d2b11234c4f811339e36a30a9832ccdf3339 Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 18 Sep 2024 17:00:36 +0200 Subject: [PATCH 14/30] adjustments to cop netcdf and existing heating distribution --- workflow/Snakefile | 57 ++++++++++++------- .../scripts/add_district_heating_subnodes.py | 37 +++++++++++- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 68247bd8..858cd12b 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -62,18 +62,18 @@ from pathlib import Path data_dir = Path("workflow/submodules/pypsa-eur/data") -rule get_data: - output: - [ - str(Path("data") / p.relative_to(data_dir)) - for p in data_dir.rglob("*") - if p.is_file() - ], - shell: - """ - mkdir -p data - cp -nR {data_dir}/. data/ - """ +# rule get_data: +# output: +# [ +# str(Path("data") / p.relative_to(data_dir)) +# for p in data_dir.rglob("*") +# if p.is_file() +# ], +# shell: +# """ +# mkdir -p data +# cp -nR {data_dir}/. data/ +# """ rule clean: @@ -200,6 +200,8 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ "district_heating" ].get("add_subnodes", True): + ruleorder: add_district_heating_subnodes > prepare_sector_network + rule add_district_heating_subnodes: params: district_heating=config_provider("sector", "district_heating"), @@ -213,15 +215,21 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ ), fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", cities="data/fernwaermeatlas/cities_geolocations.geojson", - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + existing_heating_distribution=resources( + "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + ), output: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", district_heating_subnodes=resources( "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), - cop_air_total_extended=resources( - "cop_air_total_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + cop_profiles_extended=resources( + "cop_profiles_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + ), + existing_heating_distribution_extended=resources( + "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" ), resources: mem_mb=1000, @@ -398,7 +406,9 @@ use rule add_existing_baseyear from pypsaeur with: **{ k: v for k, v in rules.add_existing_baseyear.input.items() - if k != "network" and k != "cop_air_total" + if k != "network" + and k != "cop_profiles" + and k != "existing_heating_distribution" }, network=( RESULTS @@ -407,16 +417,25 @@ use rule add_existing_baseyear from pypsaeur with: else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), - cop_air_total=( + cop_profiles=( resources( - "cop_air_total_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + "cop_profiles_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" ) if config["sector"]["district_heating"].get("add_subnodes", True) - else resources("cop_air_total_elec_s{simpl}_{clusters}.nc") + else resources("cop_profiles_elec_s{simpl}_{clusters}.nc") ), custom_powerplants=resources( "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), + existing_heating_distribution=( + resources( + "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + ) + if config["sector"]["district_heating"].get("add_subnodes", True) + else resources( + "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + ) + ), def input_profile_tech_brownfield(w): diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 49d66b61..d5e7a6c7 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -220,9 +220,28 @@ def extend_cops(cops, subnodes): # Combine the renamed entry with the extended dataset cops_extended = xr.concat([cops_extended, renamed_entry], dim="name") + # Change dtype of the name dimension to string + cops_extended.coords["name"] = cops_extended.coords["name"].astype(str) + return cops_extended +def extend_heating_distribution(existing_heating_distribution, subnodes): + """ + Extend heating distribution by subnodes mirroring the distribution of the + corresponding mother node. + """ + # Merge the existing heating distribution with subnodes on the cluster name + mother_nodes = existing_heating_distribution.loc[subnodes.cluster.unique()] + mother_nodes["cities"] = subnodes.groupby("cluster")["Stadt"].apply(list) + # Explode the list of cities + mother_nodes = mother_nodes.explode(("cities", "")) + mother_nodes.index = mother_nodes.index + " " + mother_nodes[("cities", "")] + mother_nodes.drop(columns=("cities", ""), inplace=True) + + return mother_nodes + + if __name__ == "__main__": if "snakemake" not in globals(): import os @@ -237,7 +256,7 @@ def extend_cops(cops, subnodes): snakemake = mock_snakemake( "add_district_heating_subnodes", simpl="", - clusters=22, + clusters=27, opts="", ll="vopt", sector_opts="none", @@ -275,7 +294,19 @@ def extend_cops(cops, subnodes): add_subnodes(n, subnodes) if snakemake.config["foresight"] == "myopic": - cops = xr.open_dataarray(snakemake.input.cop_air_total) + cops = xr.open_dataarray(snakemake.input.cop_profiles) cops_extended = extend_cops(cops, subnodes) - cops_extended.to_netcdf(snakemake.output.cop_air_total_extended) + cops_extended.to_netcdf(snakemake.output.cop_profiles_extended) + + existing_heating_distribution = pd.read_csv( + snakemake.input.existing_heating_distribution, + header=[0, 1], + index_col=0, + ) + existing_heating_distribution_extended = extend_heating_distribution( + existing_heating_distribution, subnodes + ) + existing_heating_distribution_extended.to_csv( + snakemake.output.existing_heating_distribution_extended + ) n.export_to_netcdf(snakemake.output.network) From cd756641687e993c1b22a1430acd3839ba45c262 Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 19 Sep 2024 11:04:56 +0200 Subject: [PATCH 15/30] fix extension of existing_heating df --- workflow/scripts/add_district_heating_subnodes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index d5e7a6c7..26e978a5 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -238,8 +238,10 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): mother_nodes = mother_nodes.explode(("cities", "")) mother_nodes.index = mother_nodes.index + " " + mother_nodes[("cities", "")] mother_nodes.drop(columns=("cities", ""), inplace=True) - - return mother_nodes + existing_heating_distribution_extended = pd.concat( + [existing_heating_distribution, mother_nodes] + ) + return existing_heating_distribution_extended if __name__ == "__main__": From 5b9ed82460fb549d79b448438ad5d43ada936d7f Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 19 Sep 2024 18:17:29 +0200 Subject: [PATCH 16/30] fix snakemake issue for existing heating --- workflow/Snakefile | 20 ++++++++++++------- .../scripts/add_district_heating_subnodes.py | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 858cd12b..6454d47c 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -200,11 +200,13 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ "district_heating" ].get("add_subnodes", True): - ruleorder: add_district_heating_subnodes > prepare_sector_network + # ruleorder: prepare_sector_network > add_district_heating_subnodes + baseyear_value = config["scenario"]["planning_horizons"][0] rule add_district_heating_subnodes: params: district_heating=config_provider("sector", "district_heating"), + baseyear=config_provider("scenario", "planning_horizons", 0), input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", @@ -217,19 +219,23 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ cities="data/fernwaermeatlas/cities_geolocations.geojson", cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), existing_heating_distribution=resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + f"existing_heating_distribution_elec_s{{simpl}}_{{clusters}}_{baseyear_value}.csv" ), output: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc", + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc", district_heating_subnodes=resources( "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), cop_profiles_extended=resources( "cop_profiles_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" ), - existing_heating_distribution_extended=resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + existing_heating_distribution_extended=( + resources( + "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + ) + if baseyear_value == "{planning_horizons}" + else [] ), resources: mem_mb=1000, @@ -412,7 +418,7 @@ use rule add_existing_baseyear from pypsaeur with: }, network=( RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -452,7 +458,7 @@ use rule add_brownfield from pypsaeur with: **{k: v for k, v in rules.add_brownfield.input.items() if k != "network"}, network=( RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_dh.nc" + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 26e978a5..4c1244fc 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -300,6 +300,7 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): cops_extended = extend_cops(cops, subnodes) cops_extended.to_netcdf(snakemake.output.cop_profiles_extended) + if snakemake.wildcards.planning_horizons == str(snakemake.params["baseyear"]): existing_heating_distribution = pd.read_csv( snakemake.input.existing_heating_distribution, header=[0, 1], From 2d6949e42576fa2c48c73b8611c8557a4e4565d0 Mon Sep 17 00:00:00 2001 From: cpschau Date: Fri, 20 Sep 2024 13:58:45 +0200 Subject: [PATCH 17/30] dirty fix for workflow and optional output --- workflow/Snakefile | 9 ++++----- workflow/scripts/add_district_heating_subnodes.py | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 6454d47c..4d7b7b79 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -200,7 +200,6 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ "district_heating" ].get("add_subnodes", True): - # ruleorder: prepare_sector_network > add_district_heating_subnodes baseyear_value = config["scenario"]["planning_horizons"][0] rule add_district_heating_subnodes: @@ -223,7 +222,7 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ ), output: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc", + + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", district_heating_subnodes=resources( "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), @@ -234,7 +233,7 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" ) - if baseyear_value == "{planning_horizons}" + if baseyear_value != "{planning_horizons}" else [] ), resources: @@ -418,7 +417,7 @@ use rule add_existing_baseyear from pypsaeur with: }, network=( RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -458,7 +457,7 @@ use rule add_brownfield from pypsaeur with: **{k: v for k, v in rules.add_brownfield.input.items() if k != "network"}, network=( RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 4c1244fc..5d81576f 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -312,4 +312,8 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): existing_heating_distribution_extended.to_csv( snakemake.output.existing_heating_distribution_extended ) + else: + # write empty file to output + with open(snakemake.output.existing_heating_distribution_extended, "w") as f: + pass n.export_to_netcdf(snakemake.output.network) From 1a2ed801b94918df9aeef333bec402bfb2034bc3 Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 24 Sep 2024 10:41:34 +0200 Subject: [PATCH 18/30] match CHP by LAU shapes --- workflow/Snakefile | 4 ++++ workflow/scripts/add_district_heating_subnodes.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 4d7b7b79..72d0ac88 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -220,6 +220,10 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ existing_heating_distribution=resources( f"existing_heating_distribution_elec_s{{simpl}}_{{clusters}}_{baseyear_value}.csv" ), + lau=storage( + "https://gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson.zip", + keep_local=True, + ), output: network=RESULTS + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 5d81576f..831e5693 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -18,7 +18,7 @@ def encode_utf8(city_name): return city_name.encode("utf-8") -def prepare_subnodes(subnodes, cities, regions_onshore, heat_techs, head=40): +def prepare_subnodes(subnodes, cities, regions_onshore, lau, heat_techs, head=40): # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities # If head is boolean set it to 40 for default behavior if isinstance(head, bool): @@ -46,6 +46,13 @@ def prepare_subnodes(subnodes, cities, regions_onshore, heat_techs, head=40): subnodes["cluster"] = subnodes.apply( lambda x: regions_onshore.geometry.contains(x.geometry).idxmax(), axis=1 ) + subnodes["lau"] = subnodes.apply( + lambda x: lau.loc[lau.geometry.contains(x.geometry).idxmax(), "LAU_ID"], axis=1 + ) + subnodes["lau_shape"] = subnodes.apply( + lambda x: lau.loc[lau.geometry.contains(x.geometry).idxmax(), "geometry"].wkt, + axis=1, + ) subnodes["nuts3"] = subnodes.apply( lambda x: heat_techs.geometry.contains(x.geometry).idxmax(), axis=1, @@ -272,6 +279,10 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): heat_techs = gpd.read_file(snakemake.input.heating_technologies_nuts3).set_index( "index" ) + lau = gpd.read_file( + f"{snakemake.input.lau}!LAU_RG_01M_2021_3035.geojson", crs="EPSG:3035" + ).to_crs("EPSG:4326") + fernwaermeatlas = pd.read_excel( snakemake.input.fernwaermeatlas, sheet_name="Fernwärmeatlas_öffentlich", @@ -288,6 +299,7 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): fernwaermeatlas, cities, regions_onshore, + lau, heat_techs, head=snakemake.params.district_heating["add_subnodes"], ) From 913da0bc149d9ea819aa23a65d1f15300014136f Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 24 Sep 2024 15:50:27 +0200 Subject: [PATCH 19/30] added industry heat load --- .../scripts/add_district_heating_subnodes.py | 86 +++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 831e5693..933d604e 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -34,6 +34,12 @@ def prepare_subnodes(subnodes, cities, regions_onshore, lau, heat_techs, head=40 by="Wärmeeinspeisung in GWh/a", ascending=False ).head(head) + subnodes["yearly_heat_demand_MWh"] = subnodes["Wärmeeinspeisung in GWh/a"] * 1e3 + + logger.info( + f"The selected district heating networks have an overall yearly heat demand of {subnodes['yearly_heat_demand_MWh'].sum()} MWh/a. " + ) + subnodes["geometry"] = subnodes["Stadt"].apply( lambda s: cities.loc[cities["Stadt"] == s, "geometry"].values[0] ) @@ -89,50 +95,80 @@ def add_subnodes(n, subnodes): ) # Add heat loads + + uch_load_cluster = ( + n.snapshot_weightings.generators + @ n.loads_t.p_set[f"{row['cluster']} urban central heat"] + ) + lti_load_cluster = ( + n.loads.loc[f"{row['cluster']} low-temperature heat for industry", "p_set"] + * 8760 + ) + dh_load_cluster = uch_load_cluster + lti_load_cluster + lti_share = lti_load_cluster / dh_load_cluster + scalar = min( 1, - ( - row["Wärmeeinspeisung in GWh/a"] - * 1e3 - / n.loads_t.p_set.filter(regex=f"{row['cluster']} urban central heat") - .sum(axis=1) - .mul(n.snapshot_weightings.generators) - .sum() - ), - ) - lost_load = ( - row["Wärmeeinspeisung in GWh/a"] * 1e3 - - n.loads_t.p_set.filter(regex=f"{row['cluster']} urban central heat") - .sum(axis=1) - .mul(n.snapshot_weightings.generators) - .sum() + (row["yearly_heat_demand_MWh"] / dh_load_cluster), ) + + lost_load = row["yearly_heat_demand_MWh"] - dh_load_cluster + if scalar == 1: logger.info( f"District heating load of {row['Stadt']} exceeds load of its assigned cluster {row['cluster']}. {lost_load} MWh/a are disregarded." ) - heat_load = scalar * n.loads_t.p_set.filter( - regex=f"{row['cluster']} urban central heat" - ).rename( - { - f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadt']} urban central heat" - }, - axis=1, + uch_load = ( + scalar + * (1 - lti_share) + * n.loads_t.p_set.filter( + regex=f"{row['cluster']} urban central heat" + ).rename( + { + f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadt']} urban central heat" + }, + axis=1, + ) ) n.madd( "Load", [name], bus=name, - p_set=heat_load, + p_set=uch_load, carrier="urban central heat", location=row["cluster"], ) + lti_load = ( + scalar + * lti_share + * n.loads.filter( + regex=f"{row['cluster']} low-temperature heat for industry", axis=0 + ).p_set.rename( + { + f"{row['cluster']} low-temperature heat for industry": f"{row['cluster']} {row['Stadt']} low-temperature heat for industry" + }, + axis=0, + ) + ) + n.madd( + "Load", + [f"{name} low-temperature heat for industry"], + bus=name, + p_set=lti_load, + carrier="low-temperature heat for industry", + location=row["cluster"], + ) + # Adjust loads of cluster buses - n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar + n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar * ( + 1 - lti_share + ) + n.loads.loc[f'{row["cluster"]} low-temperature heat for industry', "p_set"] *= ( + 1 - scalar * lti_share + ) # Replicate district heating stores and links of mother node for subnodes - # TODO: Add heat pump links n.madd( "Bus", From 8dbb0cda84a5d64cfeb310b570808b7891d9a3d4 Mon Sep 17 00:00:00 2001 From: cpschau Date: Tue, 22 Oct 2024 17:48:55 +0200 Subject: [PATCH 20/30] make workflow run after merge --- workflow/Snakefile | 107 +++++++++++------- .../scripts/add_district_heating_subnodes.py | 48 ++++++-- workflow/scripts/build_existing_chp_de.py | 22 ++-- 3 files changed, 114 insertions(+), 63 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 72d0ac88..30bb7ba7 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -115,9 +115,9 @@ rule retrieve_ariadne_database: def input_profile_offwind(w): return { - f"profile_{tech}": resources(f"profile_{tech}.nc") + f"profile_{tech}": resources("profile_{clusters}_" + tech + ".nc") for tech in ["offwind-ac", "offwind-dc", "offwind-float"] - if (tech in config["electricity"]["renewable_carriers"]) + if (tech in config_provider("electricity", "renewable_carriers")(w)) } @@ -130,7 +130,7 @@ use rule prepare_sector_network from pypsaeur with: if k != "district_heat_share" }, district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}-modified.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}-modified.csv" ), @@ -170,13 +170,15 @@ rule build_mobility_demand: leitmodelle=config_provider("iiasa_database", "leitmodelle"), input: ariadne=resources("ariadne_database.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), output: mobility_demand=resources( - "mobility_demand_aladin_{simpl}_{clusters}_{planning_horizons}.csv" + "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" ), resources: mem_mb=1000, + log: + logs("build_mobility_demand_{clusters}_{planning_horizons}.log"), script: "scripts/build_mobility_demand.py" @@ -208,17 +210,17 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ baseyear=config_provider("scenario", "planning_horizons", 0), input: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), nuts3=resources("nuts3_shapes.geojson"), regions_onshore=resources( - "regions_onshore_elec_s{simpl}_{clusters}.geojson" + "regions_onshore_base_s_{clusters}.geojson" ), fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", cities="data/fernwaermeatlas/cities_geolocations.geojson", - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), existing_heating_distribution=resources( - f"existing_heating_distribution_elec_s{{simpl}}_{{clusters}}_{baseyear_value}.csv" + f"existing_heating_distribution_base_s_{{clusters}}_{baseyear_value}.csv" ), lau=storage( "https://gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson.zip", @@ -226,16 +228,16 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ ), output: network=RESULTS - + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", district_heating_subnodes=resources( - "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + "district_heating_subnodes_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), cop_profiles_extended=resources( - "cop_profiles_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + "cop_profiles_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" ), existing_heating_distribution_extended=( resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + "existing_heating_distribution_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" ) if baseyear_value != "{planning_horizons}" else [] @@ -254,13 +256,13 @@ rule modify_district_heat_share: district_heating=config_provider("sector", "district_heating"), input: heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}.csv" ), output: district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}-modified.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}-modified.csv" ), resources: mem_mb=1000, @@ -293,30 +295,49 @@ rule modify_prenetwork: land_transport_electric_share=config_provider( "sector", "land_transport_electric_share" ), + onshore_nep_force=config_provider("onshore_nep_force"), + offshore_nep_force=config_provider("offshore_nep_force"), + shipping_methanol_efficiency=config_provider( + "sector", "shipping_methanol_efficiency" + ), + shipping_oil_efficiency=config_provider("sector", "shipping_oil_efficiency"), + shipping_methanol_share=config_provider("sector", "shipping_methanol_share"), + mwh_meoh_per_tco2=config_provider("sector", "MWh_MeOH_per_tCO2"), input: + costs_modifications="ariadne-data/costs_{planning_horizons}-modifications.csv", network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", wkn=( - resources("wasserstoff_kernnetz_elec_s{simpl}_{clusters}.csv") + resources("wasserstoff_kernnetz_base_s_{clusters}.csv") if config_provider("wasserstoff_kernnetz", "enable") else [] ), costs=resources("costs_{planning_horizons}.csv"), aladin_demand=resources( - "mobility_demand_aladin_{simpl}_{clusters}_{planning_horizons}.csv" + "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" ), - transport_data=resources("transport_data_s{simpl}_{clusters}.csv"), + transport_data=resources("transport_data_s_{clusters}.csv"), biomass_potentials=resources( - "biomass_potentials_s{simpl}_{clusters}_{planning_horizons}.csv" + "biomass_potentials_s_{clusters}_{planning_horizons}.csv" ), industrial_demand=resources( - "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), + pop_weighted_energy_totals=resources( + "pop_weighted_energy_totals_s_{clusters}.csv" + ), + shipping_demand=resources("shipping_demand_s_{clusters}.csv"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), + offshore_connection_points="ariadne-data/offshore_connection_points.csv", output: network=RESULTS - + "prenetworks-final/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-final/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", resources: mem_mb=1000, + log: + RESULTS + + "logs/modify_prenetwork_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", script: "scripts/modify_prenetwork.py" @@ -341,7 +362,7 @@ use rule solve_sector_network_myopic from pypsaeur with: if k != "network" }, network=RESULTS - + "prenetworks-final/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-final/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", co2_totals_name=resources("co2_totals.csv"), @@ -389,17 +410,17 @@ rule build_existing_chp_de: "https://raw.githubusercontent.com/WZBSocialScienceCenter/plz_geocoord/master/plz_geocoord.csv", keep_local=True, ), - busmap=resources("networks/base.nc"), + regions=resources("regions_onshore_base_s_{clusters}.geojson"), district_heating_subnodes=( resources( - "district_heating_subnodes_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + "district_heating_subnodes_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ) if config["sector"]["district_heating"].get("add_subnodes", True) else [] ), output: german_chp=resources( - "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" + "german_chp_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), script: "scripts/build_existing_chp_de.py" @@ -421,35 +442,35 @@ use rule add_existing_baseyear from pypsaeur with: }, network=( RESULTS - + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + + "prenetworks/base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), cop_profiles=( resources( - "cop_profiles_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + "cop_profiles_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" ) if config["sector"]["district_heating"].get("add_subnodes", True) - else resources("cop_profiles_elec_s{simpl}_{clusters}.nc") + else resources("cop_profiles_base_s_{clusters}.nc") ), custom_powerplants=resources( - "german_chp_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" + "german_chp_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), existing_heating_distribution=( resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + "existing_heating_distribution_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" ) if config["sector"]["district_heating"].get("add_subnodes", True) else resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv" ) ), def input_profile_tech_brownfield(w): return { - f"profile_{tech}": resources(f"profile_{tech}.nc") + f"profile_{tech}": resources("profile_{clusters}_" + tech + ".nc") for tech in config_provider("electricity", "renewable_carriers")(w) if tech != "hydro" } @@ -461,10 +482,10 @@ use rule add_brownfield from pypsaeur with: **{k: v for k, v in rules.add_brownfield.input.items() if k != "network"}, network=( RESULTS - + "prenetworks/elec-extended_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + + "prenetworks/base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" if config["sector"]["district_heating"].get("add_subnodes", True) else RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), @@ -558,11 +579,11 @@ rule cluster_wasserstoff_kernnetz: kernnetz=config_provider("wasserstoff_kernnetz"), input: cleaned_h2_network=resources("wasserstoff_kernnetz.csv"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), output: clustered_h2_network=resources( - "wasserstoff_kernnetz_elec_s{simpl}_{clusters}.csv" + "wasserstoff_kernnetz_base_s_{clusters}.csv" ), script: "scripts/cluster_wasserstoff_kernnetz.py" @@ -596,14 +617,14 @@ rule export_ariadne_variables: template=resources("template_ariadne_database.xlsx"), industry_demands=expand( resources( - "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), **config["scenario"], allow_missing=True, ), networks=expand( RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", **config["scenario"], allow_missing=True, ), @@ -707,7 +728,7 @@ rule build_scenarios: rule check_sector_ratios: input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", log: "logs/check_sector_ratios.log", script: diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 933d604e..ae3c3588 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -89,7 +89,7 @@ def add_subnodes(n, subnodes): y=row.geometry.y, x=row.geometry.x, country="DE", - location=row["cluster"], + location=f"{row['cluster']} {row['Stadt']}", carrier="urban central heat", unit="MWh_th", ) @@ -136,7 +136,7 @@ def add_subnodes(n, subnodes): bus=name, p_set=uch_load, carrier="urban central heat", - location=row["cluster"], + location=f"{row['cluster']} {row['Stadt']}", ) lti_load = ( @@ -153,11 +153,11 @@ def add_subnodes(n, subnodes): ) n.madd( "Load", - [f"{name} low-temperature heat for industry"], + [f"{row['cluster']} {row['Stadt']} low-temperature heat for industry"], bus=name, p_set=lti_load, carrier="low-temperature heat for industry", - location=row["cluster"], + location=f"{row['cluster']} {row['Stadt']}", ) # Adjust loads of cluster buses @@ -173,7 +173,7 @@ def add_subnodes(n, subnodes): n.madd( "Bus", [f"{row['cluster']} {row['Stadt']} urban central water tanks"], - location=row["cluster"], + location=f"{row['cluster']} {row['Stadt']}", carrier="urban central water tanks", unit="MWh_th", ) @@ -275,12 +275,36 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): corresponding mother node. """ # Merge the existing heating distribution with subnodes on the cluster name - mother_nodes = existing_heating_distribution.loc[subnodes.cluster.unique()] - mother_nodes["cities"] = subnodes.groupby("cluster")["Stadt"].apply(list) + mother_nodes = ( + existing_heating_distribution.loc[subnodes.cluster.unique()] + .unstack(-1) + .to_frame() + ) + cities_within_cluster = subnodes.groupby("cluster")["Stadt"].apply(list) + mother_nodes["cities"] = mother_nodes.apply( + lambda i: cities_within_cluster[i.name[2]], axis=1 + ) # Explode the list of cities - mother_nodes = mother_nodes.explode(("cities", "")) - mother_nodes.index = mother_nodes.index + " " + mother_nodes[("cities", "")] - mother_nodes.drop(columns=("cities", ""), inplace=True) + mother_nodes = mother_nodes.explode("cities") + + # Reset index to temporarily flatten it + mother_nodes_reset = mother_nodes.reset_index() + + # Append city name to the third level of the index + mother_nodes_reset["name"] = ( + mother_nodes_reset["name"] + " " + mother_nodes_reset["cities"] + ) + + # Set the index back + mother_nodes = mother_nodes_reset.set_index(["heat name", "technology", "name"]) + + # Drop the temporary 'cities' column + mother_nodes.drop("cities", axis=1, inplace=True) + + # Reformat to match the existing heating distribution + mother_nodes = mother_nodes.squeeze().unstack(-1).T + + # Combine the exploded data with the existing heating distribution existing_heating_distribution_extended = pd.concat( [existing_heating_distribution, mother_nodes] ) @@ -316,7 +340,9 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): "index" ) lau = gpd.read_file( - f"{snakemake.input.lau}!LAU_RG_01M_2021_3035.geojson", crs="EPSG:3035" + # "/home/cpschau/Code/dev/pypsa-ariadne/.snakemake/storage/http/gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson/LAU_RG_01M_2021_3035.geojson", + f"{snakemake.input.lau}!LAU_RG_01M_2021_3035.geojson", + crs="EPSG:3035", ).to_crs("EPSG:4326") fernwaermeatlas = pd.read_excel( diff --git a/workflow/scripts/build_existing_chp_de.py b/workflow/scripts/build_existing_chp_de.py index c48d3caf..7e3bb3bf 100644 --- a/workflow/scripts/build_existing_chp_de.py +++ b/workflow/scripts/build_existing_chp_de.py @@ -215,14 +215,16 @@ def assign_subnode(CHP_de, subnodes): ) CHP_de.crs = subnodes.crs # Set nuts_3 shape wkt column as geometry - subnodes["geometry"] = gpd.GeoSeries.from_wkt(subnodes["nuts3_shape"]) - subnodes.drop("nuts3_shape", axis=1, inplace=True) + subnodes["geometry"] = gpd.GeoSeries.from_wkt(subnodes["lau_shape"]) + subnodes.drop("lau_shape", axis=1, inplace=True) subnodes.index.rename("city", inplace=True) # Assign subnode to CHP plants based on the nuts3 region CHP_de = CHP_de.sjoin(subnodes, how="left", predicate="within") - CHP_de["subnode"] = CHP_de["cluster"] + " " + CHP_de["city"] - CHP_de.drop(["city", "cluster"], axis=1, inplace=True) + # Insert leading whitespace for citynames where not nan + CHP_de["city"] = CHP_de["city"].apply(lambda x: " " + x if pd.notna(x) else "") + CHP_de["bus"] = CHP_de["bus"] + CHP_de["city"] + CHP_de.drop("city", axis=1, inplace=True) return CHP_de @@ -241,7 +243,7 @@ def assign_subnode(CHP_de, subnodes): snakemake = mock_snakemake( "build_existing_chp_de", simpl="", - clusters=44, + clusters=27, opts="", ll="vopt", sector_opts="none", @@ -268,14 +270,16 @@ def assign_subnode(CHP_de, subnodes): CHP_de = clean_data(combustion, biomass, geodata) CHP_de = calculate_efficiency(CHP_de) - bn = pypsa.Network(snakemake.input.busmap) - substations = bn.buses.query("substation_lv") - CHP_de = map_country_bus(CHP_de, substations) + logger.info("Mapping CHP plants to regions") + regions = gpd.read_file(snakemake.input.regions).set_index("name") + geometry = gpd.points_from_xy(CHP_de["lon"], CHP_de["lat"]) + gdf = gpd.GeoDataFrame(geometry=geometry, crs=4326) + CHP_de["bus"] = gpd.sjoin_nearest(gdf, regions, how="left")["name"] if snakemake.params.add_district_heating_subnodes: subnodes = gpd.read_file( snakemake.input.district_heating_subnodes, - columns=["Stadt", "cluster", "nuts3_shape"], + columns=["Stadt", "lau_shape"], ).set_index("Stadt") CHP_de = assign_subnode(CHP_de, subnodes) From ade4e85cc422a744f44e426b4ba3282ea4fc52e5 Mon Sep 17 00:00:00 2001 From: cpschau Date: Wed, 23 Oct 2024 08:30:21 +0200 Subject: [PATCH 21/30] add heat vent and remove load shedder --- workflow/scripts/add_district_heating_subnodes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index ae3c3588..1225b9f4 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -226,17 +226,17 @@ def add_subnodes(n, subnodes): ) n.madd("Link", heat_pumps.index, efficiency=heat_pumps_t, **heat_pumps) - # Add artificial gas boiler to subnode + # Add heat vent to subnode n.madd( "Generator", - [f"{name} load shedding"], + [f"{name} heat vent"], bus=name, - carrier="gas", + location=f"{row['cluster']} {row['Stadt']}", + carrier="urban central heat vent", p_nom_extendable=True, - p_nom_max=1e6, - capital_cost=10000, - marginal_cost=200, - efficiency=1, + p_min_pu=-1, + p_max_pu=0, + unit="MWh_th", ) return From ce3d85ba851f96a9abdbaa802119780f4eed9762 Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 14 Nov 2024 10:48:37 +0100 Subject: [PATCH 22/30] fix workflow --- workflow/Snakefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 30bb7ba7..250aee5b 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -218,7 +218,7 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ ), fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", cities="data/fernwaermeatlas/cities_geolocations.geojson", - cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}_{planning_horizons}.nc"), existing_heating_distribution=resources( f"existing_heating_distribution_base_s_{{clusters}}_{baseyear_value}.csv" ), @@ -233,11 +233,11 @@ if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ "district_heating_subnodes_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" ), cop_profiles_extended=resources( - "cop_profiles_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + "cop_profiles_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ), existing_heating_distribution_extended=( resources( - "existing_heating_distribution_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + "existing_heating_distribution_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ) if baseyear_value != "{planning_horizons}" else [] @@ -449,17 +449,17 @@ use rule add_existing_baseyear from pypsaeur with: ), cop_profiles=( resources( - "cop_profiles_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.nc" + "cop_profiles_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" ) if config["sector"]["district_heating"].get("add_subnodes", True) - else resources("cop_profiles_base_s_{clusters}.nc") + else resources("cop_profiles_base_s_{clusters}_{planning_horizons}.nc") ), custom_powerplants=resources( "german_chp_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ), existing_heating_distribution=( resources( - "existing_heating_distribution_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-extended.csv" + "existing_heating_distribution_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" ) if config["sector"]["district_heating"].get("add_subnodes", True) else resources( From 1901d24b6e5a77530346a67996f3a6e74d663b82 Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 14 Nov 2024 13:04:31 +0100 Subject: [PATCH 23/30] uncomment get_data --- workflow/Snakefile | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 250aee5b..f1978e71 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -62,18 +62,18 @@ from pathlib import Path data_dir = Path("workflow/submodules/pypsa-eur/data") -# rule get_data: -# output: -# [ -# str(Path("data") / p.relative_to(data_dir)) -# for p in data_dir.rglob("*") -# if p.is_file() -# ], -# shell: -# """ -# mkdir -p data -# cp -nR {data_dir}/. data/ -# """ +rule get_data: + output: + [ + str(Path("data") / p.relative_to(data_dir)) + for p in data_dir.rglob("*") + if p.is_file() + ], + shell: + """ + mkdir -p data + cp -nR {data_dir}/. data/ + """ rule clean: From e76741bde6a84324f0c9f7f03415a6e9b64f828b Mon Sep 17 00:00:00 2001 From: cpschau Date: Mon, 2 Dec 2024 17:16:22 +0100 Subject: [PATCH 24/30] add type hints and docstrings; clean-up unused code snippets; simplify snakefile --- workflow/Snakefile | 93 +++++++-------- .../scripts/add_district_heating_subnodes.py | 112 +++++++++++------- workflow/scripts/build_existing_chp_de.py | 10 +- 3 files changed, 120 insertions(+), 95 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index f1978e71..66d7f188 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -198,54 +198,51 @@ rule build_egon_data: "scripts/build_egon_data.py" -if config["sector"]["district_heating"]["add_subnodes"] and config["sector"][ - "district_heating" -].get("add_subnodes", True): - - baseyear_value = config["scenario"]["planning_horizons"][0] - - rule add_district_heating_subnodes: - params: - district_heating=config_provider("sector", "district_heating"), - baseyear=config_provider("scenario", "planning_horizons", 0), - input: - network=RESULTS - + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), - nuts3=resources("nuts3_shapes.geojson"), - regions_onshore=resources( - "regions_onshore_base_s_{clusters}.geojson" - ), - fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", - cities="data/fernwaermeatlas/cities_geolocations.geojson", - cop_profiles=resources("cop_profiles_base_s_{clusters}_{planning_horizons}.nc"), - existing_heating_distribution=resources( - f"existing_heating_distribution_base_s_{{clusters}}_{baseyear_value}.csv" - ), - lau=storage( - "https://gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson.zip", - keep_local=True, - ), - output: - network=RESULTS - + "prenetworks/base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - district_heating_subnodes=resources( - "district_heating_subnodes_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" - ), - cop_profiles_extended=resources( - "cop_profiles_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" - ), - existing_heating_distribution_extended=( - resources( - "existing_heating_distribution_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" - ) - if baseyear_value != "{planning_horizons}" - else [] - ), - resources: - mem_mb=1000, - script: - "scripts/add_district_heating_subnodes.py" +baseyear_value = config["scenario"]["planning_horizons"][0] + + +rule add_district_heating_subnodes: + params: + district_heating=config_provider("sector", "district_heating"), + baseyear=config_provider("scenario", "planning_horizons", 0), + input: + network=RESULTS + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + heating_technologies_nuts3=resources("heating_technologies_nuts3.geojson"), + nuts3=resources("nuts3_shapes.geojson"), + regions_onshore=resources( + "regions_onshore_base_s_{clusters}.geojson" + ), + fernwaermeatlas="data/fernwaermeatlas/fernwaermeatlas.xlsx", + cities="data/fernwaermeatlas/cities_geolocations.geojson", + cop_profiles=resources("cop_profiles_base_s_{clusters}_{planning_horizons}.nc"), + existing_heating_distribution=resources( + f"existing_heating_distribution_base_s_{{clusters}}_{baseyear_value}.csv" + ), + lau=storage( + "https://gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson.zip", + keep_local=True, + ), + output: + network=RESULTS + + "prenetworks/base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + district_heating_subnodes=resources( + "district_heating_subnodes_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.geojson" + ), + cop_profiles_extended=resources( + "cop_profiles_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc" + ), + existing_heating_distribution_extended=( + resources( + "existing_heating_distribution_base-extended_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.csv" + ) + if baseyear_value != "{planning_horizons}" + else [] + ), + resources: + mem_mb=1000, + script: + "scripts/add_district_heating_subnodes.py" ruleorder: modify_district_heat_share > build_district_heat_share diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 1225b9f4..94af2b37 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -10,16 +10,30 @@ import pandas as pd import pypsa import xarray as xr -from geopy.geocoders import Nominatim +from typing import Union -# Function to encode city names in UTF-8 -def encode_utf8(city_name): - return city_name.encode("utf-8") - - -def prepare_subnodes(subnodes, cities, regions_onshore, lau, heat_techs, head=40): - # TODO: Embed I&O in snakemake rule, add potentials, match CHP capacities +def prepare_subnodes( + subnodes: pd.DataFrame, + cities: gpd.GeoDataFrame, + regions_onshore: gpd.GeoDataFrame, + lau: gpd.GeoDataFrame, + head: Union[int, bool] = 40, +) -> gpd.GeoDataFrame: + """ + Prepare subnodes by filtering district heating systems data for largest systems and assigning the corresponding LAU and onshore region shapes. + + Parameters: + subnodes (pd.DataFrame): DataFrame containing information about district heating systems. + cities (gpd.GeoDataFrame): GeoDataFrame containing city coordinates with columns 'Stadt' and 'geometry'. + regions_onshore (gpd.GeoDataFrame): GeoDataFrame containing onshore region geometries of clustered network. + lau (gpd.GeoDataFrame): GeoDataFrame containing LAU (Local Administrative Units) geometries and IDs. + heat_techs (gpd.GeoDataFrame): GeoDataFrame containing heat technology geometries. + head (Union[int, bool], optional): Number of largest district heating networks to keep. Defaults to 40. If set to True, it will be set to 40. + + Returns: + gpd.GeoDataFrame: GeoDataFrame with processed subnodes, including geometries, clusters, LAU IDs, and NUTS3 shapes. + """ # If head is boolean set it to 40 for default behavior if isinstance(head, bool): head = 40 @@ -45,6 +59,7 @@ def prepare_subnodes(subnodes, cities, regions_onshore, lau, heat_techs, head=40 ) subnodes = subnodes.dropna(subset=["geometry"]) + # Convert the DataFrame to a GeoDataFrame subnodes = gpd.GeoDataFrame(subnodes, crs="EPSG:4326") @@ -59,27 +74,29 @@ def prepare_subnodes(subnodes, cities, regions_onshore, lau, heat_techs, head=40 lambda x: lau.loc[lau.geometry.contains(x.geometry).idxmax(), "geometry"].wkt, axis=1, ) - subnodes["nuts3"] = subnodes.apply( - lambda x: heat_techs.geometry.contains(x.geometry).idxmax(), - axis=1, - ) - subnodes["nuts3_shape"] = subnodes.apply( - lambda x: heat_techs.loc[ - heat_techs.geometry.contains(x.geometry).idxmax(), "geometry" - ].wkt, - axis=1, - ) return subnodes -def add_subnodes(n, subnodes): +def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: """ - Add subnodes to the network and adjust loads and capacities accordingly. + Add largest district heating systems subnodes to the network. They are initialized with + - the total annual heat demand taken from the mother node, that is assigned to urban central heat and low-temperature heat for industry, + - the heat demand profiles taken from the mother node, + - and the district heating investment options (stores, links) from the mother node, + - and heat vents as generator components + The district heating loads in the mother nodes are recuded accordingly. + + Parameters: + n (pypsa.Network): The PyPSA network object to which subnodes will be added. + subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. + + Returns: + None """ # Add subnodes to network - for idx, row in subnodes.iterrows(): + for _, row in subnodes.iterrows(): name = f'{row["cluster"]} {row["Stadt"]} urban central heat' # Add buses @@ -94,8 +111,7 @@ def add_subnodes(n, subnodes): unit="MWh_th", ) - # Add heat loads - + # Add heat loads for urban central heat and low-temperature heat for industry uch_load_cluster = ( n.snapshot_weightings.generators @ n.loads_t.p_set[f"{row['cluster']} urban central heat"] @@ -107,19 +123,19 @@ def add_subnodes(n, subnodes): dh_load_cluster = uch_load_cluster + lti_load_cluster lti_share = lti_load_cluster / dh_load_cluster - scalar = min( + demand_ratio = min( 1, (row["yearly_heat_demand_MWh"] / dh_load_cluster), ) lost_load = row["yearly_heat_demand_MWh"] - dh_load_cluster - if scalar == 1: + if demand_ratio == 1: logger.info( f"District heating load of {row['Stadt']} exceeds load of its assigned cluster {row['cluster']}. {lost_load} MWh/a are disregarded." ) uch_load = ( - scalar + demand_ratio * (1 - lti_share) * n.loads_t.p_set.filter( regex=f"{row['cluster']} urban central heat" @@ -140,7 +156,7 @@ def add_subnodes(n, subnodes): ) lti_load = ( - scalar + demand_ratio * lti_share * n.loads.filter( regex=f"{row['cluster']} low-temperature heat for industry", axis=0 @@ -161,15 +177,14 @@ def add_subnodes(n, subnodes): ) # Adjust loads of cluster buses - n.loads_t.p_set.loc[:, f'{row["cluster"]} urban central heat'] *= 1 - scalar * ( - 1 - lti_share - ) + n.loads_t.p_set.loc[ + :, f'{row["cluster"]} urban central heat' + ] *= 1 - demand_ratio * (1 - lti_share) n.loads.loc[f'{row["cluster"]} low-temperature heat for industry', "p_set"] *= ( - 1 - scalar * lti_share + 1 - demand_ratio * lti_share ) # Replicate district heating stores and links of mother node for subnodes - n.madd( "Bus", [f"{row['cluster']} {row['Stadt']} urban central water tanks"], @@ -242,10 +257,17 @@ def add_subnodes(n, subnodes): return -def extend_cops(cops, subnodes): +def extend_cops(cops: xr.DataArray, subnodes: gpd.GeoDataFrame) -> xr.DataArray: """ - Extend COPs by subnodes mirroring the timeseries of the corresponding + Extend COPs (Coefficient of Performance) by subnodes mirroring the timeseries of the corresponding mother node. + + Parameters: + cops (xr.DataArray): DataArray containing COP timeseries data. + subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. + + Returns: + xr.DataArray: Extended DataArray with COP timeseries for subnodes. """ cops_extended = cops.copy() @@ -269,10 +291,19 @@ def extend_cops(cops, subnodes): return cops_extended -def extend_heating_distribution(existing_heating_distribution, subnodes): +def extend_heating_distribution( + existing_heating_distribution: pd.DataFrame, subnodes: gpd.GeoDataFrame +) -> pd.DataFrame: """ Extend heating distribution by subnodes mirroring the distribution of the corresponding mother node. + + Parameters: + existing_heating_distribution (pd.DataFrame): DataFrame containing the existing heating distribution. + subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. + + Returns: + pd.DataFrame: Extended DataFrame with heating distribution for subnodes. """ # Merge the existing heating distribution with subnodes on the cluster name mother_nodes = ( @@ -336,11 +367,8 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): logger.info("Adding SysGF-specific functionality") n = pypsa.Network(snakemake.input.network) - heat_techs = gpd.read_file(snakemake.input.heating_technologies_nuts3).set_index( - "index" - ) + lau = gpd.read_file( - # "/home/cpschau/Code/dev/pypsa-ariadne/.snakemake/storage/http/gisco-services.ec.europa.eu/distribution/v2/lau/download/ref-lau-2021-01m.geojson/LAU_RG_01M_2021_3035.geojson", f"{snakemake.input.lau}!LAU_RG_01M_2021_3035.geojson", crs="EPSG:3035", ).to_crs("EPSG:4326") @@ -351,18 +379,12 @@ def extend_heating_distribution(existing_heating_distribution, subnodes): ) cities = gpd.read_file(snakemake.input.cities) regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") - # Assign onshore region to heat techs based on geometry - heat_techs["cluster"] = heat_techs.apply( - lambda x: regions_onshore.geometry.contains(x.geometry).idxmax(), - axis=1, - ) subnodes = prepare_subnodes( fernwaermeatlas, cities, regions_onshore, lau, - heat_techs, head=snakemake.params.district_heating["add_subnodes"], ) subnodes.to_file(snakemake.output.district_heating_subnodes, driver="GeoJSON") diff --git a/workflow/scripts/build_existing_chp_de.py b/workflow/scripts/build_existing_chp_de.py index 7e3bb3bf..6e8b3e51 100644 --- a/workflow/scripts/build_existing_chp_de.py +++ b/workflow/scripts/build_existing_chp_de.py @@ -204,9 +204,16 @@ def BP(cap, year): return CHP_de -def assign_subnode(CHP_de, subnodes): +def assign_subnode(CHP_de: pd.DataFrame, subnodes: gpd.GeoDataFrame) -> pd.DataFrame: """ Assign subnodes to the CHP plants based on their location. + + Parameters: + CHP_de (pd.DataFrame): DataFrame containing CHP plant data with latitude and longitude. + subnodes (gpd.GeoDataFrame): GeoDataFrame containing subnode data with geometries. + + Returns: + pd.DataFrame: DataFrame with assigned subnodes. """ # Make a geodataframe from CHP_de using the lat and lon columns @@ -250,7 +257,6 @@ def assign_subnode(CHP_de, subnodes): planning_horizons="2020", run="KN2045_Bal_v4", ) - # snakemake = mock_snakemake("build_existing_chp_de") logging.basicConfig(level=snakemake.config["logging"]["level"]) From f5d4363eb153f2f02316bfc8fc96a762daf18684 Mon Sep 17 00:00:00 2001 From: cpschau Date: Mon, 2 Dec 2024 17:27:46 +0100 Subject: [PATCH 25/30] change variable name row to subnode --- .../scripts/add_district_heating_subnodes.py | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 94af2b37..2f395648 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -96,17 +96,17 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: """ # Add subnodes to network - for _, row in subnodes.iterrows(): - name = f'{row["cluster"]} {row["Stadt"]} urban central heat' + for _, subnode in subnodes.iterrows(): + name = f'{subnode["cluster"]} {subnode["Stadt"]} urban central heat' # Add buses n.madd( "Bus", [name], - y=row.geometry.y, - x=row.geometry.x, + y=subnode.geometry.y, + x=subnode.geometry.x, country="DE", - location=f"{row['cluster']} {row['Stadt']}", + location=f"{subnode['cluster']} {subnode['Stadt']}", carrier="urban central heat", unit="MWh_th", ) @@ -114,10 +114,12 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: # Add heat loads for urban central heat and low-temperature heat for industry uch_load_cluster = ( n.snapshot_weightings.generators - @ n.loads_t.p_set[f"{row['cluster']} urban central heat"] + @ n.loads_t.p_set[f"{subnode['cluster']} urban central heat"] ) lti_load_cluster = ( - n.loads.loc[f"{row['cluster']} low-temperature heat for industry", "p_set"] + n.loads.loc[ + f"{subnode['cluster']} low-temperature heat for industry", "p_set" + ] * 8760 ) dh_load_cluster = uch_load_cluster + lti_load_cluster @@ -125,23 +127,23 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: demand_ratio = min( 1, - (row["yearly_heat_demand_MWh"] / dh_load_cluster), + (subnode["yearly_heat_demand_MWh"] / dh_load_cluster), ) - lost_load = row["yearly_heat_demand_MWh"] - dh_load_cluster + lost_load = subnode["yearly_heat_demand_MWh"] - dh_load_cluster if demand_ratio == 1: logger.info( - f"District heating load of {row['Stadt']} exceeds load of its assigned cluster {row['cluster']}. {lost_load} MWh/a are disregarded." + f"District heating load of {subnode['Stadt']} exceeds load of its assigned cluster {subnode['cluster']}. {lost_load} MWh/a are disregarded." ) uch_load = ( demand_ratio * (1 - lti_share) * n.loads_t.p_set.filter( - regex=f"{row['cluster']} urban central heat" + regex=f"{subnode['cluster']} urban central heat" ).rename( { - f"{row['cluster']} urban central heat": f"{row['cluster']} {row['Stadt']} urban central heat" + f"{subnode['cluster']} urban central heat": f"{subnode['cluster']} {subnode['Stadt']} urban central heat" }, axis=1, ) @@ -152,53 +154,55 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: bus=name, p_set=uch_load, carrier="urban central heat", - location=f"{row['cluster']} {row['Stadt']}", + location=f"{subnode['cluster']} {subnode['Stadt']}", ) lti_load = ( demand_ratio * lti_share * n.loads.filter( - regex=f"{row['cluster']} low-temperature heat for industry", axis=0 + regex=f"{subnode['cluster']} low-temperature heat for industry", axis=0 ).p_set.rename( { - f"{row['cluster']} low-temperature heat for industry": f"{row['cluster']} {row['Stadt']} low-temperature heat for industry" + f"{subnode['cluster']} low-temperature heat for industry": f"{subnode['cluster']} {subnode['Stadt']} low-temperature heat for industry" }, axis=0, ) ) n.madd( "Load", - [f"{row['cluster']} {row['Stadt']} low-temperature heat for industry"], + [ + f"{subnode['cluster']} {subnode['Stadt']} low-temperature heat for industry" + ], bus=name, p_set=lti_load, carrier="low-temperature heat for industry", - location=f"{row['cluster']} {row['Stadt']}", + location=f"{subnode['cluster']} {subnode['Stadt']}", ) # Adjust loads of cluster buses n.loads_t.p_set.loc[ - :, f'{row["cluster"]} urban central heat' + :, f'{subnode["cluster"]} urban central heat' ] *= 1 - demand_ratio * (1 - lti_share) - n.loads.loc[f'{row["cluster"]} low-temperature heat for industry', "p_set"] *= ( - 1 - demand_ratio * lti_share - ) + n.loads.loc[ + f'{subnode["cluster"]} low-temperature heat for industry', "p_set" + ] *= (1 - demand_ratio * lti_share) # Replicate district heating stores and links of mother node for subnodes n.madd( "Bus", - [f"{row['cluster']} {row['Stadt']} urban central water tanks"], - location=f"{row['cluster']} {row['Stadt']}", + [f"{subnode['cluster']} {subnode['Stadt']} urban central water tanks"], + location=f"{subnode['cluster']} {subnode['Stadt']}", carrier="urban central water tanks", unit="MWh_th", ) stores = ( - n.stores.filter(like=f"{row['cluster']} urban central", axis=0) + n.stores.filter(like=f"{subnode['cluster']} urban central", axis=0) .reset_index() .replace( { - f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + f"{subnode['cluster']} urban central": f"{subnode['cluster']} {subnode['Stadt']} urban central" }, regex=True, ) @@ -208,11 +212,11 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: links = ( n.links.loc[~n.links.carrier.str.contains("heat pump")] - .filter(like=f"{row['cluster']} urban central", axis=0) + .filter(like=f"{subnode['cluster']} urban central", axis=0) .reset_index() .replace( { - f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + f"{subnode['cluster']} urban central": f"{subnode['cluster']} {subnode['Stadt']} urban central" }, regex=True, ) @@ -222,22 +226,24 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: # Add heat pumps to subnode heat_pumps = ( - n.links.filter(regex=f"{row['cluster']} urban central.*heat pump", axis=0) + n.links.filter( + regex=f"{subnode['cluster']} urban central.*heat pump", axis=0 + ) .reset_index() .replace( { - f"{row['cluster']} urban central": f"{row['cluster']} {row['Stadt']} urban central" + f"{subnode['cluster']} urban central": f"{subnode['cluster']} {subnode['Stadt']} urban central" }, regex=True, ) .set_index("Link") ).drop("efficiency", axis=1) heat_pumps_t = n.links_t.efficiency.filter( - regex=f"{row['cluster']} urban central.*heat pump" + regex=f"{subnode['cluster']} urban central.*heat pump" ) heat_pumps_t.columns = heat_pumps_t.columns.str.replace( - f"{row['cluster']} urban central", - f"{row['cluster']} {row['Stadt']} urban central", + f"{subnode['cluster']} urban central", + f"{subnode['cluster']} {subnode['Stadt']} urban central", ) n.madd("Link", heat_pumps.index, efficiency=heat_pumps_t, **heat_pumps) @@ -246,7 +252,7 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: "Generator", [f"{name} heat vent"], bus=name, - location=f"{row['cluster']} {row['Stadt']}", + location=f"{subnode['cluster']} {subnode['Stadt']}", carrier="urban central heat vent", p_nom_extendable=True, p_min_pu=-1, @@ -272,9 +278,9 @@ def extend_cops(cops: xr.DataArray, subnodes: gpd.GeoDataFrame) -> xr.DataArray: cops_extended = cops.copy() # Iterate over the DataFrame rows - for _, row in subnodes.iterrows(): - cluster_name = row["cluster"] - city_name = row["Stadt"] + for _, subnode in subnodes.iterrows(): + cluster_name = subnode["cluster"] + city_name = subnode["Stadt"] # Select the xarray entry where name matches the cluster selected_entry = cops.sel(name=cluster_name) From 892ead0509718963d1117566b3e310624bef0532 Mon Sep 17 00:00:00 2001 From: cpschau Date: Mon, 2 Dec 2024 17:33:04 +0100 Subject: [PATCH 26/30] add fernwaermeatlas data --- .../cities_geolocations.geojson | 167 ++++++++++++++++++ data/fernwaermeatlas/fernwaermeatlas.xlsx | Bin 0 -> 21880 bytes 2 files changed, 167 insertions(+) create mode 100755 data/fernwaermeatlas/cities_geolocations.geojson create mode 100755 data/fernwaermeatlas/fernwaermeatlas.xlsx diff --git a/data/fernwaermeatlas/cities_geolocations.geojson b/data/fernwaermeatlas/cities_geolocations.geojson new file mode 100755 index 00000000..b67c63b0 --- /dev/null +++ b/data/fernwaermeatlas/cities_geolocations.geojson @@ -0,0 +1,167 @@ +{ +"type": "FeatureCollection", +"name": "cities_geolocation", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, +"features": [ +{ "type": "Feature", "properties": { "Stadt": "Berlin" }, "geometry": { "type": "Point", "coordinates": [ 13.3989421, 52.5108638 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hamburg" }, "geometry": { "type": "Point", "coordinates": [ 10.000654, 53.550341 ] } }, +{ "type": "Feature", "properties": { "Stadt": "München" }, "geometry": { "type": "Point", "coordinates": [ 11.5753822, 48.1371079 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Köln" }, "geometry": { "type": "Point", "coordinates": [ 6.959974, 50.938361 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Mülheim an der Ruhr" }, "geometry": { "type": "Point", "coordinates": [ 6.8829192, 51.4272925 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Leverkusen" }, "geometry": { "type": "Point", "coordinates": [ 6.9881194, 51.0324743 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bonn" }, "geometry": { "type": "Point", "coordinates": [ 7.10066, 50.735851 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bergisch Gladbach" }, "geometry": { "type": "Point", "coordinates": [ 7.1277379, 50.9929303 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Frankfurt am Main" }, "geometry": { "type": "Point", "coordinates": [ 8.6820917, 50.1106444 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Offenbach am Main" }, "geometry": { "type": "Point", "coordinates": [ 8.7610698, 50.1055002 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Stuttgart" }, "geometry": { "type": "Point", "coordinates": [ 9.1800132, 48.7784485 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Düsseldorf" }, "geometry": { "type": "Point", "coordinates": [ 6.7763137, 51.2254018 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Neuss" }, "geometry": { "type": "Point", "coordinates": [ 6.6916476, 51.1981778 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dortmund" }, "geometry": { "type": "Point", "coordinates": [ 7.4652789, 51.5142273 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Essen" }, "geometry": { "type": "Point", "coordinates": [ 7.0158171, 51.4582235 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Duisburg" }, "geometry": { "type": "Point", "coordinates": [ 6.759562, 51.434999 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bochum" }, "geometry": { "type": "Point", "coordinates": [ 7.2196635, 51.4818111 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Krefeld" }, "geometry": { "type": "Point", "coordinates": [ 6.5623343, 51.3331205 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Oberhausen" }, "geometry": { "type": "Point", "coordinates": [ 6.8514435, 51.4696137 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Herne" }, "geometry": { "type": "Point", "coordinates": [ 7.219985, 51.5380394 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Gelsenkirchen" }, "geometry": { "type": "Point", "coordinates": [ 7.0960124, 51.5110321 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bottrop" }, "geometry": { "type": "Point", "coordinates": [ 6.929204, 51.521581 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Recklinghausen" }, "geometry": { "type": "Point", "coordinates": [ 7.1978546, 51.6143815 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Moers" }, "geometry": { "type": "Point", "coordinates": [ 6.62843, 51.451283 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Leipzig" }, "geometry": { "type": "Point", "coordinates": [ 12.3747329, 51.3406321 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bremen" }, "geometry": { "type": "Point", "coordinates": [ 8.8071646, 53.0758196 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dresden" }, "geometry": { "type": "Point", "coordinates": [ 13.7381437, 51.0493286 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hannover" }, "geometry": { "type": "Point", "coordinates": [ 9.7385532, 52.3744779 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Nürnberg" }, "geometry": { "type": "Point", "coordinates": [ 11.077298, 49.453872 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Fürth" }, "geometry": { "type": "Point", "coordinates": [ 10.9893626, 49.4772475 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wuppertal" }, "geometry": { "type": "Point", "coordinates": [ 7.1780374, 51.264018 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hagen" }, "geometry": { "type": "Point", "coordinates": [ 7.473296, 51.3582945 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Solingen" }, "geometry": { "type": "Point", "coordinates": [ 7.0845893, 51.1721629 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Remscheid" }, "geometry": { "type": "Point", "coordinates": [ 7.228287474564793, 51.184517 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bielefeld" }, "geometry": { "type": "Point", "coordinates": [ 8.531007, 52.0191005 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Münster" }, "geometry": { "type": "Point", "coordinates": [ 7.6251879, 51.9625101 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Karlsruhe" }, "geometry": { "type": "Point", "coordinates": [ 8.4034195, 49.0068705 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Mannheim" }, "geometry": { "type": "Point", "coordinates": [ 8.4673098, 49.4892913 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Ludwigshafen am Rhein" }, "geometry": { "type": "Point", "coordinates": [ 8.4381568, 49.4704113 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Augsburg" }, "geometry": { "type": "Point", "coordinates": [ 10.8979522, 48.3690341 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wiesbaden" }, "geometry": { "type": "Point", "coordinates": [ 8.2416556, 50.0820384 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Mainz" }, "geometry": { "type": "Point", "coordinates": [ 8.2762513, 50.0012314 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Mönchengladbach" }, "geometry": { "type": "Point", "coordinates": [ 6.4353792, 51.1947131 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Braunschweig" }, "geometry": { "type": "Point", "coordinates": [ 10.5236066, 52.2646577 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kiel" }, "geometry": { "type": "Point", "coordinates": [ 10.135555, 54.3227085 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Chemnitz" }, "geometry": { "type": "Point", "coordinates": [ 12.918914, 50.8323531 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Aachen" }, "geometry": { "type": "Point", "coordinates": [ 6.083862, 50.776351 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Magdeburg" }, "geometry": { "type": "Point", "coordinates": [ 11.6399609, 52.1315889 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Halle" }, "geometry": { "type": "Point", "coordinates": [ 11.9705452, 51.4825041 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Freiburg im Breisgau" }, "geometry": { "type": "Point", "coordinates": [ 7.8494005, 47.9960901 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Lübeck" }, "geometry": { "type": "Point", "coordinates": [ 10.684738, 53.866444 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Erfurt" }, "geometry": { "type": "Point", "coordinates": [ 11.0287364, 50.9777974 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Rostock" }, "geometry": { "type": "Point", "coordinates": [ 12.1400211, 54.0886707 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kassel" }, "geometry": { "type": "Point", "coordinates": [ 9.4924096, 51.3154546 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Saarbrücken" }, "geometry": { "type": "Point", "coordinates": [ 6.996379, 49.234362 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hamm" }, "geometry": { "type": "Point", "coordinates": [ 7.815197, 51.6804093 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Potsdam" }, "geometry": { "type": "Point", "coordinates": [ 13.0591397, 52.4009309 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Oldenburg in Holstein" }, "geometry": { "type": "Point", "coordinates": [ 10.8809805, 54.2922574 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Osnabrück" }, "geometry": { "type": "Point", "coordinates": [ 8.047635, 52.2719595 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Heidelberg" }, "geometry": { "type": "Point", "coordinates": [ 8.694724, 49.4093582 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Darmstadt" }, "geometry": { "type": "Point", "coordinates": [ 8.6736295, 49.8851869 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Paderborn" }, "geometry": { "type": "Point", "coordinates": [ 8.764869778177559, 51.71895955 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Regensburg" }, "geometry": { "type": "Point", "coordinates": [ 12.0974869, 49.0195333 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Ingolstadt" }, "geometry": { "type": "Point", "coordinates": [ 11.4250395, 48.7630165 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Würzburg" }, "geometry": { "type": "Point", "coordinates": [ 9.9309779, 49.7933723 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Ulm" }, "geometry": { "type": "Point", "coordinates": [ 9.9912458, 48.3984968 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wolfsburg" }, "geometry": { "type": "Point", "coordinates": [ 10.7861682, 52.4205588 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Heilbronn" }, "geometry": { "type": "Point", "coordinates": [ 9.218655, 49.142291 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Pforzheim" }, "geometry": { "type": "Point", "coordinates": [ 8.7029532, 48.8908846 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Göttingen" }, "geometry": { "type": "Point", "coordinates": [ 9.9351811, 51.5328328 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Reutlingen" }, "geometry": { "type": "Point", "coordinates": [ 9.2114144, 48.4919508 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Koblenz" }, "geometry": { "type": "Point", "coordinates": [ 7.5943951, 50.3533278 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bremerhaven" }, "geometry": { "type": "Point", "coordinates": [ 8.5851945, 53.5505392 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Jena" }, "geometry": { "type": "Point", "coordinates": [ 11.5879359, 50.9281717 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Erlangen" }, "geometry": { "type": "Point", "coordinates": [ 11.0056, 49.5928616 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Trier" }, "geometry": { "type": "Point", "coordinates": [ 6.6441878, 49.7596208 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Salzgitter" }, "geometry": { "type": "Point", "coordinates": [ 10.3593147, 52.1503721 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Siegen" }, "geometry": { "type": "Point", "coordinates": [ 8.0256131, 50.8751175 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hildesheim" }, "geometry": { "type": "Point", "coordinates": [ 9.9513046, 52.1521636 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Cottbus" }, "geometry": { "type": "Point", "coordinates": [ 14.3357307, 51.7567447 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Aschaffenburg" }, "geometry": { "type": "Point", "coordinates": [ 9.1493636, 49.9740542 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Baunatal" }, "geometry": { "type": "Point", "coordinates": [ 9.4119007, 51.2550775 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bergkamen" }, "geometry": { "type": "Point", "coordinates": [ 7.6362876, 51.6149389 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Brühl" }, "geometry": { "type": "Point", "coordinates": [ 6.9037057, 50.8291313 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Datteln" }, "geometry": { "type": "Point", "coordinates": [ 7.3385906, 51.651468 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dillingen-Saar" }, "geometry": { "type": "Point", "coordinates": [ 6.7243197, 49.3552721 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dinslaken" }, "geometry": { "type": "Point", "coordinates": [ 6.7345106, 51.5623618 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Eisenhüttenstadt" }, "geometry": { "type": "Point", "coordinates": [ 14.6294413, 52.1448863 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Ensdorf" }, "geometry": { "type": "Point", "coordinates": [ 11.9353839, 49.3435347 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Esslingen am Neckar" }, "geometry": { "type": "Point", "coordinates": [ 9.3071685, 48.7427584 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Flensburg" }, "geometry": { "type": "Point", "coordinates": [ 9.4333264, 54.7833021 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Grevenbroich" }, "geometry": { "type": "Point", "coordinates": [ 6.584893682540318, 51.0862467 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Gladbeck" }, "geometry": { "type": "Point", "coordinates": [ 6.9877343, 51.5718665 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Glücksburg (Ostsee)" }, "geometry": { "type": "Point", "coordinates": [ 9.562033402693672, 54.84544485 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hanau" }, "geometry": { "type": "Point", "coordinates": [ 8.9169797, 50.132881 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Harrislee" }, "geometry": { "type": "Point", "coordinates": [ 9.3915374, 54.8047109 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Herten" }, "geometry": { "type": "Point", "coordinates": [ 7.1368071, 51.5942009 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hohenmölsen" }, "geometry": { "type": "Point", "coordinates": [ 12.0981844, 51.1573976 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hünxe" }, "geometry": { "type": "Point", "coordinates": [ 6.7660319, 51.6414581 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hürth" }, "geometry": { "type": "Point", "coordinates": [ 6.876568, 50.8807379 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Ketsch" }, "geometry": { "type": "Point", "coordinates": [ 8.5237178, 49.367538 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Langballig" }, "geometry": { "type": "Point", "coordinates": [ 9.663334714510913, 54.7919719 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Lünen" }, "geometry": { "type": "Point", "coordinates": [ 7.5228088, 51.6142482 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Marl" }, "geometry": { "type": "Point", "coordinates": [ 7.0829054, 51.6485843 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Merseburg" }, "geometry": { "type": "Point", "coordinates": [ 11.996148, 51.3564413 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Neukirchen-Vluyn" }, "geometry": { "type": "Point", "coordinates": [ 6.5467641, 51.4413742 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Neu-Ulm" }, "geometry": { "type": "Point", "coordinates": [ 9.9987169, 48.3943949 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Püttlingen" }, "geometry": { "type": "Point", "coordinates": [ 6.8827786, 49.287307 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Quierschied" }, "geometry": { "type": "Point", "coordinates": [ 7.052847, 49.3260163 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Saarlouis" }, "geometry": { "type": "Point", "coordinates": [ 6.749846, 49.3164661 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Schongau" }, "geometry": { "type": "Point", "coordinates": [ 10.8967857, 47.8134583 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Schwedt-Oder" }, "geometry": { "type": "Point", "coordinates": [ 14.2840858, 53.0586366 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Schwetzingen" }, "geometry": { "type": "Point", "coordinates": [ 8.5735135, 49.3832919 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Sindelfingen" }, "geometry": { "type": "Point", "coordinates": [ 9.0035455, 48.7084162 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Speyer" }, "geometry": { "type": "Point", "coordinates": [ 8.433615, 49.3165553 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Spremberg" }, "geometry": { "type": "Point", "coordinates": [ 14.3804302, 51.5714513 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Süderbrarup" }, "geometry": { "type": "Point", "coordinates": [ 9.775192, 54.6354193 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Tarp" }, "geometry": { "type": "Point", "coordinates": [ 9.4026852, 54.6641816 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Voerde (Niederrhein)" }, "geometry": { "type": "Point", "coordinates": [ 6.6811994, 51.5975224 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Völklingen" }, "geometry": { "type": "Point", "coordinates": [ 6.859519, 49.2522866 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wadgassen" }, "geometry": { "type": "Point", "coordinates": [ 6.7922183, 49.2634657 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wallerfangen" }, "geometry": { "type": "Point", "coordinates": [ 6.7183652, 49.3277048 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Wees" }, "geometry": { "type": "Point", "coordinates": [ 9.5186181, 54.8060252 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Zolling" }, "geometry": { "type": "Point", "coordinates": [ 11.7727339, 48.4514051 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Stendal" }, "geometry": { "type": "Point", "coordinates": [ 11.8594279, 52.6050782 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Schwerin" }, "geometry": { "type": "Point", "coordinates": [ 11.4148038, 53.6288297 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Schweinfurt" }, "geometry": { "type": "Point", "coordinates": [ 10.233302, 50.0499945 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Neumünster" }, "geometry": { "type": "Point", "coordinates": [ 9.9815377, 54.0757442 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Weißwasser-O.L." }, "geometry": { "type": "Point", "coordinates": [ 14.6373221, 51.5028807 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Lemgo" }, "geometry": { "type": "Point", "coordinates": [ 8.9012894, 52.0280674 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bergheim" }, "geometry": { "type": "Point", "coordinates": [ 6.6410004, 50.9540457 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Brandenburg an der Havel" }, "geometry": { "type": "Point", "coordinates": [ 12.5497933, 52.4108261 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Castrop-Rauxel" }, "geometry": { "type": "Point", "coordinates": [ 7.3106175, 51.5646195 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dessau-Roßlau" }, "geometry": { "type": "Point", "coordinates": [ 12.2312238, 51.8465924 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Frankfurt (Oder)" }, "geometry": { "type": "Point", "coordinates": [ 14.549452, 52.3412273 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Gera" }, "geometry": { "type": "Point", "coordinates": [ 12.0832666, 50.8765537 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Greifswald" }, "geometry": { "type": "Point", "coordinates": [ 13.3815238, 54.095791 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Hameln" }, "geometry": { "type": "Point", "coordinates": [ 9.3561569, 52.1039941 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Iserlohn" }, "geometry": { "type": "Point", "coordinates": [ 7.6999713, 51.3746778 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kaiserslautern" }, "geometry": { "type": "Point", "coordinates": [ 7.7689951, 49.4432174 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kamp-Lintfort" }, "geometry": { "type": "Point", "coordinates": [ 6.547923, 51.5017981 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Altenholz" }, "geometry": { "type": "Point", "coordinates": [ 10.1187695, 54.3956762 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Annaberg-Buchholz" }, "geometry": { "type": "Point", "coordinates": [ 13.0106108, 50.5788781 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bad Homburg v.d. Höhe" }, "geometry": { "type": "Point", "coordinates": [ 8.6169093, 50.2267699 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bad Mergentheim" }, "geometry": { "type": "Point", "coordinates": [ 9.7730692, 49.490532 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Bernburg (Saale)" }, "geometry": { "type": "Point", "coordinates": [ 11.7391606, 51.7930788 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Coswig" }, "geometry": { "type": "Point", "coordinates": [ 12.458638, 51.8803541 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Dreieich" }, "geometry": { "type": "Point", "coordinates": [ 8.7123912, 50.011974 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Flensburg" }, "geometry": { "type": "Point", "coordinates": [ 9.4333264, 54.7833021 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Gelsenkirchen" }, "geometry": { "type": "Point", "coordinates": [ 7.0960124, 51.5110321 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Görlitz" }, "geometry": { "type": "Point", "coordinates": [ 14.991018, 51.1563185 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kamen" }, "geometry": { "type": "Point", "coordinates": [ 7.6616804, 51.5918019 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Kiel" }, "geometry": { "type": "Point", "coordinates": [ 10.135555, 54.3227085 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Neustrelitz" }, "geometry": { "type": "Point", "coordinates": [ 13.0630004, 53.3617163 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Pfaffenhofen a.d. Ilm" }, "geometry": { "type": "Point", "coordinates": [ 11.5084954, 48.5296743 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Pullach i. Isartal" }, "geometry": { "type": "Point", "coordinates": [ 11.5217455, 48.0556122 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Rottweil" }, "geometry": { "type": "Point", "coordinates": [ 8.6251283, 48.165531 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Soltau" }, "geometry": { "type": "Point", "coordinates": [ 9.8433909, 52.9859666 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Traunreut" }, "geometry": { "type": "Point", "coordinates": [ 12.5952942, 47.9627599 ] } }, +{ "type": "Feature", "properties": { "Stadt": "Zittau" }, "geometry": { "type": "Point", "coordinates": [ 14.8064807, 50.8960964 ] } } +] +} diff --git a/data/fernwaermeatlas/fernwaermeatlas.xlsx b/data/fernwaermeatlas/fernwaermeatlas.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..4edd34dfafaa453129eb3ac9d0312076563b0cd9 GIT binary patch literal 21880 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 literal 0 HcmV?d00001 From 11b3ec9ac5deea1dcc770083a7b776a746a40060 Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 12 Dec 2024 17:35:48 +0100 Subject: [PATCH 27/30] change docstring to numpy --- .../scripts/add_district_heating_subnodes.py | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 2f395648..df88baa2 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- + + import logging logger = logging.getLogger(__name__) @@ -23,16 +25,23 @@ def prepare_subnodes( """ Prepare subnodes by filtering district heating systems data for largest systems and assigning the corresponding LAU and onshore region shapes. - Parameters: - subnodes (pd.DataFrame): DataFrame containing information about district heating systems. - cities (gpd.GeoDataFrame): GeoDataFrame containing city coordinates with columns 'Stadt' and 'geometry'. - regions_onshore (gpd.GeoDataFrame): GeoDataFrame containing onshore region geometries of clustered network. - lau (gpd.GeoDataFrame): GeoDataFrame containing LAU (Local Administrative Units) geometries and IDs. - heat_techs (gpd.GeoDataFrame): GeoDataFrame containing heat technology geometries. - head (Union[int, bool], optional): Number of largest district heating networks to keep. Defaults to 40. If set to True, it will be set to 40. - - Returns: - gpd.GeoDataFrame: GeoDataFrame with processed subnodes, including geometries, clusters, LAU IDs, and NUTS3 shapes. + Parameters + ---------- + subnodes : pd.DataFrame + DataFrame containing information about district heating systems. + cities : gpd.GeoDataFrame + GeoDataFrame containing city coordinates with columns 'Stadt' and 'geometry'. + regions_onshore : gpd.GeoDataFrame + GeoDataFrame containing onshore region geometries of clustered network. + lau : gpd.GeoDataFrame + GeoDataFrame containing LAU (Local Administrative Units) geometries and IDs. + head : Union[int, bool], optional + Number of largest district heating networks to keep. Defaults to 40. If set to True, it will be set to 40. + + Returns + ------- + gpd.GeoDataFrame + GeoDataFrame with processed subnodes, including geometries, clusters, LAU IDs, and NUTS3 shapes. """ # If head is boolean set it to 40 for default behavior if isinstance(head, bool): @@ -80,18 +89,24 @@ def prepare_subnodes( def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: """ - Add largest district heating systems subnodes to the network. They are initialized with + Add largest district heating systems subnodes to the network. + + They are initialized with: - the total annual heat demand taken from the mother node, that is assigned to urban central heat and low-temperature heat for industry, - the heat demand profiles taken from the mother node, - - and the district heating investment options (stores, links) from the mother node, - - and heat vents as generator components - The district heating loads in the mother nodes are recuded accordingly. - - Parameters: - n (pypsa.Network): The PyPSA network object to which subnodes will be added. - subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. - - Returns: + - the district heating investment options (stores, links) from the mother node, + - and heat vents as generator components. + The district heating loads in the mother nodes are reduced accordingly. + + Parameters + ---------- + n : pypsa.Network + The PyPSA network object to which subnodes will be added. + subnodes : gpd.GeoDataFrame + GeoDataFrame containing information about district heating subnodes. + + Returns + ------- None """ @@ -304,12 +319,17 @@ def extend_heating_distribution( Extend heating distribution by subnodes mirroring the distribution of the corresponding mother node. - Parameters: - existing_heating_distribution (pd.DataFrame): DataFrame containing the existing heating distribution. - subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. - - Returns: - pd.DataFrame: Extended DataFrame with heating distribution for subnodes. + Parameters + ---------- + existing_heating_distribution : pd.DataFrame + DataFrame containing the existing heating distribution. + subnodes : gpd.GeoDataFrame + GeoDataFrame containing information about district heating subnodes. + + Returns + ------- + pd.DataFrame + Extended DataFrame with heating distribution for subnodes. """ # Merge the existing heating distribution with subnodes on the cluster name mother_nodes = ( From c1aaae810a40b2d082205019368f349fc0d78f73 Mon Sep 17 00:00:00 2001 From: cpschau Date: Thu, 12 Dec 2024 17:42:12 +0100 Subject: [PATCH 28/30] add comments --- workflow/scripts/add_district_heating_subnodes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index df88baa2..978c0705 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -126,7 +126,7 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: unit="MWh_th", ) - # Add heat loads for urban central heat and low-temperature heat for industry + # Get heat loads for urban central heat and low-temperature heat for industry uch_load_cluster = ( n.snapshot_weightings.generators @ n.loads_t.p_set[f"{subnode['cluster']} urban central heat"] @@ -137,9 +137,12 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: ] * 8760 ) + + # Calculate share of low-temperature heat for industry in total district heating load of cluster dh_load_cluster = uch_load_cluster + lti_load_cluster lti_share = lti_load_cluster / dh_load_cluster + # Calculate demand ratio between load of subnode according to Fernwärmeatlas and remaining load of assigned cluster demand_ratio = min( 1, (subnode["yearly_heat_demand_MWh"] / dh_load_cluster), @@ -147,10 +150,13 @@ def add_subnodes(n: pypsa.Network, subnodes: gpd.GeoDataFrame) -> None: lost_load = subnode["yearly_heat_demand_MWh"] - dh_load_cluster + # District heating demand exceeding the original cluster load is disregarded if demand_ratio == 1: logger.info( f"District heating load of {subnode['Stadt']} exceeds load of its assigned cluster {subnode['cluster']}. {lost_load} MWh/a are disregarded." ) + + # Add load components to subnode preserving the share of low-temperature heat for industry of the cluster uch_load = ( demand_ratio * (1 - lti_share) From 57aa50c6f22b619b1817d47d4a4836617ae244e8 Mon Sep 17 00:00:00 2001 From: cpschau Date: Fri, 13 Dec 2024 11:12:29 +0100 Subject: [PATCH 29/30] another docstring conversion to numpy format --- workflow/scripts/add_district_heating_subnodes.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/workflow/scripts/add_district_heating_subnodes.py b/workflow/scripts/add_district_heating_subnodes.py index 978c0705..303ad2ee 100644 --- a/workflow/scripts/add_district_heating_subnodes.py +++ b/workflow/scripts/add_district_heating_subnodes.py @@ -289,12 +289,17 @@ def extend_cops(cops: xr.DataArray, subnodes: gpd.GeoDataFrame) -> xr.DataArray: Extend COPs (Coefficient of Performance) by subnodes mirroring the timeseries of the corresponding mother node. - Parameters: - cops (xr.DataArray): DataArray containing COP timeseries data. - subnodes (gpd.GeoDataFrame): GeoDataFrame containing information about district heating subnodes. + Parameters + ---------- + cops : xr.DataArray + DataArray containing COP timeseries data. + subnodes : gpd.GeoDataFrame + GeoDataFrame containing information about district heating subnodes. - Returns: - xr.DataArray: Extended DataArray with COP timeseries for subnodes. + Returns + ------- + xr.DataArray + Extended DataArray with COP timeseries for subnodes. """ cops_extended = cops.copy() From af60c5220558cf5e7c4821c7d063534401256ff6 Mon Sep 17 00:00:00 2001 From: cpschau Date: Fri, 13 Dec 2024 11:14:46 +0100 Subject: [PATCH 30/30] one more numpy docstring --- workflow/scripts/build_existing_chp_de.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/workflow/scripts/build_existing_chp_de.py b/workflow/scripts/build_existing_chp_de.py index 6e8b3e51..c14c7e55 100644 --- a/workflow/scripts/build_existing_chp_de.py +++ b/workflow/scripts/build_existing_chp_de.py @@ -208,12 +208,17 @@ def assign_subnode(CHP_de: pd.DataFrame, subnodes: gpd.GeoDataFrame) -> pd.DataF """ Assign subnodes to the CHP plants based on their location. - Parameters: - CHP_de (pd.DataFrame): DataFrame containing CHP plant data with latitude and longitude. - subnodes (gpd.GeoDataFrame): GeoDataFrame containing subnode data with geometries. - - Returns: - pd.DataFrame: DataFrame with assigned subnodes. + Parameters + ---------- + CHP_de : pd.DataFrame + DataFrame containing CHP plant data with latitude and longitude. + subnodes : gpd.GeoDataFrame + GeoDataFrame containing subnode data with geometries. + + Returns + ------- + pd.DataFrame + DataFrame with assigned subnodes. """ # Make a geodataframe from CHP_de using the lat and lon columns