2021-04-15 12:21:48 +00:00
|
|
|
"""
|
2021-06-21 10:34:08 +00:00
|
|
|
Builds clustered natural gas network based on data from:
|
2021-08-04 08:28:50 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
[1] the SciGRID Gas project
|
|
|
|
(https://www.gas.scigrid.de/)
|
2021-08-04 08:28:50 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
[2] ENTSOG capacity map
|
|
|
|
(https://www.entsog.eu/sites/default/files/2019-10/Capacities%20for%20Transmission%20Capacity%20Map%20RTS008_NS%20-%20DWH_final.xlsx)
|
2021-04-15 12:21:48 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
import re
|
|
|
|
import json
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
import pandas as pd
|
2021-06-21 10:34:08 +00:00
|
|
|
import geopandas as gpd
|
2021-06-23 12:53:54 +00:00
|
|
|
import numpy as np
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
from shapely.geometry import Point
|
|
|
|
|
|
|
|
|
|
|
|
def concat_gdf(gdf_list, crs='EPSG:4326'):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Convert to gepandas dataframe with given Coordinate Reference System (crs)."""
|
|
|
|
return gpd.GeoDataFrame(pd.concat(gdf_list),crs=crs)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
def string2list(string, with_None=True):
|
|
|
|
"""Convert string format to a list."""
|
|
|
|
p = re.compile('(?<!\\\\)\'')
|
|
|
|
string = p.sub('\"', string)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
if with_None:
|
|
|
|
p2 = re.compile('None')
|
|
|
|
string = p2.sub('\"None\"', string)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
return json.loads(string)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def load_gas_network(df_path):
|
|
|
|
"""Load and format gas network data."""
|
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
df = pd.read_csv(df_path, sep=',')
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
df.long = df.long.apply(string2list)
|
|
|
|
df.lat = df.lat.apply(string2list)
|
|
|
|
df.node_id = df.node_id.apply(string2list)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# pipes which can be used in both directions
|
|
|
|
both_direct_df = df[df.is_bothDirection == 1].reset_index(drop=True)
|
|
|
|
both_direct_df.node_id = both_direct_df.node_id.apply(lambda x: [x[1], x[0]])
|
|
|
|
both_direct_df.long = both_direct_df.long.apply(lambda x: [x[1], x[0]])
|
|
|
|
both_direct_df.lat = both_direct_df.lat.apply(lambda x: [x[1], x[0]])
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
df_singledirect = pd.concat([df, both_direct_df]).reset_index(drop=True)
|
|
|
|
df_singledirect.drop('is_bothDirection', axis=1)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# create shapely geometry points
|
|
|
|
df['point1'] = df.apply(lambda x: Point((x['long'][0], x['lat'][0])), axis=1)
|
|
|
|
df['point2'] = df.apply(lambda x: Point((x['long'][1], x['lat'][1])), axis=1)
|
|
|
|
df['point1_name'] = df.node_id.str[0]
|
|
|
|
df['point2_name'] = df.node_id.str[1]
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
part1 = df[['point1', 'point1_name']]
|
|
|
|
part2 = df[['point2', 'point2_name']]
|
|
|
|
part1.columns = ['geometry', 'name']
|
|
|
|
part2.columns = ['geometry', 'name']
|
|
|
|
points = [part1, part2]
|
|
|
|
points = concat_gdf(points)
|
|
|
|
points = points.drop_duplicates()
|
|
|
|
points.reset_index(drop=True, inplace=True)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
return df, points
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def load_bus_regions(onshore_path, offshore_path):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Load pypsa-eur on- and offshore regions and concat."""
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
bus_regions_offshore = gpd.read_file(offshore_path)
|
|
|
|
bus_regions_onshore = gpd.read_file(onshore_path)
|
|
|
|
bus_regions = concat_gdf([bus_regions_offshore, bus_regions_onshore])
|
|
|
|
bus_regions = bus_regions.dissolve(by='name', aggfunc='sum')
|
|
|
|
bus_regions = bus_regions.reset_index()
|
|
|
|
|
|
|
|
return bus_regions
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def points2buses(input_points, bus_regions):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Map gas network points to network buses depending on bus region."""
|
2021-08-04 08:28:50 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
points = input_points.copy()
|
|
|
|
points['bus'] = None
|
2021-08-04 08:28:50 +00:00
|
|
|
buses_list = set(bus_regions.name)
|
2021-06-21 10:34:08 +00:00
|
|
|
for bus in buses_list:
|
2021-08-04 08:28:50 +00:00
|
|
|
mask = bus_regions[bus_regions.name == bus]
|
2021-06-21 10:34:08 +00:00
|
|
|
index = gpd.clip(points, mask).index
|
|
|
|
points.loc[index, 'bus'] = bus
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
return points
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def build_gas_network_topology(df, points2buses):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Create gas network between pypsa buses.
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
df : pd.DataFrame
|
|
|
|
gas network data
|
|
|
|
points2buses_map : pd.DataFrame
|
|
|
|
mapping of gas network points to pypsa buses
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
gas_connections : pd.DataFrame
|
|
|
|
gas network connecting pypsa buses
|
2021-04-15 12:21:48 +00:00
|
|
|
"""
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
tmp_df = points2buses[['bus', 'name']]
|
|
|
|
|
|
|
|
tmp_df.columns = ['buses_start', 'name']
|
|
|
|
gas_connections = df.merge(tmp_df, left_on='point1_name', right_on='name')
|
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
tmp_df.columns = ['buses_destination', 'name']
|
2021-08-04 08:28:50 +00:00
|
|
|
gas_connections = gas_connections.merge(tmp_df, left_on='point2_name', right_on='name')
|
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# drop all pipes connecting the same bus
|
2021-08-04 08:28:50 +00:00
|
|
|
gas_connections = gas_connections[gas_connections.buses_start != gas_connections.buses_destination]
|
|
|
|
gas_connections.reset_index(drop=True, inplace=True)
|
|
|
|
gas_connections.drop(['point1', 'point2'], axis=1, inplace=True)
|
2021-06-21 10:34:08 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
return gas_connections
|
2021-06-21 10:34:08 +00:00
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def check_missing(nodes, gas_connections):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Check which nodes are not connected to the gas network."""
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
start_buses = gas_connections.buses_start.dropna().unique()
|
|
|
|
end_buses = gas_connections.buses_destination.dropna().unique()
|
|
|
|
|
|
|
|
missing_start = nodes[[bus not in start_buses for bus in nodes]]
|
|
|
|
missing_end = nodes[[bus not in end_buses for bus in nodes]]
|
|
|
|
|
|
|
|
logger.info(f"- The following buses are missing in gas network data as a start bus:"
|
|
|
|
f"\n {', '.join(map(str, missing_start))} \n"
|
|
|
|
f"- The following buses are missing in gas network data as an end bus:"
|
|
|
|
f"\n {', '.join(map(str, missing_end))} \n"
|
|
|
|
f"- The following buses are missing completely:"
|
|
|
|
f"\n {', '.join(map(str, missing_start.intersection(missing_end)))}")
|
|
|
|
|
|
|
|
|
|
|
|
def clean_dataset(nodes, gas_connections):
|
2021-06-21 10:34:08 +00:00
|
|
|
"""Convert units and save only necessary data."""
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
check_missing(nodes, gas_connections)
|
|
|
|
|
|
|
|
determine_pipe_capacity(gas_connections)
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'is_bothDirection',
|
|
|
|
'capacity_recalculated',
|
|
|
|
'buses_start',
|
|
|
|
'buses_destination',
|
|
|
|
'id',
|
|
|
|
'length_km'
|
|
|
|
]
|
|
|
|
clean_pipes = gas_connections[cols].dropna()
|
2021-06-21 10:34:08 +00:00
|
|
|
|
2021-06-23 12:53:54 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# convert GW -> MW
|
2021-06-23 12:53:54 +00:00
|
|
|
clean_pipes.loc[:, 'capacity_recalculated'] *= 1e3
|
2021-08-04 08:28:50 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# rename columns
|
2021-08-04 08:28:50 +00:00
|
|
|
to_rename = {
|
|
|
|
'capacity_recalculated': 'pipe_capacity_MW',
|
|
|
|
'buses_start': 'bus0',
|
|
|
|
'buses_destination': 'bus1'
|
|
|
|
}
|
|
|
|
clean_pipes.rename(columns=to_rename, inplace=True)
|
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
return clean_pipes
|
|
|
|
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
def diameter2capacity(pipe_diameter_mm):
|
2021-06-23 12:53:54 +00:00
|
|
|
"""Calculate pipe capacity based on diameter.
|
|
|
|
|
|
|
|
20 inch (500 mm) 50 bar -> 1.5 GW CH4 pipe capacity (LHV)
|
|
|
|
24 inch (600 mm) 50 bar -> 5 GW CH4 pipe capacity (LHV)
|
|
|
|
36 inch (900 mm) 50 bar -> 11.25 GW CH4 pipe capacity (LHV)
|
|
|
|
48 inch (1200 mm) 80 bar -> 21.7 GW CH4 pipe capacity (LHV)
|
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
Based on p.15 of https://gasforclimate2050.eu/wp-content/uploads/2020/07/2020_European-Hydrogen-Backbone_Report.pdf
|
|
|
|
"""
|
|
|
|
|
|
|
|
# slopes definitions
|
|
|
|
m0 = (5 - 1.5) / (600 - 500)
|
|
|
|
m1 = (11.25 - 5) / (900 - 600)
|
|
|
|
m2 = (21.7 - 11.25) / (1200 - 900)
|
|
|
|
|
|
|
|
# intercept
|
|
|
|
a0 = -16
|
|
|
|
a1 = -7.5
|
|
|
|
a2 = -20.1
|
2021-06-23 12:53:54 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
if pipe_diameter_mm < 500:
|
2021-06-23 12:53:54 +00:00
|
|
|
return np.nan
|
2021-08-04 08:28:50 +00:00
|
|
|
elif pipe_diameter_mm < 600:
|
|
|
|
return a0 + m0 * pipe_diameter_mm
|
|
|
|
elif pipe_diameter_mm < 900:
|
|
|
|
return a1 + m1 * pipe_diameter_mm
|
2021-06-23 12:53:54 +00:00
|
|
|
else:
|
2021-08-04 08:28:50 +00:00
|
|
|
return a2 + m2 * pipe_diameter_mm
|
2021-06-23 12:53:54 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
def determine_pipe_capacity(gas_network):
|
2021-06-23 12:53:54 +00:00
|
|
|
"""Check pipe capacity depending on diameter and pressure."""
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
gas_network["capacity_recalculated"] = gas_network.diameter_mm.apply(diameter2capacity)
|
|
|
|
|
2021-06-23 12:53:54 +00:00
|
|
|
# if pipe capacity smaller than 1.5 GW take original pipe capacity
|
2021-08-04 08:28:50 +00:00
|
|
|
low_cap = gas_network.Capacity_GWh_h < 1.5
|
|
|
|
gas_network.loc[low_cap, "capacity_recalculated"] = gas_network.loc[low_cap, "capacity_recalculated"].fillna(gas_network.loc[low_cap, "Capacity_GWh_h"])
|
|
|
|
|
|
|
|
# for pipes without diameter assume 500 mm diameter
|
2021-06-23 12:53:54 +00:00
|
|
|
gas_network["capacity_recalculated"].fillna(1.5, inplace=True)
|
2021-08-04 08:28:50 +00:00
|
|
|
|
|
|
|
# for nord stream take orginal data
|
2021-06-23 12:53:54 +00:00
|
|
|
nord_stream = gas_network[gas_network.max_pressure_bar==220].index
|
|
|
|
gas_network.loc[nord_stream, "capacity_recalculated"] = gas_network.loc[nord_stream, "Capacity_GWh_h"]
|
|
|
|
|
2021-04-15 12:21:48 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
if 'snakemake' not in globals():
|
2021-06-21 10:34:08 +00:00
|
|
|
from helper import mock_snakemake
|
|
|
|
snakemake = mock_snakemake('build_gas_network',
|
2021-08-04 08:28:50 +00:00
|
|
|
network='elec', simpl='', clusters='37',
|
|
|
|
lv='1.0', opts='', planning_horizons='2020',
|
|
|
|
sector_opts='168H-T-H-B-I')
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
logging.basicConfig(level=snakemake.config['logging_level'])
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# import gas network data
|
2021-08-04 08:28:50 +00:00
|
|
|
gas_network, points = load_gas_network(snakemake.input.gas_network)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# get clustered bus regions
|
2021-08-04 08:28:50 +00:00
|
|
|
bus_regions = load_bus_regions(
|
|
|
|
snakemake.input.regions_onshore,
|
|
|
|
snakemake.input.regions_offshore
|
|
|
|
)
|
|
|
|
nodes = pd.Index(bus_regions.name.unique())
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-06-21 10:34:08 +00:00
|
|
|
# map gas network points to network buses
|
2021-08-04 08:28:50 +00:00
|
|
|
points2buses_map = points2buses(points, bus_regions)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
# create gas network between pypsa nodes
|
|
|
|
gas_connections = build_gas_network_topology(gas_network, points2buses_map)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
gas_connections = clean_dataset(nodes, gas_connections)
|
2021-04-15 12:21:48 +00:00
|
|
|
|
2021-08-04 08:28:50 +00:00
|
|
|
gas_connections.to_csv(snakemake.output.clustered_gas_network)
|