Passed
Pull Request — dev (#1278)
by
unknown
02:07
created

download_h2_grid_data()   A

Complexity

Conditions 5

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 33
rs 9.1832
c 0
b 0
f 0
cc 5
nop 0
1
"""
2
The central module containing all code dealing with the H2 grid in eGon100RE
3
4
The H2 grid, present only in eGon100RE, is composed of two parts:
5
  * a fixed part with the same topology than the CH4 grid and with
6
    carrier 'H2_retrofit' corresponding to the retrofiting of a share of
7
    the CH4 grid into an hydrogen grid,
8
  * an extendable part with carrier 'H2_gridextension', linking each
9
    H2_salcavern bus to the closest H2 bus: this part as no
10
    capacity (p_nom = 0) but it could be extended.
11
As the CH4 grid, the H2 pipelines are modelled by PyPSA links.
12
13
"""
14
from geoalchemy2.types import Geometry
15
from shapely.geometry import LineString, MultiLineString, Point
16
import geopandas as gpd
17
import pandas as pd
18
import os
19
from urllib.request import urlretrieve
20
from pathlib import Path
21
from fuzzywuzzy import process
22
from shapely import wkb
23
import math
24
import re
25
from itertools import count
26
import numpy as np
27
from scipy.spatial import cKDTree
28
29
from egon.data import config, db
30
from egon.data.datasets.scenario_parameters import get_sector_parameters
31
from egon.data.datasets.scenario_parameters.parameters import annualize_capital_costs
32
33
34
def insert_h2_pipelines(scn_name):
35
    "Insert H2_grid based on Input Data from FNB-Gas"
36
    
37
    download_h2_grid_data()
38
    H2_grid_Neubau, H2_grid_Umstellung, H2_grid_Erweiterung = read_h2_excel_sheets()
39
    h2_bus_location = pd.read_csv(Path(".")/"h2_grid_nodes.csv")
40
    con=db.engine()
41
    
42
    sources = config.datasets()["etrago_hydrogen"]["sources"]
43
    target = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_links"]
44
    
45
    h2_buses_df = pd.read_sql(
46
    f"""
47
    SELECT bus_id, x, y FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]}
48
    WHERE carrier in ('H2_grid')
49
    AND scn_name = '{scn_name}'   
50
    """
51
    , con)
52
    
53
    # Delete old entries
54
    db.execute_sql(
55
        f"""
56
        DELETE FROM {target["schema"]}.{target["table"]}
57
        WHERE "carrier" = 'H2_grid'
58
        AND scn_name = '{scn_name}' AND bus0 IN (
59
          SELECT bus_id
60
          FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]}
61
          WHERE country = 'DE'
62
        )
63
        """
64
    )
65
    
66
    
67
    target = config.datasets()["etrago_hydrogen"]["targets"]["hydrogen_links"]
68
    
69
    for df in [H2_grid_Neubau, H2_grid_Umstellung, H2_grid_Erweiterung]:
70
        
71
        if df is H2_grid_Neubau:        
72
            df.rename(columns={'Planerische \nInbetriebnahme': 'Planerische Inbetriebnahme'}, inplace=True)
73
            df.loc[df['Endpunkt\n(Ort)'] == 'AQD Anlandung', 'Endpunkt\n(Ort)'] = 'Schillig' 
74
            df.loc[df['Endpunkt\n(Ort)'] == 'Hallendorf', 'Endpunkt\n(Ort)'] = 'Salzgitter'
75
        
76
        if df is H2_grid_Erweiterung:
77
            df.rename(columns={'Umstellungsdatum/ Planerische Inbetriebnahme': 'Planerische Inbetriebnahme', 
78
                               'Nenndurchmesser (DN)': 'Nenndurchmesser \n(DN)',
79
                               'Investitionskosten\n(Mio. Euro),\nKostenschätzung': 'Investitionskosten*\n(Mio. Euro)'}, 
80
                      inplace=True)
81
            df = df[df['Berücksichtigung im Kernnetz \n[ja/nein/zurückgezogen]'].str.strip().str.lower() == 'ja']
82
            df.loc[df['Endpunkt\n(Ort)'] == 'Osdorfer Straße', 'Endpunkt\n(Ort)'] = 'Berlin- Lichterfelde'
83
            
84
        h2_bus_location['Ort'] = h2_bus_location['Ort'].astype(str).str.strip()
85
        df['Anfangspunkt\n(Ort)'] = df['Anfangspunkt\n(Ort)'].astype(str).str.strip()
86
        df['Endpunkt\n(Ort)'] = df['Endpunkt\n(Ort)'].astype(str).str.strip()
87
88
        df = df[['Anfangspunkt\n(Ort)', 'Endpunkt\n(Ort)', 'Nenndurchmesser \n(DN)', 'Druckstufe (DP)\n[mind. 30 barg]', 
89
                              'Investitionskosten*\n(Mio. Euro)', 'Planerische Inbetriebnahme', 'Länge \n(km)']]
90
        
91
        # matching start- and endpoint of each pipeline with georeferenced data
92
        df['Anfangspunkt_matched'] = fuzzy_match(df, h2_bus_location, 'Anfangspunkt\n(Ort)')
93
        df['Endpunkt_matched'] = fuzzy_match(df, h2_bus_location, 'Endpunkt\n(Ort)')
94
        
95
        # manuell adjustments based on Detailmaßnahmenkarte der FNB-Gas [https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/]
96
        df= fix_h2_grid_infrastructure(df)
97
        
98
        df_merged = pd.merge(df, h2_bus_location[['Ort', 'geom', 'x', 'y']], 
99
                             how='left', left_on='Anfangspunkt_matched', right_on='Ort').rename(
100
                             columns={'geom': 'geom_start', 'x': 'x_start', 'y': 'y_start'})
101
        df_merged = pd.merge(df_merged, h2_bus_location[['Ort', 'geom', 'x', 'y']], 
102
                             how='left', left_on='Endpunkt_matched', right_on='Ort').rename(
103
                             columns={'geom': 'geom_end', 'x': 'x_end', 'y': 'y_end'})
104
                 
105
        H2_grid_df = df_merged.dropna(subset=['geom_start', 'geom_end'])   
106
        H2_grid_df = H2_grid_df[H2_grid_df['geom_start'] != H2_grid_df['geom_end']]
107
        H2_grid_df = pd.merge(H2_grid_df, h2_buses_df, how='left', left_on=['x_start', 'y_start'], right_on=['x','y']).rename(
108
            columns={'bus_id': 'bus0'})
109
        H2_grid_df = pd.merge(H2_grid_df, h2_buses_df, how='left', left_on=['x_end', 'y_end'], right_on=['x','y']).rename(
110
            columns={'bus_id': 'bus1'})
111
        H2_grid_df[['bus0', 'bus1']] = H2_grid_df[['bus0', 'bus1']].astype('Int64')
112
        
113
        H2_grid_df['geom_start'] = H2_grid_df['geom_start'].apply(lambda x: wkb.loads(bytes.fromhex(x)))
114
        H2_grid_df['geom_end'] = H2_grid_df['geom_end'].apply(lambda x: wkb.loads(bytes.fromhex(x)))
115
        H2_grid_df['topo'] = H2_grid_df.apply(
116
            lambda row: LineString([row['geom_start'], row['geom_end']]), axis=1)
117
        H2_grid_df['geom'] = H2_grid_df.apply(
118
            lambda row: MultiLineString([LineString([row['geom_start'], row['geom_end']])]), axis=1)
119
        H2_grid_gdf = gpd.GeoDataFrame(H2_grid_df, geometry='geom', crs=4326)
120
        
121
        scn_params = get_sector_parameters("gas", scn_name)        
122
        next_link_id = db.next_etrago_id('link')
123
        
124
        H2_grid_gdf['link_id'] = range(next_link_id, next_link_id + len(H2_grid_gdf))
125
        H2_grid_gdf['scn_name'] = scn_name
126
        H2_grid_gdf['carrier'] = 'H2_grid'
127
        H2_grid_gdf['Planerische Inbetriebnahme'] = H2_grid_gdf['Planerische Inbetriebnahme'].astype(str).apply(
128
                lambda x: int(re.findall(r'\d{4}', x)[-1]) if re.findall(r'\d{4}', x) else 
129
                            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)
130
        H2_grid_gdf['build_year'] = H2_grid_gdf['Planerische Inbetriebnahme'].astype('Int64')
131
        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 )
