2023-03-06 08:27:45 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2023-03-06 17:49:23 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2021-2023 The PyPSA-Eur Authors
|
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: MIT
|
2023-03-09 11:45:43 +00:00
|
|
|
"""
|
|
|
|
Compute biogas and solid biomass potentials for each clustered model region
|
|
|
|
using data from JRC ENSPRESO.
|
|
|
|
"""
|
2023-03-06 17:49:23 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
import geopandas as gpd
|
2023-03-06 08:27:45 +00:00
|
|
|
import pandas as pd
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def build_nuts_population_data(year=2013):
|
|
|
|
pop = pd.read_csv(
|
|
|
|
snakemake.input.nuts3_population,
|
2023-03-06 08:27:45 +00:00
|
|
|
sep=r"\,| \t|\t",
|
|
|
|
engine="python",
|
2021-07-11 15:52:32 +00:00
|
|
|
na_values=[":"],
|
2023-03-06 08:27:45 +00:00
|
|
|
index_col=1,
|
2021-07-11 15:52:32 +00:00
|
|
|
)[str(year)]
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
# only countries
|
|
|
|
pop.drop("EU28", inplace=True)
|
|
|
|
|
|
|
|
# mapping from Cantons to NUTS3
|
|
|
|
cantons = pd.read_csv(snakemake.input.swiss_cantons)
|
|
|
|
cantons = cantons.set_index(cantons.HASC.str[3:]).NUTS
|
2023-03-06 08:27:45 +00:00
|
|
|
cantons = cantons.str.pad(5, side="right", fillchar="0")
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
# get population by NUTS3
|
2023-03-06 08:27:45 +00:00
|
|
|
swiss = pd.read_excel(
|
|
|
|
snakemake.input.swiss_population, skiprows=3, index_col=0
|
|
|
|
).loc["Residents in 1000"]
|
2021-07-11 15:52:32 +00:00
|
|
|
swiss = swiss.rename(cantons).filter(like="CH")
|
|
|
|
|
|
|
|
# aggregate also to higher order NUTS levels
|
|
|
|
swiss = [swiss.groupby(swiss.index.str[:i]).sum() for i in range(2, 6)]
|
|
|
|
|
|
|
|
# merge Europe + Switzerland
|
2023-02-22 12:00:06 +00:00
|
|
|
pop = pd.concat([pop, pd.concat(swiss)]).to_frame("total")
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
# add missing manually
|
|
|
|
pop["AL"] = 2893
|
|
|
|
pop["BA"] = 3871
|
|
|
|
pop["RS"] = 7210
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
pop["ct"] = pop.index.str[:2]
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
return pop
|
|
|
|
|
|
|
|
|
|
|
|
def enspreso_biomass_potentials(year=2020, scenario="ENS_Low"):
|
2021-08-16 12:14:05 +00:00
|
|
|
"""
|
|
|
|
Loads the JRC ENSPRESO biomass potentials.
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-08-16 12:14:05 +00:00
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
year : int
|
|
|
|
The year for which potentials are to be taken.
|
|
|
|
Can be {2010, 2020, 2030, 2040, 2050}.
|
|
|
|
scenario : str
|
|
|
|
The scenario. Can be {"ENS_Low", "ENS_Med", "ENS_High"}.
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-08-16 12:14:05 +00:00
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
pd.DataFrame
|
|
|
|
Biomass potentials for given year and scenario
|
|
|
|
in TWh/a by commodity and NUTS2 region.
|
|
|
|
"""
|
2021-07-11 15:52:32 +00:00
|
|
|
glossary = pd.read_excel(
|
2021-08-10 08:28:50 +00:00
|
|
|
str(snakemake.input.enspreso_biomass),
|
2021-07-11 15:52:32 +00:00
|
|
|
sheet_name="Glossary",
|
|
|
|
usecols="B:D",
|
|
|
|
skiprows=1,
|
2023-03-06 08:27:45 +00:00
|
|
|
index_col=0,
|
2021-07-11 15:52:32 +00:00
|
|
|
)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
df = pd.read_excel(
|
2021-08-10 08:28:50 +00:00
|
|
|
str(snakemake.input.enspreso_biomass),
|
2021-07-11 15:52:32 +00:00
|
|
|
sheet_name="ENER - NUTS2 BioCom E",
|
2023-03-06 08:27:45 +00:00
|
|
|
usecols="A:H",
|
2021-07-11 15:52:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
df["group"] = df["E-Comm"].map(glossary.group)
|
|
|
|
df["commodity"] = df["E-Comm"].map(glossary.description)
|
|
|
|
|
|
|
|
to_rename = {
|
|
|
|
"NUTS2 Potential available by Bio Commodity": "potential",
|
|
|
|
"NUST2": "NUTS2",
|
|
|
|
}
|
|
|
|
df.rename(columns=to_rename, inplace=True)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
# fill up with NUTS0 if NUTS2 is not given
|
2023-03-06 08:27:45 +00:00
|
|
|
df.NUTS2 = df.apply(lambda x: x.NUTS0 if x.NUTS2 == "-" else x.NUTS2, axis=1)
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
# convert PJ to TWh
|
|
|
|
df.potential /= 3.6
|
|
|
|
df.Unit = "TWh/a"
|
|
|
|
|
|
|
|
dff = df.query("Year == @year and Scenario == @scenario")
|
|
|
|
|
|
|
|
bio = dff.groupby(["NUTS2", "commodity"]).potential.sum().unstack()
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
# currently Serbia and Kosovo not split, so aggregate
|
|
|
|
bio.loc["RS"] += bio.loc["XK"]
|
|
|
|
bio.drop("XK", inplace=True)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
return bio
|
|
|
|
|
|
|
|
|
2023-03-06 08:27:45 +00:00
|
|
|
def disaggregate_nuts0(bio):
|
2021-08-16 12:14:05 +00:00
|
|
|
"""
|
2023-03-06 08:27:45 +00:00
|
|
|
Some commodities are only given on NUTS0 level. These are disaggregated
|
|
|
|
here using the NUTS2 population as distribution key.
|
|
|
|
|
2021-08-16 12:14:05 +00:00
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
bio : pd.DataFrame
|
|
|
|
from enspreso_biomass_potentials()
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-08-16 12:14:05 +00:00
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
pd.DataFrame
|
|
|
|
"""
|
2021-07-11 15:52:32 +00:00
|
|
|
pop = build_nuts_population_data()
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
# get population in nuts2
|
|
|
|
pop_nuts2 = pop.loc[pop.index.str.len() == 4]
|
|
|
|
by_country = pop_nuts2.total.groupby(pop_nuts2.ct).sum()
|
|
|
|
pop_nuts2["fraction"] = pop_nuts2.total / pop_nuts2.ct.map(by_country)
|
|
|
|
|
|
|
|
# distribute nuts0 data to nuts2 by population
|
|
|
|
bio_nodal = bio.loc[pop_nuts2.ct]
|
|
|
|
bio_nodal.index = pop_nuts2.index
|
|
|
|
bio_nodal = bio_nodal.mul(pop_nuts2.fraction, axis=0)
|
|
|
|
|
|
|
|
# update inplace
|
|
|
|
bio.update(bio_nodal)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
return bio
|
|
|
|
|
|
|
|
|
|
|
|
def build_nuts2_shapes():
|
|
|
|
"""
|
|
|
|
- load NUTS2 geometries
|
|
|
|
- add RS, AL, BA country shapes (not covered in NUTS 2013)
|
|
|
|
- consistently name ME, MK
|
|
|
|
"""
|
2023-03-06 08:27:45 +00:00
|
|
|
nuts2 = gpd.GeoDataFrame(
|
|
|
|
gpd.read_file(snakemake.input.nuts2).set_index("id").geometry
|
|
|
|
)
|
2021-07-11 15:52:32 +00:00
|
|
|
|
2023-03-06 08:27:45 +00:00
|
|
|
countries = gpd.read_file(snakemake.input.country_shapes).set_index("name")
|
2022-04-11 15:08:25 +00:00
|
|
|
missing_iso2 = countries.index.intersection(["AL", "RS", "BA"])
|
|
|
|
missing = countries.loc[missing_iso2]
|
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
nuts2.rename(index={"ME00": "ME", "MK00": "MK"}, inplace=True)
|
|
|
|
|
2022-04-12 12:37:05 +00:00
|
|
|
return pd.concat([nuts2, missing])
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def area(gdf):
|
2023-03-06 08:27:45 +00:00
|
|
|
"""
|
|
|
|
Returns area of GeoDataFrame geometries in square kilometers.
|
|
|
|
"""
|
2021-07-11 15:52:32 +00:00
|
|
|
return gdf.to_crs(epsg=3035).area.div(1e6)
|
|
|
|
|
|
|
|
|
|
|
|
def convert_nuts2_to_regions(bio_nuts2, regions):
|
2021-08-16 12:14:05 +00:00
|
|
|
"""
|
2023-03-06 08:27:45 +00:00
|
|
|
Converts biomass potentials given in NUTS2 to PyPSA-Eur regions based on
|
|
|
|
the overlay of both GeoDataFrames in proportion to the area.
|
2021-08-16 12:14:05 +00:00
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
bio_nuts2 : gpd.GeoDataFrame
|
|
|
|
JRC ENSPRESO biomass potentials indexed by NUTS2 shapes.
|
|
|
|
regions : gpd.GeoDataFrame
|
|
|
|
PyPSA-Eur clustered onshore regions
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
gpd.GeoDataFrame
|
|
|
|
"""
|
2021-07-11 15:52:32 +00:00
|
|
|
# calculate area of nuts2 regions
|
|
|
|
bio_nuts2["area_nuts2"] = area(bio_nuts2)
|
|
|
|
|
2021-11-04 20:48:54 +00:00
|
|
|
overlay = gpd.overlay(regions, bio_nuts2, keep_geom_type=True)
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
# calculate share of nuts2 area inside region
|
|
|
|
overlay["share"] = area(overlay) / overlay["area_nuts2"]
|
|
|
|
|
|
|
|
# multiply all nuts2-level values with share of nuts2 inside region
|
2023-03-06 08:27:45 +00:00
|
|
|
adjust_cols = overlay.columns.difference(
|
|
|
|
{"name", "area_nuts2", "geometry", "share"}
|
|
|
|
)
|
2021-07-11 15:52:32 +00:00
|
|
|
overlay[adjust_cols] = overlay[adjust_cols].multiply(overlay["share"], axis=0)
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2023-04-20 18:37:27 +00:00
|
|
|
bio_regions = overlay.dissolve("name", aggfunc="sum")
|
2021-07-01 18:09:04 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
bio_regions.drop(["area_nuts2", "share"], axis=1, inplace=True)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
return bio_regions
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2023-03-06 08:27:45 +00:00
|
|
|
if "snakemake" not in globals():
|
2023-03-06 18:09:45 +00:00
|
|
|
from _helpers import mock_snakemake
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2023-03-06 08:27:45 +00:00
|
|
|
snakemake = mock_snakemake("build_biomass_potentials", simpl="", clusters="5")
|
|
|
|
|
2023-05-17 17:25:45 +00:00
|
|
|
params = snakemake.params["biomass"]
|
|
|
|
year = params["year"]
|
|
|
|
scenario = params["scenario"]
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
enspreso = enspreso_biomass_potentials(year, scenario)
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
enspreso = disaggregate_nuts0(enspreso)
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
nuts2 = build_nuts2_shapes()
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
df_nuts2 = gpd.GeoDataFrame(nuts2.geometry).join(enspreso)
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
regions = gpd.read_file(snakemake.input.regions_onshore)
|
2020-09-21 16:35:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
df = convert_nuts2_to_regions(df_nuts2, regions)
|
2020-09-21 16:35:45 +00:00
|
|
|
|
2021-07-11 15:52:32 +00:00
|
|
|
df.to_csv(snakemake.output.biomass_potentials_all)
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2023-05-17 17:25:45 +00:00
|
|
|
grouper = {v: k for k, vv in params["classes"].items() for v in vv}
|
2021-07-01 18:09:04 +00:00
|
|
|
df = df.groupby(grouper, axis=1).sum()
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2023-03-06 08:27:45 +00:00
|
|
|
df *= 1e6 # TWh/a to MWh/a
|
2021-07-01 18:09:04 +00:00
|
|
|
df.index.name = "MWh/a"
|
2019-12-04 15:38:19 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
df.to_csv(snakemake.output.biomass_potentials)
|