diff --git a/setup.py b/setup.py index fe2be13b4..b85864409 100755 --- a/setup.py +++ b/setup.py @@ -122,6 +122,7 @@ def read(*names, **kwargs): "wtforms", "xarray", "xlrd", + "fuzzywuzzy", ], extras_require={ "dev": ["black", "flake8", "isort>=5", "pre-commit", "pytest", "tox"] diff --git a/src/egon/data/datasets.yml b/src/egon/data/datasets.yml index 02e901686..99bb8d1de 100644 --- a/src/egon/data/datasets.yml +++ b/src/egon/data/datasets.yml @@ -685,6 +685,19 @@ etrago_hydrogen: H2_AC_map: schema: 'grid' table: 'egon_etrago_ac_h2' + links: + schema: 'grid' + table: 'egon_etrago_link' + H2_grid: + new_constructed_pipes: + url: 'https://fnb-gas.de/wp-content/uploads/2024/07/2024_07_22_Anlage3_FNB_Massnahmenliste_Neubau.xlsx' + path: "Anlage_3_Wasserstoffkernnetz_Neubau.xlsx" + converted_ch4_pipes: + url: 'https://fnb-gas.de/wp-content/uploads/2024/07/2024_07_22_Anlage4_FNB_Massnahmenliste_Umstellung.xlsx' + path: "Anlage_4_Wasserstoffkernnetz_Umstellung.xlsx" + pipes_of_further_h2_grid_operators: + url: 'https://fnb-gas.de/wp-content/uploads/2024/07/2024_07_22_Anlage2_Leitungsmeldungen_weiterer_potenzieller_Wasserstoffnetzbetreiber.xlsx' + path: "Anlage_2_Wasserstoffkernetz_weitere_Leitungen.xlsx" targets: hydrogen_buses: schema: 'grid' @@ -1274,4 +1287,4 @@ home_batteries: scenario_path: sources: - url_status2019: 'https://zenodo.org/records/13865306/files/PoWerD_status2019_v2.backup' \ No newline at end of file + url_status2019: 'https://zenodo.org/records/13865306/files/PoWerD_status2019_v2.backup' diff --git a/src/egon/data/datasets/demandregio/__init__.py b/src/egon/data/datasets/demandregio/__init__.py index 67fa95f09..fe1ac407d 100644 --- a/src/egon/data/datasets/demandregio/__init__.py +++ b/src/egon/data/datasets/demandregio/__init__.py @@ -631,9 +631,25 @@ def insert_cts_ind(scenario, year, engine, target_values): "targets" ] + # Workaround: Since the disaggregator does not work anymore, data from + # previous runs is used for eGon2035 and eGon100RE + if scenario == "eGon2035": + ec_cts_ind2 = pd.read_csv( + "data_bundle_powerd_data/egon_demandregio_cts_ind_egon2035.csv" + ) + ec_cts_ind2.to_sql( + targets["cts_ind_demand"]["table"], + engine, + targets["cts_ind_demand"]["schema"], + if_exists="append", + index=False, + ) + return + if scenario == "eGon100RE": ec_cts_ind2 = pd.read_csv( - "data_bundle_powerd_data/egon_demandregio_cts_ind.csv") + "data_bundle_powerd_data/egon_demandregio_cts_ind.csv" + ) ec_cts_ind2.to_sql( targets["cts_ind_demand"]["table"], engine, diff --git a/src/egon/data/datasets/gas_areas.py b/src/egon/data/datasets/gas_areas.py index 8ea41993e..ef5c98e86 100755 --- a/src/egon/data/datasets/gas_areas.py +++ b/src/egon/data/datasets/gas_areas.py @@ -108,11 +108,23 @@ def create_voronoi(scn_name, carrier): """, geom_col="geometry", ).to_crs(epsg=4326) + + + if isinstance(carrier, str): + if carrier == "H2": + carriers = ["H2", "H2_grid"] + else: + carriers = [carrier] + else: + carriers = carrier + + carrier_strings = "', '".join(carriers) + db.execute_sql( f""" DELETE FROM grid.egon_gas_voronoi - WHERE "carrier" = '{carrier}' and "scn_name" = '{scn_name}'; + WHERE "carrier" IN ('{carrier_strings}') and "scn_name" = '{scn_name}'; """ ) @@ -122,13 +134,14 @@ def create_voronoi(scn_name, carrier): FROM grid.egon_etrago_bus WHERE scn_name = '{scn_name}' AND country = 'DE' - AND carrier = '{carrier}'; + AND carrier IN ('{carrier_strings}'); """, ).to_crs(epsg=4326) - + + if len(buses) == 0: return - + # generate voronois # For some scenarios it is defined that there is only 1 bus (e.g. gas). It # means that there will be just 1 voronoi covering the entire german diff --git a/src/egon/data/datasets/hydrogen_etrago/__init__.py b/src/egon/data/datasets/hydrogen_etrago/__init__.py index d4f3e5d68..1cb694cec 100755 --- a/src/egon/data/datasets/hydrogen_etrago/__init__.py +++ b/src/egon/data/datasets/hydrogen_etrago/__init__.py @@ -28,6 +28,7 @@ insert_H2_overground_storage, insert_H2_saltcavern_storage, ) +from egon.data import config class HydrogenBusEtrago(Dataset): @@ -62,11 +63,12 @@ def __init__(self, dependencies): dependencies=dependencies, tasks=( calculate_and_map_saltcavern_storage_potential, - insert_hydrogen_buses, + insert_h2_buses_for_scn, ), ) + class HydrogenStoreEtrago(Dataset): """ Insert the H2 stores into the database for Germany @@ -185,10 +187,10 @@ def __init__(self, dependencies): class HydrogenGridEtrago(Dataset): """ - Insert the H2 grid in Germany into the database for eGon100RE + Insert the H2 grid in Germany into the database for eGon2035 and eGon100RE Insert the H2 links (pipelines) into Germany in the database for the - scenario eGon100RE by executing the function + scenario eGon2035/eGon100RE by executing the function :py:func:`insert_h2_pipelines `. *Dependencies* @@ -210,10 +212,29 @@ class HydrogenGridEtrago(Dataset): #: version: str = "0.0.2" + def __init__(self, dependencies): super().__init__( name=self.name, version=self.version, dependencies=dependencies, - tasks=(insert_h2_pipelines,), + tasks = insert_h2_pipelines_for_scn, ) + +def insert_h2_pipelines_for_scn(): + scenarios = config.settings()["egon-data"]["--scenarios"] + + if "eGon2035" in scenarios: + insert_h2_pipelines("eGon2035") + + if "eGon100RE" in scenarios: + insert_h2_pipelines("eGon100RE") + +def insert_h2_buses_for_scn(): + scenarios = config.settings()["egon-data"]["--scenarios"] + + if "eGon2035" in scenarios: + insert_hydrogen_buses("eGon2035") + + if "eGon100RE" in scenarios: + insert_hydrogen_buses("eGon100RE") \ No newline at end of file diff --git a/src/egon/data/datasets/hydrogen_etrago/bus.py b/src/egon/data/datasets/hydrogen_etrago/bus.py index da92c6939..e5db16759 100755 --- a/src/egon/data/datasets/hydrogen_etrago/bus.py +++ b/src/egon/data/datasets/hydrogen_etrago/bus.py @@ -15,53 +15,117 @@ """ from geoalchemy2 import Geometry +from pathlib import Path +import pandas as pd +import geopandas as gpd +from shapely.wkb import loads +import numpy as np +from scipy.spatial import cKDTree from egon.data import config, db from egon.data.datasets.etrago_helpers import ( - copy_and_modify_buses, finalize_bus_insertion, initialise_bus_insertion, ) -def insert_hydrogen_buses(): +def insert_hydrogen_buses(scn_name): """ - Insert hydrogen buses into the database (in etrago table) - - Hydrogen buses are inserted into the database using the functions: - * :py:func:`insert_H2_buses_from_CH4_grid` for H2 buses - * :py:func:`insert_H2_buses_from_saltcavern` for the H2_saltcavern - buses + Insert hydrogen buses into the database. + Location of H2_buses are based on grid nodes of Wasserstoffkernnetz published by FNB-GAS + and the location of saltcaverns in germany. Parameters ---------- - No parameter is required. + scn_name: str + Name of scenario """ - s = config.settings()["egon-data"]["--scenarios"] - scn = [] - if "eGon2035" in s: - scn.append("eGon2035") - if "eGon100RE" in s: - scn.append("eGon100RE") - - for scenario in scn: - sources = config.datasets()["etrago_hydrogen"]["sources"] - target = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_buses"] - # initalize dataframe for hydrogen buses - carrier = "H2_saltcavern" - hydrogen_buses = initialise_bus_insertion( - carrier, target, scenario=scenario - ) - insert_H2_buses_from_saltcavern( - hydrogen_buses, carrier, sources, target, scenario - ) + + h2_input= pd.read_csv(Path(".")/"h2_grid_nodes.csv") + h2_input.geom = h2_input.geom.apply(lambda wkb_hex: loads(bytes.fromhex(wkb_hex))) + + sources = config.datasets()["etrago_hydrogen"]["sources"] + target_buses = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_buses"] + h2_buses = initialise_bus_insertion('H2_grid', target_buses, scenario = scn_name) + + db.execute_sql( + f""" + DELETE FROM {target_buses['schema']}.{target_buses['table']} + WHERE scn_name = '{scn_name}' + AND carrier = 'H2' AND country = 'DE' + """ + ) + + h2_buses.x = h2_input.x + h2_buses.y = h2_input.y + h2_buses.geom = h2_input.geom + h2_buses.carrier = 'H2_grid' + h2_buses.scn_name = scn_name + next_bus_id = db.next_etrago_id('bus') + h2_buses.bus_id= range(next_bus_id, next_bus_id + len(h2_input)) + + h2_buses.to_postgis( + target_buses["table"], + schema=target_buses["schema"], + if_exists="append", + con=db.engine(), + dtype={"geom": Geometry()} + ) - carrier = "H2" - hydrogen_buses = initialise_bus_insertion( - carrier, target, scenario=scenario + #insert additional_buses for potential Methanisation to CH4_buses nearby: + h2_buses = h2_buses.to_crs(epsg=32632) + + sql_CH4_buses = f""" + SELECT bus_id, x, y, ST_Transform(geom, 32632) as geom + FROM {target_buses["schema"]}.{target_buses["table"]} + WHERE carrier = 'CH4' + AND scn_name = '{scn_name}' AND country = 'DE' + """ + CH4_buses = gpd.read_postgis(sql_CH4_buses, con=db.engine()) + + additional_H2_buses = [] + H2_coords = np.array([(point.x, point.y) for point in h2_buses.geometry]) + H2_tree = cKDTree(H2_coords) + for idx, ch4_bus in CH4_buses.iterrows(): + ch4_coords = [ch4_bus['geom'].x, ch4_bus['geom'].y] + + # filter nearest h2_bus + dist, nearest_idx = H2_tree.query(ch4_coords, k=1) + # critcical distance assumed with 10km based on former ammount of h2_buses + if dist > 10000: + # Neuen H2-Bus hinzufügen + additional_H2_buses.append({ + 'scn_name': scn_name, + 'bus_id': None, + 'x': ch4_bus['x'], + 'y': ch4_bus['y'], + 'carrier': 'H2', + 'geom': ch4_bus['geom'] + }) + + if additional_H2_buses: + additional_H2_buses = gpd.GeoDataFrame(additional_H2_buses, geometry='geom', crs=CH4_buses.crs) + additional_H2_buses =additional_H2_buses.to_crs(epsg=4326) + + next_bus_id = db.next_etrago_id('bus') + additional_H2_buses['bus_id'] = range(next_bus_id, next_bus_id + len(additional_H2_buses)) + # Insert data to db + additional_H2_buses.to_postgis( + target_buses["table"], + schema= target_buses["schema"], + con=db.engine(), + if_exists="append", + dtype={"geom": Geometry()} + ) + + #insert h2_buses_from_saltcaverns + hydrogen_buses = initialise_bus_insertion( + "H2_saltcavern", target_buses, scenario=scn_name + ) + insert_H2_buses_from_saltcavern( + hydrogen_buses, "H2_saltcavern", sources, target_buses, scn_name ) - insert_H2_buses_from_CH4_grid(hydrogen_buses, carrier, target, scenario) def insert_H2_buses_from_saltcavern(gdf, carrier, sources, target, scn_name): @@ -131,53 +195,3 @@ def insert_H2_buses_from_saltcavern(gdf, carrier, sources, target, scn_name): ) -def insert_H2_buses_from_CH4_grid(gdf, carrier, target, scn_name): - """ - Insert the H2 buses based on CH4 grid into the database. - - At each CH4 location, in other words at each intersection of the CH4 - grid, a H2 bus is created. - - Parameters - ---------- - gdf : geopandas.GeoDataFrame - GeoDataFrame containing the empty bus data. - carrier : str - Name of the carrier. - target : dict - Target schema and table information. - scn_name : str - Name of the scenario. - - """ - # Connect to local database - engine = db.engine() - - # Select the CH4 buses - sql_CH4 = f"""SELECT bus_id, scn_name, geom - FROM grid.egon_etrago_bus - WHERE carrier = 'CH4' AND scn_name = '{scn_name}' - AND country = 'DE';""" - - gdf_H2 = db.select_geodataframe(sql_CH4, epsg=4326) - # CH4 bus ids and respective hydrogen bus ids are written to db for - # later use (CH4 grid to H2 links) - CH4_bus_ids = gdf_H2[["bus_id", "scn_name"]].copy() - - H2_bus_ids = finalize_bus_insertion( - gdf_H2, carrier, target, scenario=scn_name - ) - - gdf_H2_CH4 = H2_bus_ids[["bus_id"]].rename(columns={"bus_id": "bus_H2"}) - gdf_H2_CH4["bus_CH4"] = CH4_bus_ids["bus_id"] - gdf_H2_CH4["scn_name"] = CH4_bus_ids["scn_name"] - - # Insert data to db - gdf_H2_CH4.to_sql( - "egon_etrago_ch4_h2", - engine, - schema="grid", - index=False, - if_exists="replace", - ) - diff --git a/src/egon/data/datasets/hydrogen_etrago/h2_grid.py b/src/egon/data/datasets/hydrogen_etrago/h2_grid.py index eac0133b3..9ead3d23e 100755 --- a/src/egon/data/datasets/hydrogen_etrago/h2_grid.py +++ b/src/egon/data/datasets/hydrogen_etrago/h2_grid.py @@ -12,177 +12,647 @@ """ from geoalchemy2.types import Geometry -from shapely.geometry import MultiLineString +from shapely.geometry import LineString, MultiLineString, Point import geopandas as gpd +import pandas as pd +import os +from urllib.request import urlretrieve +from pathlib import Path +from fuzzywuzzy import process +from shapely import wkb +import math +import re +from itertools import count +import numpy as np +from scipy.spatial import cKDTree -from egon.data import db -from egon.data.datasets.etrago_setup import link_geom_from_buses +from egon.data import config, db from egon.data.datasets.scenario_parameters import get_sector_parameters +from egon.data.datasets.scenario_parameters.parameters import annualize_capital_costs -def insert_h2_pipelines(): +def insert_h2_pipelines(scn_name): + "Insert H2_grid based on Input Data from FNB-Gas" + + download_h2_grid_data() + H2_grid_Neubau, H2_grid_Umstellung, H2_grid_Erweiterung = read_h2_excel_sheets() + h2_bus_location = pd.read_csv(Path(".")/"h2_grid_nodes.csv") + con=db.engine() + + sources = config.datasets()["etrago_hydrogen"]["sources"] + target = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_links"] + + h2_buses_df = pd.read_sql( + f""" + SELECT bus_id, x, y FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE carrier in ('H2_grid') + AND scn_name = '{scn_name}' """ - Insert hydrogen grid (H2 links) into the database for eGon100RE. - - Insert the H2 grid by executing the following steps: - * Copy the CH4 links in Germany from eGon100RE - * Overwrite the followings columns: - * bus0 and bus1 using the grid.egon_etrago_ch4_h2 table - * carrier, scn_name - * p_nom: the value attributed there corresponds to the share - of p_nom of the specific pipe that could be retrofited into - H2 pipe. This share is the same for every pipeline and is - calculated in the PyPSA-eur-sec run. - * Create new extendable pipelines to link the existing grid to the - H2_saltcavern buses - * Clean database - * Attribute link_id to the links - * Insert the into the database - - This function inserts data into the database and has no return. - - """ - H2_buses = db.select_geodataframe( - f""" - SELECT * FROM grid.egon_etrago_bus WHERE scn_name = 'eGon100RE' AND - carrier IN ('H2', 'H2_saltcavern') and country = 'DE' - """, - epsg=4326, - ) - - pipelines = db.select_geodataframe( + , con) + + # Delete old entries + db.execute_sql( f""" - SELECT * FROM grid.egon_etrago_link - WHERE scn_name = 'eGon100RE' AND carrier = 'CH4' - AND bus0 IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = 'eGon100RE' AND country = 'DE' - ) AND bus1 IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = 'eGon100RE' AND country = 'DE' - ); + DELETE FROM {target["schema"]}.{target["table"]} + WHERE "carrier" = 'H2_grid' + AND scn_name = '{scn_name}' AND bus0 IN ( + SELECT bus_id + FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE country = 'DE' + ) """ ) + + + target = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_links"] + + for df in [H2_grid_Neubau, H2_grid_Umstellung, H2_grid_Erweiterung]: + + if df is H2_grid_Neubau: + df.rename(columns={'Planerische \nInbetriebnahme': 'Planerische Inbetriebnahme'}, inplace=True) + df.loc[df['Endpunkt\n(Ort)'] == 'AQD Anlandung', 'Endpunkt\n(Ort)'] = 'Schillig' + df.loc[df['Endpunkt\n(Ort)'] == 'Hallendorf', 'Endpunkt\n(Ort)'] = 'Salzgitter' + + if df is H2_grid_Erweiterung: + df.rename(columns={'Umstellungsdatum/ Planerische Inbetriebnahme': 'Planerische Inbetriebnahme', + 'Nenndurchmesser (DN)': 'Nenndurchmesser \n(DN)', + 'Investitionskosten\n(Mio. Euro),\nKostenschätzung': 'Investitionskosten*\n(Mio. Euro)'}, + inplace=True) + df = df[df['Berücksichtigung im Kernnetz \n[ja/nein/zurückgezogen]'].str.strip().str.lower() == 'ja'] + df.loc[df['Endpunkt\n(Ort)'] == 'Osdorfer Straße', 'Endpunkt\n(Ort)'] = 'Berlin- Lichterfelde' + + h2_bus_location['Ort'] = h2_bus_location['Ort'].astype(str).str.strip() + df['Anfangspunkt\n(Ort)'] = df['Anfangspunkt\n(Ort)'].astype(str).str.strip() + df['Endpunkt\n(Ort)'] = df['Endpunkt\n(Ort)'].astype(str).str.strip() - CH4_H2_busmap = db.select_dataframe( - f""" - SELECT * FROM grid.egon_etrago_ch4_h2 WHERE scn_name = 'eGon100RE' - """, - index_col="bus_CH4", - ) + df = df[['Anfangspunkt\n(Ort)', 'Endpunkt\n(Ort)', 'Nenndurchmesser \n(DN)', 'Druckstufe (DP)\n[mind. 30 barg]', + 'Investitionskosten*\n(Mio. Euro)', 'Planerische Inbetriebnahme', 'Länge \n(km)']] + + # matching start- and endpoint of each pipeline with georeferenced data + df['Anfangspunkt_matched'] = fuzzy_match(df, h2_bus_location, 'Anfangspunkt\n(Ort)') + df['Endpunkt_matched'] = fuzzy_match(df, h2_bus_location, 'Endpunkt\n(Ort)') + + # manuell adjustments based on Detailmaßnahmenkarte der FNB-Gas [https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/] + df= fix_h2_grid_infrastructure(df) + + df_merged = pd.merge(df, h2_bus_location[['Ort', 'geom', 'x', 'y']], + how='left', left_on='Anfangspunkt_matched', right_on='Ort').rename( + columns={'geom': 'geom_start', 'x': 'x_start', 'y': 'y_start'}) + df_merged = pd.merge(df_merged, h2_bus_location[['Ort', 'geom', 'x', 'y']], + how='left', left_on='Endpunkt_matched', right_on='Ort').rename( + columns={'geom': 'geom_end', 'x': 'x_end', 'y': 'y_end'}) + + H2_grid_df = df_merged.dropna(subset=['geom_start', 'geom_end']) + H2_grid_df = H2_grid_df[H2_grid_df['geom_start'] != H2_grid_df['geom_end']] + H2_grid_df = pd.merge(H2_grid_df, h2_buses_df, how='left', left_on=['x_start', 'y_start'], right_on=['x','y']).rename( + columns={'bus_id': 'bus0'}) + H2_grid_df = pd.merge(H2_grid_df, h2_buses_df, how='left', left_on=['x_end', 'y_end'], right_on=['x','y']).rename( + columns={'bus_id': 'bus1'}) + H2_grid_df[['bus0', 'bus1']] = H2_grid_df[['bus0', 'bus1']].astype('Int64') + + H2_grid_df['geom_start'] = H2_grid_df['geom_start'].apply(lambda x: wkb.loads(bytes.fromhex(x))) + H2_grid_df['geom_end'] = H2_grid_df['geom_end'].apply(lambda x: wkb.loads(bytes.fromhex(x))) + H2_grid_df['topo'] = H2_grid_df.apply( + lambda row: LineString([row['geom_start'], row['geom_end']]), axis=1) + H2_grid_df['geom'] = H2_grid_df.apply( + lambda row: MultiLineString([LineString([row['geom_start'], row['geom_end']])]), axis=1) + H2_grid_gdf = gpd.GeoDataFrame(H2_grid_df, geometry='geom', crs=4326) + + scn_params = get_sector_parameters("gas", scn_name) + next_link_id = db.next_etrago_id('link') + + H2_grid_gdf['link_id'] = range(next_link_id, next_link_id + len(H2_grid_gdf)) + H2_grid_gdf['scn_name'] = scn_name + H2_grid_gdf['carrier'] = 'H2_grid' + H2_grid_gdf['Planerische Inbetriebnahme'] = H2_grid_gdf['Planerische Inbetriebnahme'].astype(str).apply( + lambda x: int(re.findall(r'\d{4}', x)[-1]) if re.findall(r'\d{4}', x) else + int(re.findall(r'\d{2}\.\d{2}\.(\d{4})', x)[-1]) if re.findall(r'\d{2}\.\d{2}\.(\d{4})', x) else None) + H2_grid_gdf['build_year'] = H2_grid_gdf['Planerische Inbetriebnahme'].astype('Int64') + H2_grid_gdf['p_nom'] = H2_grid_gdf.apply(lambda row:calculate_H2_capacity(row['Druckstufe (DP)\n[mind. 30 barg]'], row['Nenndurchmesser \n(DN)']), axis=1 ) + H2_grid_gdf['p_nom_min'] = H2_grid_gdf['p_nom'] + H2_grid_gdf['p_nom_max'] = float("Inf") + H2_grid_gdf['p_nom_extendable'] = False + H2_grid_gdf['lifetime'] = scn_params["lifetime"]["H2_pipeline"] + H2_grid_gdf['capital_cost'] = H2_grid_gdf.apply(lambda row: annualize_capital_costs( + (float(row["Investitionskosten*\n(Mio. Euro)"]) * 10**6 / row['p_nom']) + if pd.notna(row["Investitionskosten*\n(Mio. Euro)"]) + and str(row["Investitionskosten*\n(Mio. Euro)"]).replace(",", "").replace(".", "").isdigit() + and float(row["Investitionskosten*\n(Mio. Euro)"]) != 0 + else scn_params["overnight_cost"]["H2_pipeline"] * row['Länge \n(km)'], + row['lifetime'], 0.05), axis=1) + H2_grid_gdf['p_min_pu'] = -1 + + selected_columns = [ + 'scn_name', 'link_id', 'bus0', 'bus1', 'build_year', 'p_nom', 'p_nom_min', + 'p_nom_extendable', 'capital_cost', 'geom', 'topo', 'carrier', 'p_nom_max', 'p_min_pu', + ] + + H2_grid_final=H2_grid_gdf[selected_columns] + + # Insert data to db + H2_grid_final.to_postgis( + target["table"], + con, + schema= target["schema"], + if_exists="append", + dtype={"geom": Geometry()}, + ) + + #connect saltcaverns to H2_grid + connect_saltcavern_to_h2_grid(scn_name) + + #connect neighbour countries to H2_grid + connect_h2_grid_to_neighbour_countries(scn_name) - scn_params = get_sector_parameters("gas", "eGon100RE") - pipelines["carrier"] = "H2_retrofit" - pipelines["p_nom"] *= ( - scn_params["retrofitted_CH4pipeline-to-H2pipeline_share"] - ) - # map pipeline buses - pipelines["bus0"] = CH4_H2_busmap.loc[pipelines["bus0"], "bus_H2"].values - pipelines["bus1"] = CH4_H2_busmap.loc[pipelines["bus1"], "bus_H2"].values - pipelines["scn_name"] = "eGon100RE" - pipelines["p_min_pu"] = -1.0 - pipelines["capital_cost"] = ( - scn_params["capital_cost"]["H2_pipeline_retrofit"] - * pipelines["length"] - / 1e3 - ) +def replace_pipeline(df, start, end, intermediate): + """ + Method for adjusting pipelines manually by splittiing pipeline with an intermediate point. + + Parameters + ---------- + df : pandas.core.frame.DataFrame + dataframe to be adjusted + start: str + startpoint of pipeline + end: str + endpoint of pipeline + intermediate: str + new intermediate point for splitting given pipeline + + Returns + --------- + df : + adjusted dataframe + + + """ + # Find rows where the start and end points match + mask = ((df['Anfangspunkt_matched'] == start) & (df['Endpunkt_matched'] == end)) | \ + ((df['Anfangspunkt_matched'] == end) & (df['Endpunkt_matched'] == start)) + + # Separate the rows to replace + if mask.any(): + df_replacement = df[~mask].copy() + row_replaced = df[mask].iloc[0] + + # Add new rows for the split pipelines + new_rows = pd.DataFrame({ + 'Anfangspunkt_matched': [start, intermediate], + 'Endpunkt_matched': [intermediate, end], + 'Nenndurchmesser \n(DN)': [row_replaced['Nenndurchmesser \n(DN)'], row_replaced['Nenndurchmesser \n(DN)']], + 'Druckstufe (DP)\n[mind. 30 barg]': [row_replaced['Druckstufe (DP)\n[mind. 30 barg]'], row_replaced['Druckstufe (DP)\n[mind. 30 barg]']], + 'Investitionskosten*\n(Mio. Euro)': [row_replaced['Investitionskosten*\n(Mio. Euro)'], row_replaced['Investitionskosten*\n(Mio. Euro)']], + 'Planerische Inbetriebnahme': [row_replaced['Planerische Inbetriebnahme'],row_replaced['Planerische Inbetriebnahme']], + 'Länge \n(km)': [row_replaced['Länge \n(km)'], row_replaced['Länge \n(km)']] + }) + + df_replacement = pd.concat([df_replacement, new_rows], ignore_index=True) + return df_replacement + else: + return df - # create new pipelines between grid and saltcaverns - new_pipelines = gpd.GeoDataFrame() - new_pipelines["bus0"] = H2_buses.loc[ - H2_buses["carrier"] == "H2_saltcavern", "bus_id" - ].values - new_pipelines["geometry"] = H2_buses.loc[ - H2_buses["carrier"] == "H2_saltcavern", "geom" - ].values - new_pipelines.set_crs(epsg=4326, inplace=True) - - # find bus in H2_grid voronoi - new_pipelines = db.assign_gas_bus_id(new_pipelines, "eGon100RE", "H2") - new_pipelines = new_pipelines.rename(columns={"bus_id": "bus1"}).drop( - columns=["bus"] - ) - # create link geometries - new_pipelines = link_geom_from_buses( - new_pipelines[["bus0", "bus1"]], "eGon100RE" - ) - new_pipelines["geom"] = new_pipelines.apply( - lambda row: MultiLineString([row["topo"]]), axis=1 - ) - new_pipelines = new_pipelines.set_geometry("geom", crs=4326) - new_pipelines["carrier"] = "H2_gridextension" - new_pipelines["scn_name"] = "eGon100RE" - new_pipelines["p_min_pu"] = -1.0 - new_pipelines["p_nom_extendable"] = True - new_pipelines["length"] = new_pipelines.to_crs(epsg=3035).geometry.length - - # Insert capital cost data - new_pipelines["capital_cost"] = ( - scn_params["capital_cost"]["H2_pipeline"] - * new_pipelines["length"] - / 1e3 - ) - # Delete old entries - db.execute_sql( - f""" - DELETE FROM grid.egon_etrago_link WHERE "carrier" IN - ('H2_retrofit', 'H2_gridextension') AND scn_name = 'eGon100RE' - AND bus0 NOT IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = 'eGon100RE' AND country != 'DE' - ) AND bus1 NOT IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = 'eGon100RE' AND country != 'DE' - ); - """ - ) +def fuzzy_match(df1, df2, column_to_match, threshold=80): + ''' + Method for matching input data of H2_grid with georeferenced data (even if the strings are not exact the same) + + Parameters + ---------- + df1 : pandas.core.frame.DataFrame + Input dataframe + df2 : pandas.core.frame.DataFrame + georeferenced dataframe with h2_buses + column_to_match: str + matching column + treshhold: float + matching percentage for succesfull comparison + + Returns + --------- + matched : list + list with all matched location names + + ''' + options = df2['Ort'].unique() + matched = [] + + # Compare every locationname in df1 with locationnames in df2 + for value in df1[column_to_match]: + match, score = process.extractOne(value, options) + if score >= threshold: + matched.append(match) + else: + matched.append(None) + + return matched + + +def calculate_H2_capacity(pressure, diameter): + ''' + Method for calculagting capacity of pipelines based on data input from FNB Gas + + Parameters + ---------- + pressure : float + input for pressure of pipeline + diameter: float + input for diameter of pipeline + column_to_match: str + matching column + treshhold: float + matching percentage for succesfull comparison + + Returns + --------- + energy_flow: float + transmission capacity of pipeline + + ''' + + pressure = str(pressure).replace(",", ".") + diameter =str(diameter) + + def convert_to_float(value): + try: + return float(value) + except ValueError: + return 400 #average value from data-source cause capacities of some lines are not fixed yet + + # in case of given range for pipeline-capacity calculate average value + if '-' in diameter: + diameters = diameter.split('-') + diameter = (convert_to_float(diameters[0]) + convert_to_float(diameters[1])) / 2 + elif '/' in diameter: + diameters = diameter.split('/') + diameter = (convert_to_float(diameters[0]) + convert_to_float(diameters[1])) / 2 + else: + try: + diameter = float(diameter) + except ValueError: + diameter = 400 #average value from data-source + + if '-' in pressure: + pressures = pressure.split('-') + pressure = (float(pressures[0]) + float(pressures[1])) / 2 + elif '/' in pressure: + pressures = pressure.split('/') + pressure = (float(pressures[0]) + float(pressures[1])) / 2 + else: + try: + pressure = float(diameter) + except ValueError: + pressure = 70 #averaqge value from data-source + + + velocity = 40 #source: L.Koops (2023): GAS PIPELINE VERSUS LIQUID HYDROGEN TRANSPORT – PERSPECTIVES FOR TECHNOLOGIES, ENERGY DEMAND ANDv TRANSPORT CAPACITY, AND IMPLICATIONS FOR AVIATION + temperature = 10+273.15 #source: L.Koops (2023): GAS PIPELINE VERSUS LIQUID HYDROGEN TRANSPORT – PERSPECTIVES FOR TECHNOLOGIES, ENERGY DEMAND ANDv TRANSPORT CAPACITY, AND IMPLICATIONS FOR AVIATION + density = pressure*10**5/(4.1243*10**3*temperature) #gasconstant H2 = 4.1243 [kJ/kgK] + mass_flow = density*math.pi*((diameter/10**3)/2)**2*velocity + energy_flow = mass_flow * 119.988 #low_heating_value H2 = 119.988 [MJ/kg] + + return energy_flow + + +def download_h2_grid_data(): + """ + Download Input data for H2_grid from FNB-Gas (https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/) + + The following data for H2 are downloaded into the folder + ./datasets/h2_data: + * Links (file Anlage_3_Wasserstoffkernnetz_Neubau.xlsx, + Anlage_4_Wasserstoffkernnetz_Umstellung.xlsx, + Anlage_2_Wasserstoffkernetz_weitere_Leitungen.xlsx) + + Returns + ------- + None + + """ + path = Path("datasets/h2_data") + os.makedirs(path, exist_ok=True) + + download_config = config.datasets()["etrago_hydrogen"]["sources"]["H2_grid"] + target_file_Um = path / download_config["converted_ch4_pipes"]["path"] + target_file_Neu = path / download_config["new_constructed_pipes"]["path"] + target_file_Erw = path / download_config["pipes_of_further_h2_grid_operators"]["path"] + + for target_file in [target_file_Neu, target_file_Um, target_file_Erw]: + if target_file is target_file_Um: + url = download_config["converted_ch4_pipes"]["url"] + elif target_file is target_file_Neu: + url = download_config["new_constructed_pipes"]['url'] + else: + url = download_config["pipes_of_further_h2_grid_operators"]['url'] + + if not os.path.isfile(target_file): + urlretrieve(url, target_file) + + +def read_h2_excel_sheets(): + """ + Read downloaded excel files with location names for future h2-pipelines + Returns + ------- + df_Neu : + df_Um : + df_Erw : + + + """ + + path = Path(".") / "datasets" / "h2_data" + download_config = config.datasets()["etrago_hydrogen"]["sources"]["H2_grid"] + excel_file_Um = pd.ExcelFile(f'{path}/{download_config["converted_ch4_pipes"]["path"]}') + excel_file_Neu = pd.ExcelFile(f'{path}/{download_config["new_constructed_pipes"]["path"]}') + excel_file_Erw = pd.ExcelFile(f'{path}/{download_config["pipes_of_further_h2_grid_operators"]["path"]}') + + df_Um= pd.read_excel(excel_file_Um, header=3) + df_Neu = pd.read_excel(excel_file_Neu, header=3) + df_Erw = pd.read_excel(excel_file_Erw, header=2) + + return df_Neu, df_Um, df_Erw + +def fix_h2_grid_infrastructure(df): + """ + Manuell adjustments for more accurate grid topology based on Detailmaßnahmenkarte der + FNB-Gas [https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/] + + Returns + ------- + df : + + """ + + df = replace_pipeline(df, 'Lubmin', 'Uckermark', 'Wrangelsburg') + df = replace_pipeline(df, 'Wrangelsburg', 'Uckermark', 'Schönermark') + df = replace_pipeline(df, 'Hemmingstedt', 'Ascheberg (Holstein)', 'Remmels Nord') + df = replace_pipeline(df, 'Heidenau', 'Elbe-Süd', 'Weißenfelde') + df = replace_pipeline(df, 'Weißenfelde', 'Elbe-Süd', 'Stade') + df = replace_pipeline(df, 'Stade AOS', 'KW Schilling', 'Abzweig Stade') + df = replace_pipeline(df, 'Rosengarten (Sottorf)', 'Moorburg', 'Leversen') + df = replace_pipeline(df, 'Leversen', 'Moorburg', 'Hamburg Süd') + df = replace_pipeline(df, 'Achim', 'Folmhusen', 'Wardenburg') + df = replace_pipeline(df, 'Achim', 'Wardenburg', 'Sandkrug') + df = replace_pipeline(df, 'Dykhausen', 'Bunde', 'Emden') + df = replace_pipeline(df, 'Emden', 'Nüttermoor', 'Jemgum') + df = replace_pipeline(df, 'Rostock', 'Glasewitz', 'Fliegerhorst Laage') + df = replace_pipeline(df, 'Wilhelmshaven', 'Dykhausen', 'Sande') + df = replace_pipeline(df, 'Wilhelmshaven Süd', 'Wilhelmshaven Nord', 'Wilhelmshaven') + df = replace_pipeline(df, 'Sande', 'Jemgum', 'Westerstede') + df = replace_pipeline(df, 'Kalle', 'Ochtrup', 'Frensdorfer Bruchgraben') + df = replace_pipeline(df, 'Frensdorfer Bruchgraben', 'Ochtrup', 'Bad Bentheim') + df = replace_pipeline(df, 'Bunde', 'Wettringen', 'Emsbüren') + df = replace_pipeline(df, 'Emsbüren', 'Dorsten', 'Ochtrup') + df = replace_pipeline(df, 'Ochtrup', 'Dorsten', 'Heek') + df = replace_pipeline(df, 'Lemförde', 'Drohne', 'Reiningen') + df = replace_pipeline(df, 'Edesbüttel', 'Bobbau', 'Uhrsleben') + df = replace_pipeline(df, 'Sixdorf', 'Wiederitzsch', 'Cörmigk') + df = replace_pipeline(df, 'Schkeuditz', 'Plaußig', 'Wiederitzsch') + df = replace_pipeline(df, 'Wiederitzsch', 'Plaußig', 'Mockau Nord') + df = replace_pipeline(df, 'Bobbau', 'Rückersdorf', 'Nempitz') + df = replace_pipeline(df, 'Räpitz', 'Böhlen', 'Kleindalzig') + df = replace_pipeline(df, 'Buchholz', 'Friedersdorf', 'Werben') + df = replace_pipeline(df, 'Radeland', 'Uckermark', 'Friedersdorf') + df = replace_pipeline(df, 'Friedersdorf', 'Uckermark', 'Herzfelde') + df = replace_pipeline(df, 'Blumberg', 'Berlin-Mitte', 'Berlin-Marzahn') + df = replace_pipeline(df, 'Radeland', 'Zethau', 'Coswig') + df = replace_pipeline(df, 'Leuna', 'Böhlen', 'Räpitz') + df = replace_pipeline(df, 'Dürrengleina', 'Stadtroda', 'Zöllnitz') + df = replace_pipeline(df, 'Mailing', 'Kötz', 'Wertingen') + df = replace_pipeline(df, 'Lampertheim', 'Rüsselsheim', 'Gernsheim-Nord') + df = replace_pipeline(df, 'Birlinghoven', 'Rüsselsheim', 'Wiesbaden') + df = replace_pipeline(df, 'Medelsheim', 'Mittelbrunn', 'Seyweiler') + df = replace_pipeline(df, 'Seyweiler', 'Dillingen', 'Fürstenhausen') + df = replace_pipeline(df, 'Reckrod', 'Wolfsbehringen', 'Eisenach') + df = replace_pipeline(df, 'Elten', 'St. Hubert', 'Hüthum') + df = replace_pipeline(df, 'St. Hubert', 'Hüthum', 'Uedener Bruch') + df = replace_pipeline(df, 'Wallach', 'Möllen', 'Spellen') + df = replace_pipeline(df, 'St. Hubert', 'Glehn', 'Krefeld') + df = replace_pipeline(df, 'Neumühl', 'Werne', 'Bottrop') + df = replace_pipeline(df, 'Bottrop', 'Werne', 'Recklinghausen') + df = replace_pipeline(df, 'Werne', 'Eisenach', 'Arnsberg-Bruchhausen') + df = replace_pipeline(df, 'Dorsten', 'Gescher', 'Gescher Süd') + df = replace_pipeline(df, 'Dorsten', 'Hamborn', 'Averbruch') + df = replace_pipeline(df, 'Neumühl', 'Bruckhausen', 'Hamborn') + df = replace_pipeline(df, 'Werne', 'Paffrath', 'Westhofen') + df = replace_pipeline(df, 'Glehn', 'Voigtslach', 'Dormagen') + df = replace_pipeline(df, 'Voigtslach', 'Paffrath','Leverkusen') + df = replace_pipeline(df, 'Glehn', 'Ludwigshafen','Wesseling') + df = replace_pipeline(df, 'Rothenstadt', 'Rimpar','Reutles') + + return df + +def connect_saltcavern_to_h2_grid(scn_name): + """ + Connect each saltcavern with nearest H2-Bus of the H2-Grid and insert the links into the database + + Returns + ------- + None + + """ + + targets = config.datasets()["etrago_hydrogen"]["targets"] + sources = config.datasets()["etrago_hydrogen"]["sources"] engine = db.engine() + + db.execute_sql(f""" + DELETE FROM {targets["hydrogen_links"]["schema"]}.{targets["hydrogen_links"]["table"]} + WHERE "carrier" in ('H2_saltcavern') + AND scn_name = '{scn_name}'; + """) + h2_buses_query = f"""SELECT bus_id, x, y,ST_Transform(geom, 32632) as geom + FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE carrier = 'H2_grid' AND scn_name = '{scn_name}' + """ + h2_buses = gpd.read_postgis(h2_buses_query, engine) + + salt_caverns_query = f"""SELECT bus_id, x, y, ST_Transform(geom, 32632) as geom + FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE carrier = 'H2_saltcavern' AND scn_name = '{scn_name}' + """ + salt_caverns = gpd.read_postgis(salt_caverns_query, engine) + + max_link_id = db.next_etrago_id('link') + next_link_id = count(start=max_link_id, step=1) + scn_params = get_sector_parameters("gas", scn_name) + + H2_coords = np.array([(point.x, point.y) for point in h2_buses.geometry]) + H2_tree = cKDTree(H2_coords) + links=[] + for idx, bus_saltcavern in salt_caverns.iterrows(): + saltcavern_coords = [bus_saltcavern['geom'].x, bus_saltcavern['geom'].y] + + dist, nearest_idx = H2_tree.query(saltcavern_coords, k=1) + nearest_h2_bus = h2_buses.iloc[nearest_idx] + + link = { + 'scn_name': scn_name, + 'bus0': nearest_h2_bus['bus_id'], + 'bus1': bus_saltcavern['bus_id'], + 'link_id': next(next_link_id), + 'carrier': 'H2_saltcavern', + 'lifetime':25, + 'p_nom_extendable': True, + 'p_min_pu': -1, + 'capital_cost': scn_params["overnight_cost"]["H2_pipeline"]*dist/1000, + 'geom': MultiLineString([LineString([(nearest_h2_bus['x'], nearest_h2_bus['y']), (bus_saltcavern['x'], bus_saltcavern['y'])])]) + } + links.append(link) - new_id = db.next_etrago_id("link") - pipelines["link_id"] = range(new_id, new_id + len(pipelines)) + links_df = gpd.GeoDataFrame(links, geometry='geom', crs=4326) - pipelines.to_crs(epsg=4326).to_postgis( - "egon_etrago_link", + links_df.to_postgis( + targets["hydrogen_links"]["table"], engine, - schema="grid", + schema=targets["hydrogen_links"]["schema"], index=False, if_exists="append", - dtype={"topo": Geometry()}, + dtype={"geom": Geometry()}, ) - new_id = db.next_etrago_id("link") - new_pipelines["link_id"] = range(new_id, new_id + len(new_pipelines)) - new_pipelines.to_postgis( - "egon_etrago_h2_link", - engine, - schema="grid", - index=False, - if_exists="replace", - dtype={"geom": Geometry(), "topo": Geometry()}, - ) +def connect_h2_grid_to_neighbour_countries(scn_name): + """ + Connect germand H2_grid with neighbour countries. All german H2-Buses wich were planned as connection + points for Import/Export of Hydrogen to corresponding neighbours country, are based on Publication + of FNB-GAS (https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/). + + Returns + ------- + None + + """ + engine = db.engine() + targets = config.datasets()["etrago_hydrogen"]["targets"] + sources = config.datasets()["etrago_hydrogen"]["sources"] + + h2_buses_df = gpd.read_postgis( + f""" + SELECT bus_id, x, y, geom + FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE carrier in ('H2_grid') + AND scn_name = '{scn_name}' - db.execute_sql( """ - select UpdateGeometrySRID('grid', 'egon_etrago_h2_link', 'topo', 4326) ; - - INSERT INTO grid.egon_etrago_link (scn_name, capital_cost, - link_id, carrier, - bus0, bus1, p_min_pu, - p_nom_extendable, length, - geom, topo) - SELECT scn_name, capital_cost, - link_id, carrier, - bus0, bus1, p_min_pu, - p_nom_extendable, length, - geom, topo - - FROM grid.egon_etrago_h2_link; - - DROP TABLE grid.egon_etrago_h2_link; + , engine) + + h2_links_df = pd.read_sql( + f""" + SELECT link_id, bus0, bus1, p_nom + FROM {sources["links"]["schema"]}.{sources["links"]["table"]} + WHERE carrier in ('H2_grid') + AND scn_name = '{scn_name}' + """ + , engine) + + abroad_buses_df = gpd.read_postgis( + f""" + SELECT bus_id, x, y, geom, country + FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]} + WHERE carrier = 'H2' AND scn_name = '{scn_name}' AND country != 'DE' + """, + engine + ) + + abroad_con_buses = [ + ('Greifenhagen', 'PL'), + ('Fürstenberg (PL)', 'PL'), + ('Eynatten', 'BE'), + ('Überackern', 'AT'), + ('Vlieghuis', 'NL'), + ('Oude', 'NL'), + ('Oude Statenzijl', 'NL'), + ('Vreden', 'NL'), + ('Elten', 'NL'), + ('Leidingen', 'FR'), + ('Carling', 'FR'), + ('Medelsheim', 'FR'), + ('Waidhaus', 'CZ'), + ('Deutschneudorf', 'CZ'), + ('Grenzach', 'CH'), + ('AWZ', 'DK'), + ('AWZ', 'SE'), + ('AQD Offshore SEN 1', 'GB'), + ('AQD Offshore SEN 1', 'NO'), + ('AQD Offshore SEN 1', 'DK'), + ('AQD Offshore SEN 1', 'NL'), + ('Fessenheim', 'FR'), + ('Ellund', 'DK') + ] + + h2_bus_location = pd.read_csv(Path(".")/"h2_grid_nodes.csv") + + ### prepare data for connecting abroad_buses + matched_locations = h2_bus_location[h2_bus_location['Ort'].isin([name for name, _ in abroad_con_buses])] + matched_buses = matched_locations.merge( + h2_buses_df, + left_on=['x', 'y'], + right_on=['x', 'y'], + how='inner' + ) + + final_matched_buses = matched_buses[['bus_id', 'Ort', 'x', 'y', 'geom_y']].rename(columns={'geom_y': 'geom'}) + + abroad_links = h2_links_df[(h2_links_df['bus0'].isin(final_matched_buses['bus_id'])) | (h2_links_df['bus1'].isin(final_matched_buses['bus_id']))] + abroad_links_bus0 = abroad_links.merge(final_matched_buses, left_on= 'bus0', right_on= 'bus_id', how='inner') + abroad_links_bus1 = abroad_links.merge(final_matched_buses, left_on= 'bus1', right_on= 'bus_id', how='inner') + abroad_con_df = pd.concat([abroad_links_bus1, abroad_links_bus0]) + + + connection_links = [] + max_link_id = db.next_etrago_id('link') + next_max_link_id = count(start=max_link_id, step=1) + + for inland_name, country_code in abroad_con_buses: + # filter out germand h2_buses for connecting neighbour-countries + inland_bus = abroad_con_df[abroad_con_df['Ort'] == inland_name] + if inland_bus.empty: + print(f"Warning: No Inland-Bus found for {inland_name}.") + continue + + # filter out corresponding abroad_bus for connecting neighbour countries + abroad_bus = abroad_buses_df[abroad_buses_df['country'] == country_code] + if abroad_bus.empty: + print(f"Warning: No Abroad-Bus found for {country_code}.") + continue + + for _, i_bus in inland_bus.iterrows(): + abroad_bus['distance'] = abroad_bus['geom'].apply( + lambda g: i_bus['geom'].distance(g) + ) + + nearest_abroad_bus = abroad_bus.loc[abroad_bus['distance'].idxmin()] + relevant_buses = inland_bus[inland_bus['bus_id'] == i_bus['bus_id']] + p_nom_value = relevant_buses['p_nom'].sum() + + connection_links.append({ + 'scn_name': scn_name, + 'carrier': 'H2_grid', + 'link_id': next(next_max_link_id), + 'bus0': i_bus['bus_id'], + 'bus1': nearest_abroad_bus['bus_id'], + 'p_nom': p_nom_value, + 'p_min_pu': -1, + 'geom': MultiLineString( + [LineString([ + (i_bus['geom'].x, i_bus['geom'].y), + (nearest_abroad_bus['geom'].x, nearest_abroad_bus['geom'].y) + ])] + ) + }) + connection_links_df = gpd.GeoDataFrame(connection_links, geometry='geom', crs="EPSG:4326") + + connection_links_df.to_postgis( + name=targets["hydrogen_links"]["table"], + con=engine, + schema=targets["hydrogen_links"]["schema"], + if_exists="append", + index=False, ) + print("Neighbour countries are succesfully connected to H2-grid") + diff --git a/src/egon/data/datasets/hydrogen_etrago/h2_to_ch4.py b/src/egon/data/datasets/hydrogen_etrago/h2_to_ch4.py index fb9ff021c..72db954ff 100755 --- a/src/egon/data/datasets/hydrogen_etrago/h2_to_ch4.py +++ b/src/egon/data/datasets/hydrogen_etrago/h2_to_ch4.py @@ -14,137 +14,127 @@ """ from geoalchemy2.types import Geometry +import geopandas as gpd +import numpy as np +from scipy.spatial import cKDTree +from shapely.geometry import LineString, MultiLineString from egon.data import db, config -from egon.data.datasets.etrago_setup import link_geom_from_buses from egon.data.datasets.scenario_parameters import get_sector_parameters def insert_h2_to_ch4_to_h2(): """ - Inserts methanisation, feed in and SMR links into the database - + Method for implementing Methanisation as optional usage of H2-Production; + For H2_Buses and CH4_Buses with distance < 10 km Methanisation/SMR Link will be implemented + Define the potentials for methanisation and Steam Methane Reaction - (SMR) modelled as extendable links as well as the H2 feedin - capacities modelled as non extendable links and insert all of them - into the database. - These tree technologies are connecting CH4 and H2 buses only. - - The capacity of the H2_feedin links is considerated as constant and - calculated as the sum of the capacities of the CH4 links connected - to the CH4 bus multiplied by the H2 energy share allowed to be fed. - This share is calculated in the function :py:func:`H2_CH4_mix_energy_fractions`. - - This function inserts data into the database and has no return. - + (SMR) modelled as extendable links + + Returns + ------- + None + """ + scenarios = config.settings()["egon-data"]["--scenarios"] + con=db.engine() + target_links = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_links"] + target_buses = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_buses"] + if "status2019" in scenarios: scenarios.remove("status2019") for scn_name in scenarios: - # Connect to local database - engine = db.engine() - - # Select CH4 and corresponding H2 buses - # No geometry required in this case! - buses = db.select_dataframe( - f""" - SELECT * FROM grid.egon_etrago_ch4_h2 WHERE scn_name = '{scn_name}' - """ - ) - - methanation = buses.copy().rename( - columns={"bus_H2": "bus0", "bus_CH4": "bus1"} - ) - SMR = buses.copy().rename( - columns={"bus_H2": "bus1", "bus_CH4": "bus0"} - ) - - # Delete old entries - db.execute_sql( - f""" - DELETE FROM grid.egon_etrago_link WHERE "carrier" IN - ('H2_to_CH4', 'H2_feedin', 'CH4_to_H2') AND scn_name = '{scn_name}' - AND bus0 NOT IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = '{scn_name}' AND country != 'DE' - ) AND bus1 NOT IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = '{scn_name}' AND country != 'DE' - ); - """ - ) - + + db.execute_sql(f""" + DELETE FROM {target_links["schema"]}.{target_links["table"]} WHERE "carrier" in ('H2_to_CH4', 'CH4_to_H2') + AND scn_name = '{scn_name}' AND bus0 IN ( + SELECT bus_id + FROM {target_buses["schema"]}.{target_buses["table"]} + WHERE country = 'DE'; + """) + + sql_CH4_buses = f""" + SELECT bus_id, x, y, ST_Transform(geom, 32632) as geom + FROM {target_buses["schema"]}.{target_buses["table"]} + WHERE carrier = 'CH4' + AND scn_name = '{scn_name}' AND country = 'DE' + """ + sql_H2_buses = f""" + SELECT bus_id, x, y, ST_Transform(geom, 32632) as geom + FROM {target_buses["schema"]}.{target_buses["table"]} + WHERE carrier in ('H2') + AND scn_name = '{scn_name}' AND country = 'DE' + """ + CH4_buses = gpd.read_postgis(sql_CH4_buses, con) + H2_buses = gpd.read_postgis(sql_H2_buses, con) + + CH4_to_H2_links = [] + H2_to_CH4_links = [] + + CH4_coords = np.array([(point.x, point.y) for point in CH4_buses.geometry]) + CH4_tree = cKDTree(CH4_coords) + + for idx, h2_bus in H2_buses.iterrows(): + h2_coords = [h2_bus['geom'].x, h2_bus['geom'].y] + + #Filter nearest CH4_bus + dist, nearest_idx = CH4_tree.query(h2_coords, k=1) + nearest_ch4_bus = CH4_buses.iloc[nearest_idx] + + if dist < 10000: + CH4_to_H2_links.append({ + 'scn_name': scn_name, + 'link_id': None, + 'bus0': nearest_ch4_bus['bus_id'], + 'bus1': h2_bus['bus_id'], + 'geom': MultiLineString([LineString([(h2_bus['x'], h2_bus['y']), (nearest_ch4_bus['x'], nearest_ch4_bus['y'])])]) + }) + + H2_to_CH4_links = [ + { + 'scn_name': link['scn_name'], + 'link_id': link['link_id'], + 'bus0': link['bus1'], # Swap bus0 and bus1 + 'bus1': link['bus0'], + 'geom': link['geom'] + } + for link in CH4_to_H2_links + ] + + #set crs for geoDataFrame + CH4_to_H2_links = gpd.GeoDataFrame(CH4_to_H2_links, geometry='geom', crs=4326) + H2_to_CH4_links = gpd.GeoDataFrame(H2_to_CH4_links, geometry='geom', crs=4326) + + scn_params = get_sector_parameters("gas", scn_name) - - technology = [methanation, SMR] - links_names = ["H2_to_CH4", "CH4_to_H2"] - - if scn_name == "eGon2035": - feed_in = methanation.copy() - pipeline_capacities = db.select_dataframe( - f""" - SELECT bus0, bus1, p_nom FROM grid.egon_etrago_link - WHERE scn_name = '{scn_name}' AND carrier = 'CH4' - AND ( - bus0 IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = '{scn_name}' AND country = 'DE' - ) OR bus1 IN ( - SELECT bus_id FROM grid.egon_etrago_bus - WHERE scn_name = '{scn_name}' AND country = 'DE' - ) - ); - """ - ) - - feed_in["p_nom"] = 0 - feed_in["p_nom_extendable"] = False - # calculation of H2 energy share via volumetric share outsourced - # in a mixture of H2 and CH4 with 15 %vol share - H2_share = scn_params["H2_feedin_volumetric_fraction"] - H2_energy_share = H2_CH4_mix_energy_fractions(H2_share) - - for bus in feed_in["bus1"].values: - # calculate the total pipeline capacity connected to a specific bus - nodal_capacity = pipeline_capacities.loc[ - (pipeline_capacities["bus0"] == bus) - | (pipeline_capacities["bus1"] == bus), - "p_nom", - ].sum() - # multiply total pipeline capacity with H2 energy share corresponding - # to volumetric share - feed_in.loc[feed_in["bus1"] == bus, "p_nom"] = ( - nodal_capacity * H2_energy_share - ) - technology.append(feed_in) - links_names.append("H2_feedin") - + technology = [CH4_to_H2_links, H2_to_CH4_links] + links_carriers = ["CH4_to_H2", "H2_to_CH4"] + # Write new entries - for table, carrier in zip(technology, links_names): + for table, carrier in zip(technology, links_carriers): # set parameters according to carrier name table["carrier"] = carrier - table["efficiency"] = scn_params["efficiency"][carrier] - if carrier != "H2_feedin": - table["p_nom_extendable"] = True - table["capital_cost"] = scn_params["capital_cost"][carrier] - table["lifetime"] = scn_params["lifetime"][carrier] + table["efficiency"] = scn_params["efficiency"][carrier] + table["p_nom_extendable"] = True + table["capital_cost"] = scn_params["capital_cost"][carrier] + table["lifetime"] = scn_params["lifetime"][carrier] new_id = db.next_etrago_id("link") table["link_id"] = range(new_id, new_id + len(table)) - table = link_geom_from_buses(table, scn_name) table.to_postgis( - "egon_etrago_link", - engine, - schema="grid", + target_links["table"], + con, + schema=target_links["schema"], index=False, if_exists="append", - dtype={"topo": Geometry()}, + dtype={"geom": Geometry()}, ) + def H2_CH4_mix_energy_fractions(x, T=25, p=50): """ diff --git a/src/egon/data/datasets/hydrogen_etrago/storage.py b/src/egon/data/datasets/hydrogen_etrago/storage.py index a8de73f6f..bba4ed3aa 100755 --- a/src/egon/data/datasets/hydrogen_etrago/storage.py +++ b/src/egon/data/datasets/hydrogen_etrago/storage.py @@ -45,7 +45,7 @@ def insert_H2_overground_storage(): f""" SELECT bus_id, scn_name, geom FROM {sources['buses']['schema']}. - {sources['buses']['table']} WHERE carrier = 'H2' + {sources['buses']['table']} WHERE carrier IN ('H2', 'H2_grid') AND scn_name = '{scn_name}' AND country = 'DE'""", index_col="bus_id", )