132
        H2_grid_gdf['p_nom_min'] = H2_grid_gdf['p_nom']
133
        H2_grid_gdf['p_nom_max'] = float("Inf")
134
        H2_grid_gdf['p_nom_extendable'] = False 
135
        H2_grid_gdf['lifetime'] = scn_params["lifetime"]["H2_pipeline"]
136
        H2_grid_gdf['capital_cost'] = H2_grid_gdf.apply(lambda row: annualize_capital_costs(
137
                    (float(row["Investitionskosten*\n(Mio. Euro)"]) * 10**6 / row['p_nom']) 
138
                    if pd.notna(row["Investitionskosten*\n(Mio. Euro)"]) 
139
                    and str(row["Investitionskosten*\n(Mio. Euro)"]).replace(",", "").replace(".", "").isdigit() 
140
                    and float(row["Investitionskosten*\n(Mio. Euro)"]) != 0  
141
                    else scn_params["overnight_cost"]["H2_pipeline"] * row['Länge \n(km)'], 
0 ignored issues
show
introduced by
The variable scn_params does not seem to be defined in case the for loop on line 69 is not entered. Are you sure this can never be the case?
Loading history...
142
                    row['lifetime'], 0.05), axis=1)
143
        H2_grid_gdf['p_min_pu'] = -1
