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