2023-03-06 08:27:45 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2024-02-19 15:21:48 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2021-2024 The PyPSA-Eur Authors
|
2023-03-06 17:49:23 +00:00
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: MIT
|
2021-11-03 19:34:43 +00:00
|
|
|
"""
|
2021-11-04 20:48:54 +00:00
|
|
|
Build import locations for fossil gas from entry-points, LNG terminals and
|
2023-03-09 11:45:43 +00:00
|
|
|
production sites with data from SciGRID_gas and Global Energy Monitor.
|
2021-11-03 19:34:43 +00:00
|
|
|
"""
|
|
|
|
|
2024-07-08 06:29:16 +00:00
|
|
|
import json
|
2021-11-03 19:34:43 +00:00
|
|
|
import logging
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
import geopandas as gpd
|
2021-11-10 17:05:47 +00:00
|
|
|
import pandas as pd
|
2024-02-12 10:53:20 +00:00
|
|
|
from _helpers import configure_logging, set_scenario_config
|
2021-11-14 15:51:34 +00:00
|
|
|
from cluster_gas_network import load_bus_regions
|
|
|
|
|
2024-01-19 09:47:58 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
|
|
|
|
def read_scigrid_gas(fn):
|
|
|
|
df = gpd.read_file(fn)
|
2024-07-08 06:29:16 +00:00
|
|
|
expanded_param = df.param.apply(json.loads).apply(pd.Series)
|
|
|
|
df = pd.concat([df, expanded_param], axis=1)
|
2021-11-03 19:34:43 +00:00
|
|
|
df.drop(["param", "uncertainty", "method"], axis=1, inplace=True)
|
|
|
|
return df
|
|
|
|
|
2023-02-16 17:27:57 +00:00
|
|
|
|
2023-07-31 08:52:37 +00:00
|
|
|
def build_gem_lng_data(fn):
|
2024-03-09 14:29:39 +00:00
|
|
|
df = pd.read_excel(fn, sheet_name="LNG terminals - data")
|
2023-02-16 10:57:33 +00:00
|
|
|
df = df.set_index("ComboID")
|
|
|
|
|
2024-01-19 11:16:07 +00:00
|
|
|
remove_country = ["Cyprus", "Turkey"] # noqa: F841
|
2024-01-19 11:37:07 +00:00
|
|
|
remove_terminal = [ # noqa: F841
|
2024-01-19 11:16:07 +00:00
|
|
|
"Puerto de la Luz LNG Terminal",
|
|
|
|
"Gran Canaria LNG Terminal",
|
2024-01-19 11:37:07 +00:00
|
|
|
]
|
2023-02-16 10:57:33 +00:00
|
|
|
|
2024-08-26 13:04:15 +00:00
|
|
|
status_list = ["Operating", "Construction"] # noqa: F841
|
|
|
|
|
2023-02-16 10:57:33 +00:00
|
|
|
df = df.query(
|
2024-08-26 13:04:15 +00:00
|
|
|
"Status in @status_list \
|
|
|
|
& FacilityType == 'Import' \
|
2023-02-16 10:57:33 +00:00
|
|
|
& Country != @remove_country \
|
|
|
|
& TerminalName != @remove_terminal \
|
2024-08-26 13:04:15 +00:00
|
|
|
& CapacityInMtpa != '--' \
|
|
|
|
& CapacityInMtpa != 0"
|
2023-02-16 10:57:33 +00:00
|
|
|
)
|
2023-02-16 17:27:57 +00:00
|
|
|
|
2023-02-16 10:57:33 +00:00
|
|
|
geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"])
|
2024-08-26 13:04:15 +00:00
|
|
|
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326")
|
|
|
|
return gdf
|
2023-02-16 10:57:33 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
|
2023-07-31 08:52:37 +00:00
|
|
|
def build_gem_prod_data(fn):
|
2024-03-09 14:29:39 +00:00
|
|
|
df = pd.read_excel(fn, sheet_name="Gas extraction - main")
|
2023-07-31 08:52:37 +00:00
|
|
|
df = df.set_index("GEM Unit ID")
|
|
|
|
|
2024-01-19 11:16:07 +00:00
|
|
|
remove_country = ["Cyprus", "Türkiye"] # noqa: F841
|
|
|
|
remove_fuel_type = ["oil"] # noqa: F841
|
2024-01-03 07:13:01 +00:00
|
|
|
|
2024-08-26 13:04:15 +00:00
|
|
|
status_list = ["operating", "in development"] # noqa: F841
|
|
|
|
|
2023-07-31 08:52:37 +00:00
|
|
|
df = df.query(
|
2024-08-26 13:04:15 +00:00
|
|
|
"Status in @status_list \
|
2023-07-31 08:52:37 +00:00
|
|
|
& 'Fuel type' != 'oil' \
|
|
|
|
& Country != @remove_country \
|
|
|
|
& ~Latitude.isna() \
|
|
|
|
& ~Longitude.isna()"
|
|
|
|
).copy()
|
|
|
|
|
2024-03-09 14:29:39 +00:00
|
|
|
p = pd.read_excel(fn, sheet_name="Gas extraction - production")
|
2023-07-31 08:52:37 +00:00
|
|
|
p = p.set_index("GEM Unit ID")
|
2024-08-26 13:04:15 +00:00
|
|
|
p = p[p["Fuel description"].str.contains("gas")]
|
2023-07-31 08:52:37 +00:00
|
|
|
|
|
|
|
capacities = pd.DataFrame(index=df.index)
|
|
|
|
for key in ["production", "production design capacity", "reserves"]:
|
2024-01-03 07:13:01 +00:00
|
|
|
cap = (
|
|
|
|
p.loc[p["Production/reserves"] == key, "Quantity (converted)"]
|
|
|
|
.groupby("GEM Unit ID")
|
|
|
|
.sum()
|
|
|
|
.reindex(df.index)
|
|
|
|
)
|
2023-07-31 08:52:37 +00:00
|
|
|
# assume capacity such that 3% of reserves can be extracted per year (25% quantile)
|
2024-01-03 07:13:01 +00:00
|
|
|
annualization_factor = 0.03 if key == "reserves" else 1.0
|
2023-07-31 08:52:37 +00:00
|
|
|
capacities[key] = cap * annualization_factor
|
|
|
|
|
2024-01-03 07:13:01 +00:00
|
|
|
df["mcm_per_year"] = (
|
|
|
|
capacities["production"]
|
|
|
|
.combine_first(capacities["production design capacity"])
|
2023-07-31 08:52:37 +00:00
|
|
|
.combine_first(capacities["reserves"])
|
2024-01-03 07:13:01 +00:00
|
|
|
)
|
2023-07-31 08:52:37 +00:00
|
|
|
|
|
|
|
geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"])
|
2024-08-26 13:04:15 +00:00
|
|
|
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326")
|
|
|
|
return gdf
|
2023-07-31 08:52:37 +00:00
|
|
|
|
|
|
|
|
2023-07-31 10:20:43 +00:00
|
|
|
def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries):
|
2021-11-03 19:34:43 +00:00
|
|
|
# LNG terminals
|
2023-07-31 08:52:37 +00:00
|
|
|
lng = build_gem_lng_data(gem_fn)
|
2022-12-15 10:42:21 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
# Entry points from outside the model scope
|
|
|
|
entry = read_scigrid_gas(entry_fn)
|
|
|
|
entry["from_country"] = entry.from_country.str.rstrip()
|
|
|
|
entry = entry.loc[
|
|
|
|
~(entry.from_country.isin(countries) & entry.to_country.isin(countries))
|
|
|
|
& ~entry.name.str.contains("Tegelen") # only take non-EU entries
|
|
|
|
| (entry.from_country == "NO") # malformed datapoint # entries from NO to GB
|
2024-07-08 06:29:16 +00:00
|
|
|
].copy()
|
2021-11-09 13:52:08 +00:00
|
|
|
|
2024-01-03 07:13:01 +00:00
|
|
|
sto = read_scigrid_gas(sto_fn)
|
2024-01-19 11:16:07 +00:00
|
|
|
remove_country = ["RU", "UA", "TR", "BY"] # noqa: F841
|
2024-07-08 06:29:16 +00:00
|
|
|
sto = sto.query("country_code not in @remove_country").copy()
|
2023-07-31 10:20:43 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
# production sites inside the model scope
|
2023-07-31 08:52:37 +00:00
|
|
|
prod = build_gem_prod_data(gem_fn)
|
2021-11-03 19:34:43 +00:00
|
|
|
|
2023-02-16 17:27:57 +00:00
|
|
|
mcm_per_day_to_mw = 437.5 # MCM/day to MWh/h
|
2023-07-31 08:52:37 +00:00
|
|
|
mcm_per_year_to_mw = 1.199 # MCM/year to MWh/h
|
2023-02-16 17:27:57 +00:00
|
|
|
mtpa_to_mw = 1649.224 # mtpa to MWh/h
|
2023-07-31 10:20:43 +00:00
|
|
|
mcm_to_gwh = 11.36 # MCM to GWh
|
|
|
|
lng["capacity"] = lng["CapacityInMtpa"] * mtpa_to_mw
|
|
|
|
entry["capacity"] = entry["max_cap_from_to_M_m3_per_d"] * mcm_per_day_to_mw
|
|
|
|
prod["capacity"] = prod["mcm_per_year"] * mcm_per_year_to_mw
|
|
|
|
sto["capacity"] = sto["max_cushionGas_M_m3"] * mcm_to_gwh
|
2021-11-09 13:52:08 +00:00
|
|
|
|
|
|
|
lng["type"] = "lng"
|
2021-11-10 17:05:47 +00:00
|
|
|
entry["type"] = "pipeline"
|
2021-11-09 13:52:08 +00:00
|
|
|
prod["type"] = "production"
|
2023-07-31 10:20:43 +00:00
|
|
|
sto["type"] = "storage"
|
2021-11-09 13:52:08 +00:00
|
|
|
|
2023-07-31 10:20:43 +00:00
|
|
|
sel = ["geometry", "capacity", "type"]
|
2021-11-09 13:52:08 +00:00
|
|
|
|
2023-07-31 10:20:43 +00:00
|
|
|
return pd.concat([prod[sel], entry[sel], lng[sel], sto[sel]], ignore_index=True)
|
2021-11-03 19:34:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if "snakemake" not in globals():
|
2023-03-06 18:09:45 +00:00
|
|
|
from _helpers import mock_snakemake
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
snakemake = mock_snakemake(
|
2023-02-16 17:27:57 +00:00
|
|
|
"build_gas_input_locations",
|
2024-08-26 13:04:15 +00:00
|
|
|
clusters="128",
|
2021-11-03 19:34:43 +00:00
|
|
|
)
|
|
|
|
|
2024-02-12 10:53:20 +00:00
|
|
|
configure_logging(snakemake)
|
|
|
|
set_scenario_config(snakemake)
|
2021-11-03 19:34:43 +00:00
|
|
|
|
2021-11-14 15:51:34 +00:00
|
|
|
regions = load_bus_regions(
|
|
|
|
snakemake.input.regions_onshore, snakemake.input.regions_offshore
|
|
|
|
)
|
|
|
|
|
|
|
|
# add a buffer to eastern countries because some
|
|
|
|
# entry points are still in Russian or Ukrainian territory.
|
|
|
|
buffer = 9000 # meters
|
|
|
|
eastern_countries = ["FI", "EE", "LT", "LV", "PL", "SK", "HU", "RO"]
|
|
|
|
add_buffer_b = regions.index.str[:2].isin(eastern_countries)
|
|
|
|
regions.loc[add_buffer_b] = (
|
|
|
|
regions[add_buffer_b].to_crs(3035).buffer(buffer).to_crs(4326)
|
2023-03-06 08:27:45 +00:00
|
|
|
)
|
2021-11-03 19:34:43 +00:00
|
|
|
|
2021-11-14 15:51:34 +00:00
|
|
|
countries = regions.index.str[:2].unique().str.replace("GB", "UK")
|
2021-11-04 20:48:54 +00:00
|
|
|
|
2021-11-03 19:34:43 +00:00
|
|
|
gas_input_locations = build_gas_input_locations(
|
2023-07-31 08:52:37 +00:00
|
|
|
snakemake.input.gem,
|
2021-11-03 19:34:43 +00:00
|
|
|
snakemake.input.entry,
|
2023-07-31 10:20:43 +00:00
|
|
|
snakemake.input.storage,
|
2021-11-04 20:48:54 +00:00
|
|
|
countries,
|
2021-11-03 19:34:43 +00:00
|
|
|
)
|
|
|
|
|
2021-11-14 15:51:34 +00:00
|
|
|
gas_input_nodes = gpd.sjoin(gas_input_locations, regions, how="left")
|
2021-11-09 13:52:08 +00:00
|
|
|
|
2024-07-08 06:29:16 +00:00
|
|
|
gas_input_nodes.rename(columns={"name": "bus"}, inplace=True)
|
2021-11-09 13:52:08 +00:00
|
|
|
|
|
|
|
gas_input_nodes.to_file(snakemake.output.gas_input_nodes, driver="GeoJSON")
|
|
|
|
|
2024-01-03 09:54:53 +00:00
|
|
|
ensure_columns = ["lng", "pipeline", "production", "storage"]
|
2021-11-10 17:05:47 +00:00
|
|
|
gas_input_nodes_s = (
|
2024-01-03 09:54:53 +00:00
|
|
|
gas_input_nodes.groupby(["bus", "type"])["capacity"]
|
|
|
|
.sum()
|
|
|
|
.unstack()
|
|
|
|
.reindex(columns=ensure_columns)
|
2023-03-06 08:27:45 +00:00
|
|
|
)
|
2023-07-31 10:20:43 +00:00
|
|
|
gas_input_nodes_s.columns.name = "capacity"
|
2021-11-03 19:34:43 +00:00
|
|
|
|
2021-11-09 13:52:08 +00:00
|
|
|
gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified)
|