144
        
145
        selected_columns = [
146
            'scn_name', 'link_id', 'bus0', 'bus1', 'build_year', 'p_nom', 'p_nom_min', 
147
            'p_nom_extendable', 'capital_cost', 'geom', 'topo', 'carrier', 'p_nom_max', 'p_min_pu',
148
        ]
149
        
150
        H2_grid_final=H2_grid_gdf[selected_columns]
151
        
152
        # Insert data to db
153
        H2_grid_final.to_postgis(
154
             target["table"],
155
             con,
156
             schema= target["schema"],
157
             if_exists="append",
158
             dtype={"geom": Geometry()},
159
        )
160
        
161
    #connect saltcaverns to H2_grid
162
    connect_saltcavern_to_h2_grid(scn_name)
163
    
164
    #connect neighbour countries to H2_grid
165
    connect_h2_grid_to_neighbour_countries(scn_name)
166
167
168
def replace_pipeline(df, start, end, intermediate):
169
    """
170
    Method for adjusting pipelines manually by splittiing pipeline with an intermediate point.
171
    
172
    Parameters
173
    ----------
174
    df : pandas.core.frame.DataFrame
175
        dataframe to be adjusted
176
    start: str
177
        startpoint of pipeline
178
    end: str
179
        endpoint of pipeline
180
    intermediate: str
181
        new intermediate point for splitting given pipeline
182
        
183
    Returns
184
    ---------
185
    df : <class 'pandas.core.frame.DataFrame'>
186
        adjusted dataframe
187
    
188
            
189
    """   
190
    # Find rows where the start and end points match
191
    mask = ((df['Anfangspunkt_matched'] == start) & (df['Endpunkt_matched'] == end)) | \
192
           ((df['Anfangspunkt_matched'] == end) & (df['Endpunkt_matched'] == start))
193
    
194
    # Separate the rows to replace
195
    if mask.any():
196
        df_replacement = df[~mask].copy()
197
        row_replaced = df[mask].iloc[0]
198
        
199
        # Add new rows for the split pipelines
200
        new_rows = pd.DataFrame({
201
            'Anfangspunkt_matched': [start, intermediate],
202
            'Endpunkt_matched': [intermediate, end],
203
            'Nenndurchmesser \n(DN)': [row_replaced['Nenndurchmesser \n(DN)'], row_replaced['Nenndurchmesser \n(DN)']],  
204
            'Druckstufe (DP)\n[mind. 30 barg]': [row_replaced['Druckstufe (DP)\n[mind. 30 barg]'], row_replaced['Druckstufe (DP)\n[mind. 30 barg]']], 
205
            'Investitionskosten*\n(Mio. Euro)': [row_replaced['Investitionskosten*\n(Mio. Euro)'], row_replaced['Investitionskosten*\n(Mio. Euro)']],  
206
            'Planerische Inbetriebnahme': [row_replaced['Planerische Inbetriebnahme'],row_replaced['Planerische Inbetriebnahme']],  
207
            'Länge \n(km)': [row_replaced['Länge \n(km)'], row_replaced['Länge \n(km)']]
208
            })
209
        
210
        df_replacement = pd.concat([df_replacement, new_rows], ignore_index=True)
211
        return df_replacement
212
    else:
213
        return df
214
215
216
217
def fuzzy_match(df1, df2, column_to_match, threshold=80):
218
    '''
219
    Method for matching input data of H2_grid with georeferenced data (even if the strings are not exact the same)
220
    
221
    Parameters
222
    ----------
223
    df1 : pandas.core.frame.DataFrame
224
        Input dataframe
225
    df2 : pandas.core.frame.DataFrame
226
        georeferenced dataframe with h2_buses
227
    column_to_match: str
228
        matching column
229
    treshhold: float
230
        matching percentage for succesfull comparison
231
    
232
    Returns
233
    ---------
234
    matched : list
235
        list with all matched location names        
236
   
237
    '''
238
    options = df2['Ort'].unique()
239
    matched = []
240
241
    # Compare every locationname in df1 with locationnames in df2
242
    for value in df1[column_to_match]:
243
        match, score = process.extractOne(value, options)
244
        if score >= threshold:
245
            matched.append(match)
246
        else:
247
            matched.append(None)
248
249
    return matched
250
251
252
def calculate_H2_capacity(pressure, diameter):
253
    '''
254
    Method for calculagting capacity of pipelines based on data input from FNB Gas
255
    
256
    Parameters
257
    ----------
258
    pressure : float
259
        input for pressure of pipeline
260
    diameter: float
261
        input for diameter of pipeline
262
    column_to_match: str
263
        matching column
264
    treshhold: float
265
        matching percentage for succesfull comparison
266
    
267
    Returns
268
    ---------
269
    energy_flow: float
270
        transmission capacity of pipeline
271
                  
272
    '''
273
      
274
    pressure = str(pressure).replace(",", ".")
275
    diameter =str(diameter)
276
    
277
    def convert_to_float(value):
278
        try:
279
            return float(value)
280
        except ValueError:
281
            return 400  #average value from data-source cause capacities of some lines are not fixed yet
282
    
283
    # in case of given range for pipeline-capacity calculate average value
284
    if '-' in diameter:
285
        diameters = diameter.split('-')
286
        diameter = (convert_to_float(diameters[0]) + convert_to_float(diameters[1])) / 2
287
    elif '/' in diameter:
288
        diameters = diameter.split('/')
289
        diameter = (convert_to_float(diameters[0]) + convert_to_float(diameters[1])) / 2
290
    else:
291
        try:
292
            diameter = float(diameter)
293
        except ValueError:
294
            diameter = 400 #average value from data-source
295
    
296
    if '-' in pressure:
297
        pressures = pressure.split('-')
298
        pressure = (float(pressures[0]) + float(pressures[1])) / 2
299
    elif '/' in pressure:
300
        pressures = pressure.split('/')
301
        pressure = (float(pressures[0]) + float(pressures[1])) / 2
302
    else:
303
        try:
304
            pressure = float(diameter)
305
        except ValueError:
306
            pressure = 70 #averaqge value from data-source
307
            
308
            
309
    velocity = 40   #source: L.Koops (2023): GAS PIPELINE VERSUS LIQUID HYDROGEN TRANSPORT – PERSPECTIVES FOR TECHNOLOGIES, ENERGY DEMAND ANDv TRANSPORT CAPACITY, AND IMPLICATIONS FOR AVIATION
310
    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
311
    density = pressure*10**5/(4.1243*10**3*temperature) #gasconstant H2 = 4.1243 [kJ/kgK]
312
    mass_flow = density*math.pi*((diameter/10**3)/2)**2*velocity
313
    energy_flow = mass_flow * 119.988         #low_heating_value H2 = 119.988 [MJ/kg]
314
315
    return energy_flow
316
317
318
def download_h2_grid_data():
319
    """
320
    Download Input data for H2_grid from FNB-Gas (https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/)
321
322
    The following data for H2 are downloaded into the folder
323
    ./datasets/h2_data:
324
      * Links (file Anlage_3_Wasserstoffkernnetz_Neubau.xlsx,
325
                    Anlage_4_Wasserstoffkernnetz_Umstellung.xlsx, 
326
                    Anlage_2_Wasserstoffkernetz_weitere_Leitungen.xlsx)
327
328
    Returns
329
    -------
330
    None
331
332
    """
333
    path = Path("datasets/h2_data") 
334
    os.makedirs(path, exist_ok=True)
335
    
336
    download_config = config.datasets()["etrago_hydrogen"]["sources"]["H2_grid"]
337
    target_file_Um = path / download_config["converted_ch4_pipes"]["path"]
338
    target_file_Neu = path / download_config["new_constructed_pipes"]["path"]
339
    target_file_Erw = path / download_config["pipes_of_further_h2_grid_operators"]["path"]
340
    
341
    for target_file in [target_file_Neu, target_file_Um, target_file_Erw]:
342
        if target_file is target_file_Um:
343
            url = download_config["converted_ch4_pipes"]["url"]
344
        elif target_file is target_file_Neu:
345
            url = download_config["new_constructed_pipes"]['url']
346
        else:
347
            url = download_config["pipes_of_further_h2_grid_operators"]['url']
348
        
349
        if not os.path.isfile(target_file):
350
            urlretrieve(url, target_file)
351
            
352
353
def read_h2_excel_sheets():
354
    """
355
   Read downloaded excel files with location names for future h2-pipelines
356
357
   Returns
358
   -------
359
   df_Neu : <class 'pandas.core.frame.DataFrame'>
360
   df_Um : <class 'pandas.core.frame.DataFrame'>
361
   df_Erw : <class 'pandas.core.frame.DataFrame'>
362
        
363
364
   """
365
    
366
    path = Path(".") / "datasets" / "h2_data"
367
    download_config = config.datasets()["etrago_hydrogen"]["sources"]["H2_grid"]
368
    excel_file_Um = pd.ExcelFile(f'{path}/{download_config["converted_ch4_pipes"]["path"]}')
369
    excel_file_Neu = pd.ExcelFile(f'{path}/{download_config["new_constructed_pipes"]["path"]}')
370
    excel_file_Erw = pd.ExcelFile(f'{path}/{download_config["pipes_of_further_h2_grid_operators"]["path"]}')
371
372
    df_Um= pd.read_excel(excel_file_Um, header=3)
373
    df_Neu = pd.read_excel(excel_file_Neu, header=3)
374
    df_Erw = pd.read_excel(excel_file_Erw, header=2)
375
    
376
    return df_Neu, df_Um, df_Erw
377
378
def fix_h2_grid_infrastructure(df):
379
    """
380
    Manuell adjustments for more accurate grid topology based on Detailmaßnahmenkarte der 
381
    FNB-Gas [https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/]
382
383
    Returns
384
    -------
385
    df : <class 'pandas.core.frame.DataFrame'>       
386
  
387
    """
388
   
389
    df = replace_pipeline(df, 'Lubmin', 'Uckermark', 'Wrangelsburg')
390
    df = replace_pipeline(df, 'Wrangelsburg', 'Uckermark', 'Schönermark')
391
    df = replace_pipeline(df, 'Hemmingstedt', 'Ascheberg (Holstein)', 'Remmels Nord')
392
    df = replace_pipeline(df, 'Heidenau', 'Elbe-Süd', 'Weißenfelde')
393
    df = replace_pipeline(df, 'Weißenfelde', 'Elbe-Süd', 'Stade')
394
    df = replace_pipeline(df, 'Stade AOS', 'KW Schilling', 'Abzweig Stade')
395
    df = replace_pipeline(df, 'Rosengarten (Sottorf)', 'Moorburg', 'Leversen')
396
    df = replace_pipeline(df, 'Leversen', 'Moorburg', 'Hamburg Süd')
397
    df = replace_pipeline(df, 'Achim', 'Folmhusen', 'Wardenburg')
398
    df = replace_pipeline(df, 'Achim', 'Wardenburg', 'Sandkrug')
399
    df = replace_pipeline(df, 'Dykhausen', 'Bunde', 'Emden')
400
    df = replace_pipeline(df, 'Emden', 'Nüttermoor', 'Jemgum')
401
    df = replace_pipeline(df, 'Rostock', 'Glasewitz', 'Fliegerhorst Laage')
402
    df = replace_pipeline(df, 'Wilhelmshaven', 'Dykhausen', 'Sande')
403
    df = replace_pipeline(df, 'Wilhelmshaven Süd', 'Wilhelmshaven Nord', 'Wilhelmshaven')
404
    df = replace_pipeline(df, 'Sande', 'Jemgum', 'Westerstede')  
405
    df = replace_pipeline(df, 'Kalle', 'Ochtrup', 'Frensdorfer Bruchgraben')  
406
    df = replace_pipeline(df, 'Frensdorfer Bruchgraben', 'Ochtrup', 'Bad Bentheim')
407
    df = replace_pipeline(df, 'Bunde', 'Wettringen', 'Emsbüren')
408
    df = replace_pipeline(df, 'Emsbüren', 'Dorsten', 'Ochtrup')
409
    df = replace_pipeline(df, 'Ochtrup', 'Dorsten', 'Heek')
410
    df = replace_pipeline(df, 'Lemförde', 'Drohne', 'Reiningen')
411
    df = replace_pipeline(df, 'Edesbüttel', 'Bobbau', 'Uhrsleben')
412
    df = replace_pipeline(df, 'Sixdorf', 'Wiederitzsch', 'Cörmigk')
413
    df = replace_pipeline(df, 'Schkeuditz', 'Plaußig', 'Wiederitzsch')
414
    df = replace_pipeline(df, 'Wiederitzsch', 'Plaußig', 'Mockau Nord')
415
    df = replace_pipeline(df, 'Bobbau', 'Rückersdorf', 'Nempitz')
416
    df = replace_pipeline(df, 'Räpitz', 'Böhlen', 'Kleindalzig')
417
    df = replace_pipeline(df, 'Buchholz', 'Friedersdorf', 'Werben')
418
    df = replace_pipeline(df, 'Radeland', 'Uckermark', 'Friedersdorf')
419
    df = replace_pipeline(df, 'Friedersdorf', 'Uckermark', 'Herzfelde')
420
    df = replace_pipeline(df, 'Blumberg', 'Berlin-Mitte', 'Berlin-Marzahn')
421
    df = replace_pipeline(df, 'Radeland', 'Zethau', 'Coswig')
422
    df = replace_pipeline(df, 'Leuna', 'Böhlen', 'Räpitz')
423
    df = replace_pipeline(df, 'Dürrengleina', 'Stadtroda', 'Zöllnitz')
424
    df = replace_pipeline(df, 'Mailing', 'Kötz', 'Wertingen')
425
    df = replace_pipeline(df, 'Lampertheim', 'Rüsselsheim', 'Gernsheim-Nord')
426
    df = replace_pipeline(df, 'Birlinghoven', 'Rüsselsheim', 'Wiesbaden')
427
    df = replace_pipeline(df, 'Medelsheim', 'Mittelbrunn', 'Seyweiler')
428
    df = replace_pipeline(df, 'Seyweiler', 'Dillingen', 'Fürstenhausen')
429
    df = replace_pipeline(df, 'Reckrod', 'Wolfsbehringen', 'Eisenach')
430
    df = replace_pipeline(df, 'Elten', 'St. Hubert', 'Hüthum')
431
    df = replace_pipeline(df, 'St. Hubert', 'Hüthum', 'Uedener Bruch')
432
    df = replace_pipeline(df, 'Wallach', 'Möllen', 'Spellen')
433
    df = replace_pipeline(df, 'St. Hubert', 'Glehn', 'Krefeld')
434
    df = replace_pipeline(df, 'Neumühl', 'Werne', 'Bottrop')
435
    df = replace_pipeline(df, 'Bottrop', 'Werne', 'Recklinghausen')
436
    df = replace_pipeline(df, 'Werne', 'Eisenach', 'Arnsberg-Bruchhausen')
437
    df = replace_pipeline(df, 'Dorsten', 'Gescher', 'Gescher Süd')
438
    df = replace_pipeline(df, 'Dorsten', 'Hamborn', 'Averbruch')
439
    df = replace_pipeline(df, 'Neumühl', 'Bruckhausen', 'Hamborn')
440
    df = replace_pipeline(df, 'Werne', 'Paffrath', 'Westhofen')
441
    df = replace_pipeline(df, 'Glehn', 'Voigtslach', 'Dormagen')
442
    df = replace_pipeline(df, 'Voigtslach', 'Paffrath','Leverkusen')
443
    df = replace_pipeline(df, 'Glehn', 'Ludwigshafen','Wesseling')
444
    df = replace_pipeline(df, 'Rothenstadt', 'Rimpar','Reutles')
445
    
446
    return df
447
448
def connect_saltcavern_to_h2_grid(scn_name):
449
    """
450
    Connect each saltcavern with nearest H2-Bus of the H2-Grid and insert the links into the database
451
452
    Returns
453
    -------
454
    None    
455
  
456
    """
457
    
458
    targets = config.datasets()["etrago_hydrogen"]["targets"]
459
    sources = config.datasets()["etrago_hydrogen"]["sources"]
460
    engine = db.engine()
461
    
462
    db.execute_sql(f"""
463
           DELETE FROM {targets["hydrogen_links"]["schema"]}.{targets["hydrogen_links"]["table"]} 
464
           WHERE "carrier" in ('H2_saltcavern')
465
           AND scn_name = '{scn_name}';    
466
           """)
467
    h2_buses_query = f"""SELECT bus_id, x, y,ST_Transform(geom, 32632) as geom 
468
                        FROM  {sources["buses"]["schema"]}.{sources["buses"]["table"]} 
469
                        WHERE carrier = 'H2_grid' AND scn_name = '{scn_name}'
470
                    """
471
    h2_buses = gpd.read_postgis(h2_buses_query, engine)
472
    
473
    salt_caverns_query = f"""SELECT bus_id, x, y, ST_Transform(geom, 32632) as geom 
474
                            FROM  {sources["buses"]["schema"]}.{sources["buses"]["table"]} 
475
                            WHERE carrier = 'H2_saltcavern'  AND scn_name = '{scn_name}'
476
                        """
477
    salt_caverns = gpd.read_postgis(salt_caverns_query, engine)
478
479
    max_link_id = db.next_etrago_id('link')
480
    next_link_id = count(start=max_link_id, step=1) 
481
    scn_params = get_sector_parameters("gas", scn_name) 
482
                
483
    H2_coords = np.array([(point.x, point.y) for point in h2_buses.geometry])
484
    H2_tree = cKDTree(H2_coords)
485
    links=[]
486
    for idx, bus_saltcavern in salt_caverns.iterrows():
487
        saltcavern_coords = [bus_saltcavern['geom'].x, bus_saltcavern['geom'].y]
488
        
489
        dist, nearest_idx = H2_tree.query(saltcavern_coords, k=1)   
490
        nearest_h2_bus = h2_buses.iloc[nearest_idx]
491
           
492
        link = {
493
            'scn_name': scn_name,
494
            'bus0': nearest_h2_bus['bus_id'],
495
            'bus1': bus_saltcavern['bus_id'],
496
            'link_id': next(next_link_id),
497
            'carrier': 'H2_saltcavern',
498
            'lifetime':25,
499
            'p_nom_extendable': True,
500
            'p_min_pu': -1,
501
            'capital_cost': scn_params["overnight_cost"]["H2_pipeline"]*dist/1000,
502
            'geom':  MultiLineString([LineString([(nearest_h2_bus['x'], nearest_h2_bus['y']), (bus_saltcavern['x'], bus_saltcavern['y'])])])       
503
        }
504
        links.append(link)
505
506
    links_df = gpd.GeoDataFrame(links, geometry='geom', crs=4326)
507
508
    links_df.to_postgis(
509
        targets["hydrogen_links"]["table"],
510
        engine,
511
        schema=targets["hydrogen_links"]["schema"],
512
        index=False,
513
        if_exists="append",
514
        dtype={"geom": Geometry()},
515
    )
516
517
518
def connect_h2_grid_to_neighbour_countries(scn_name):
519
    """
520
    Connect germand H2_grid with neighbour countries. All german H2-Buses wich were planned as connection 
521
    points for Import/Export of Hydrogen to corresponding neighbours country, are based on Publication 
522
    of FNB-GAS (https://fnb-gas.de/wasserstoffnetz-wasserstoff-kernnetz/). 
523
524
    Returns
525
    -------
526
    None    
527
  
528
    """
529
    engine = db.engine()
530
    targets = config.datasets()["etrago_hydrogen"]["targets"]
531
    sources = config.datasets()["etrago_hydrogen"]["sources"]
532
    
533
    h2_buses_df = gpd.read_postgis(
534
    f"""
535
    SELECT bus_id, x, y, geom  
536
    FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]}
537
    WHERE carrier in ('H2_grid')
538
    AND scn_name = '{scn_name}'
539
540
    """
541
    , engine)
542
    
543
    h2_links_df = pd.read_sql(
544
    f"""
545
    SELECT link_id, bus0, bus1, p_nom 
546
    FROM {sources["links"]["schema"]}.{sources["links"]["table"]}
547
    WHERE carrier in ('H2_grid')
548
    AND scn_name = '{scn_name}'
549
550
    """
551
    , engine)
552
    
553
    abroad_buses_df = gpd.read_postgis(
554
        f"""
555
        SELECT bus_id, x, y, geom, country 
556
        FROM {sources["buses"]["schema"]}.{sources["buses"]["table"]}
557
        WHERE carrier = 'H2' AND scn_name = '{scn_name}' AND country != 'DE'
558
        """,
559
        engine
560
    )
561
        
562
    abroad_con_buses = [
563
        ('Greifenhagen', 'PL'),
564
        ('Fürstenberg (PL)', 'PL'),
565
        ('Eynatten', 'BE'),
566
        ('Überackern', 'AT'),
567
        ('Vlieghuis', 'NL'),
568
        ('Oude', 'NL'),
569
        ('Oude Statenzijl', 'NL'),
570
        ('Vreden', 'NL'),
571
        ('Elten', 'NL'),
572
        ('Leidingen', 'FR'),
573
        ('Carling', 'FR'),
574
        ('Medelsheim', 'FR'),
575
        ('Waidhaus', 'CZ'),
576
        ('Deutschneudorf', 'CZ'),
577
        ('Grenzach', 'CH'),
578
        ('AWZ', 'DK'),
579
        ('AWZ', 'SE'),
580
        ('AQD Offshore SEN 1', 'GB'),
581
        ('AQD Offshore SEN 1', 'NO'),
582
        ('AQD Offshore SEN 1', 'DK'),
583
        ('AQD Offshore SEN 1', 'NL'),
584
        ('Fessenheim', 'FR'),
585
        ('Ellund', 'DK')
586
    ]
587
    
588
    h2_bus_location = pd.read_csv(Path(".")/"h2_grid_nodes.csv") 
589
     
590
    ### prepare data for connecting abroad_buses
591
    matched_locations = h2_bus_location[h2_bus_location['Ort'].isin([name for name, _ in abroad_con_buses])]
592
    matched_buses = matched_locations.merge(
593
        h2_buses_df, 
594
        left_on=['x', 'y'],
595
        right_on=['x', 'y'],  
596
        how='inner'
597
    )
598
599
    final_matched_buses = matched_buses[['bus_id', 'Ort', 'x', 'y', 'geom_y']].rename(columns={'geom_y': 'geom'})
600
601
    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']))]
602
    abroad_links_bus0 = abroad_links.merge(final_matched_buses, left_on= 'bus0', right_on= 'bus_id', how='inner') 
603
    abroad_links_bus1 = abroad_links.merge(final_matched_buses, left_on= 'bus1', right_on= 'bus_id', how='inner')
604
    abroad_con_df = pd.concat([abroad_links_bus1, abroad_links_bus0])
605
606
    
607
    connection_links = []        
608
    max_link_id = db.next_etrago_id('link')
609
    next_max_link_id = count(start=max_link_id, step=1)
610
611
    for inland_name, country_code in abroad_con_buses:
612
        # filter out germand h2_buses for connecting neighbour-countries
613
        inland_bus = abroad_con_df[abroad_con_df['Ort'] == inland_name]
614
        if inland_bus.empty:
615
            print(f"Warning: No Inland-Bus found for {inland_name}.")
616
            continue
617
618
        # filter out corresponding abroad_bus for connecting neighbour countries
619
        abroad_bus = abroad_buses_df[abroad_buses_df['country'] == country_code]
620
        if abroad_bus.empty:
621
            print(f"Warning: No Abroad-Bus found for {country_code}.")
622
            continue
623
624
        for _, i_bus in inland_bus.iterrows():
625
            abroad_bus['distance'] = abroad_bus['geom'].apply(
626
                lambda g: i_bus['geom'].distance(g)
0 ignored issues
show
introduced by
The variable i_bus does not seem to be defined for all execution paths.
Loading history...
627
            )
628
629
            nearest_abroad_bus = abroad_bus.loc[abroad_bus['distance'].idxmin()]
630
            relevant_buses = inland_bus[inland_bus['bus_id'] == i_bus['bus_id']]
631
            p_nom_value = relevant_buses['p_nom'].sum()
632
633
        connection_links.append({
634
            'scn_name': scn_name,
635
            'carrier': 'H2_grid',
636
            'link_id': next(next_max_link_id),
637
            'bus0': i_bus['bus_id'],
0 ignored issues
show
introduced by
The variable i_bus does not seem to be defined for all execution paths.
Loading history...
638
            'bus1': nearest_abroad_bus['bus_id'],
0 ignored issues
show
introduced by
The variable nearest_abroad_bus does not seem to be defined for all execution paths.
Loading history...
639
            'p_nom': p_nom_value,
0 ignored issues
show
introduced by
The variable p_nom_value does not seem to be defined for all execution paths.
Loading history...
640
            'p_min_pu': -1,
641
            'geom': MultiLineString(
642
                [LineString([
643
                    (i_bus['geom'].x, i_bus['geom'].y),
644
                    (nearest_abroad_bus['geom'].x, nearest_abroad_bus['geom'].y)
645
                ])]
646
            )
647
        })
648
    connection_links_df = gpd.GeoDataFrame(connection_links, geometry='geom', crs="EPSG:4326")
649
650
    connection_links_df.to_postgis(
651
        name=targets["hydrogen_links"]["table"],  
652
        con=engine, 
653
        schema=targets["hydrogen_links"]["schema"],  
654
        if_exists="append",
655
        index=False,
656
    )
657
    print("Neighbour countries are succesfully connected to H2-grid")