Merge pull request #89 from martavp/distribute_CO2budget

Distribute CO2 budget among the planning horizons for the myopic option
This commit is contained in:
Tom Brown 2021-01-18 18:17:15 +01:00 committed by GitHub
commit b133cd6656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 321 additions and 47 deletions

View File

@ -8,7 +8,7 @@ wildcard_constraints:
clusters="[0-9]+m?", clusters="[0-9]+m?",
sectors="[+a-zA-Z0-9]+", sectors="[+a-zA-Z0-9]+",
opts="[-+a-zA-Z0-9]*", opts="[-+a-zA-Z0-9]*",
sector_opts="[-+a-zA-Z0-9]*" sector_opts="[-+a-zA-Z0-9\.]*"
@ -292,6 +292,7 @@ rule build_retro_cost:
output: output:
retro_cost="resources/retro_cost_{network}_s{simpl}_{clusters}.csv", retro_cost="resources/retro_cost_{network}_s{simpl}_{clusters}.csv",
floor_area="resources/floor_area_{network}_s{simpl}_{clusters}.csv" floor_area="resources/floor_area_{network}_s{simpl}_{clusters}.csv"
resources: mem_mb=1000
script: "scripts/build_retro_cost.py" script: "scripts/build_retro_cost.py"

View File

@ -24,11 +24,17 @@ scenario:
# B for biomass supply, I for industry, shipping and aviation # B for biomass supply, I for industry, shipping and aviation
# solarx or onwindx changes the available installable potential by factor x # solarx or onwindx changes the available installable potential by factor x
# dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv # dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv
# for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative
# emissions throughout the transition path in the timeframe determined by the
# planning_horizons), be:beta decay; ex:exponential decay
# cb40ex0 distributes a carbon budget of 40 GtCO2 following an exponential
# decay with initial growth rate 0
planning_horizons : [2030] # investment years for myopic and perfect; or costs year for overnight planning_horizons : [2030] # investment years for myopic and perfect; or costs year for overnight
# for example, set to [2020, 2030, 2040, 2050] for myopic foresight # for example, set to [2020, 2030, 2040, 2050] for myopic foresight
# CO2 budget as a fraction of 1990 emissions # CO2 budget as a fraction of 1990 emissions
# this is over-ridden if CO2Lx is set in sector_opts # this is over-ridden if CO2Lx is set in sector_opts
# this is also over-ridden if cb is set in sector_opts
co2_budget: co2_budget:
2020: 0.7011648746 2020: 0.7011648746
2025: 0.5241935484 2025: 0.5241935484

View File

@ -16,7 +16,7 @@ its dependencies. Clone the repository:
.. code:: bash .. code:: bash
projects % git clone git@github.com:PyPSA/pypsa-eur.git projects % git clone https://github.com/PyPSA/pypsa-eur.git
then download and unpack all the PyPSA-Eur data files by running the following snakemake rule: then download and unpack all the PyPSA-Eur data files by running the following snakemake rule:
@ -32,7 +32,7 @@ Next install the technology assumptions database `technology-data <https://githu
.. code:: bash .. code:: bash
projects % git clone git@github.com:PyPSA/technology-data.git projects % git clone https://github.com/PyPSA/technology-data.git
Clone PyPSA-Eur-Sec repository Clone PyPSA-Eur-Sec repository
@ -42,7 +42,7 @@ Create a parallel directory for `PyPSA-Eur-Sec <https://github.com/PyPSA/pypsa-e
.. code:: bash .. code:: bash
projects % git clone git@github.com:PyPSA/pypsa-eur-sec.git projects % git clone https://github.com/PyPSA/pypsa-eur-sec.git
Environment/package requirements Environment/package requirements
================================ ================================
@ -54,6 +54,13 @@ The requirements are the same as `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>
xarray version >= 0.15.1, you will need the latest master branch of xarray version >= 0.15.1, you will need the latest master branch of
atlite version 0.0.2. atlite version 0.0.2.
You can create an enviroment using the environment.yaml file in pypsa-eur/envs:
.../pypsa-eur % conda env create -f envs/environment.yaml
.../pypsa-eur % conda activate pypsa-eur
See details in `PyPSA-Eur Installation <https://pypsa-eur.readthedocs.io/en/latest/installation.html>`_
Data requirements Data requirements
================= =================

View File

@ -6,7 +6,7 @@ Myopic transition path
The myopic code can be used to investigate progressive changes in a network, for instance, those taking place throughout a transition path. The capacities installed in a certain time step are maintained in the network until their operational lifetime expires. The myopic code can be used to investigate progressive changes in a network, for instance, those taking place throughout a transition path. The capacities installed in a certain time step are maintained in the network until their operational lifetime expires.
The myopic approach was initially developed and used in the paper `Early decarbonisation of the European Energy system pays off (2020) <https://arxiv.org/abs/2004.11009>`__ but the current implementation complies with the pypsa-eur-sec standard working flow and is compatible with using the higher resolution electricity transmission model `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`__ rather than a one-node-per-country model. The myopic approach was initially developed and used in the paper `Early decarbonisation of the European Energy system pays off (2020) <https://www.nature.com/articles/s41467-020-20015-4>`__ but the current implementation complies with the pypsa-eur-sec standard working flow and is compatible with using the higher resolution electricity transmission model `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`__ rather than a one-node-per-country model.
The current code applies the myopic approach to generators, storage technologies and links in the power sector and the space and water heating sector. The current code applies the myopic approach to generators, storage technologies and links in the power sector and the space and water heating sector.
@ -61,12 +61,15 @@ Wildcards
The {planning_horizons} wildcard indicates the timesteps in which the network is optimized, e.g. planning_horizons: [2020, 2030, 2040, 2050] The {planning_horizons} wildcard indicates the timesteps in which the network is optimized, e.g. planning_horizons: [2020, 2030, 2040, 2050]
Options
=============
The total carbon budget for the entire transition path can be indicated in the ``scenario.sector_opts`` in ``config.yaml``.
The carbon budget can be split among the ``planning_horizons`` following an exponential or beta decay.
E.g. ``'cb40ex0'`` splits the a carbon budget equal to 40 GtCO_2 following an exponential decay whose initial linear growth rate $r$ is zero
**{co2_budget_name} wildcard** $e(t) = e_0 (1+ (r+m)t) e^(-mt)$
The {co2_budget_name} wildcard indicates the name of the co2 budget. See details in Supplementary Note 1 of the paper `Early decarbonisation of the European Energy system pays off (2020) <https://www.nature.com/articles/s41467-020-20015-4>`__
A csv file is used as input including the planning_horizons as index, the name of co2_budget as column name, and the maximum co2 emissions (relative to 1990) as values.
Rules overview Rules overview
================= =================
@ -74,17 +77,17 @@ Rules overview
General myopic code structure General myopic code structure
=============================== ===============================
The myopic code solves the network for the time steps included in planning_horizons in a recursive loop, so that: The myopic code solves the network for the time steps included in ``planning_horizons`` in a recursive loop, so that:
1.The existing capacities (those installed before the base year are added as fixed capacities with p_nom=value, p_nom_extendable=False). E.g. for baseyear=2020, capacities installed before 2020 are added. In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``. 1.The existing capacities (those installed before the base year are added as fixed capacities with p_nom=value, p_nom_extendable=False). E.g. for baseyear=2020, capacities installed before 2020 are added. In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``.
The base year is the first element in planning_horizons. Step 1 is implemented with the rule add_baseyear for the base year and with the rule add_brownfield for the remaining planning_horizons. The base year is the first element in ``planning_horizons``. Step 1 is implemented with the rule add_baseyear for the base year and with the rule add_brownfield for the remaining planning_horizons.
2.The 2020 network is optimized. The solved network is saved in results/run_name/networks/postnetworks 2.The 2020 network is optimized. The solved network is saved in ``results/run_name/networks/postnetworks``
3.For the next planning horizon, e.g. 2030, the capacities from a previous time step are added if they are still in operation (i.e., if they fulfil planning horizon <= commissioned year + lifetime). In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``. 3.For the next planning horizon, e.g. 2030, the capacities from a previous time step are added if they are still in operation (i.e., if they fulfil planning horizon <= commissioned year + lifetime). In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``.
Steps 2 and 3 are solved recursively for all the planning_horizons included in the configuration file. Steps 2 and 3 are solved recursively for all the planning_horizons included in ``config.yaml``.
rule add_existing baseyear rule add_existing baseyear
@ -110,8 +113,8 @@ Then, the resulting network is saved in ``results/run_name/networks/prenetworks-
rule add_brownfield rule add_brownfield
=================== ===================
The rule add_brownfield loads the network in results/run_name/networks/prenetworks and performs the following operation: The rule add_brownfield loads the network in ``results/run_name/networks/prenetworks`` and performs the following operation:
1.Read the capacities optimized in the previous time step and add them to the network if they are still in operation (i.e., if they fulfil planning horizon < commissioned year + lifetime) 1.Read the capacities optimized in the previous time step and add them to the network if they are still in operation (i.e., if they fulfill planning horizon < commissioned year + lifetime)
Then, the resulting network is saved in ``results/run_name/networks/prenetworks_brownfield``. Then, the resulting network is saved in ``results/run_name/networks/prenetworks_brownfield``.

View File

@ -2,6 +2,9 @@
Release Notes Release Notes
########################################## ##########################################
Future release
===================
*For the myopic option, a carbon budget and a type of decay (exponential or beta) can be selected in the config file to distribute the budget across the planning_horizons.
PyPSA-Eur-Sec 0.4.0 (11th December 2020) PyPSA-Eur-Sec 0.4.0 (11th December 2020)
========================================= =========================================

View File

@ -106,7 +106,7 @@ Thermal energy storage using hot water tanks
Small for decentral applications. Small for decentral applications.
Big pit storage for district heating. Big water pit storage for district heating.
Hydrogen demand Hydrogen demand
@ -122,7 +122,7 @@ Industry (ammonia, precursor to hydrocarbons for chemicals and iron/steel).
Hydrogen supply Hydrogen supply
================= =================
SMR, SMR+CCS, electrolysers. Steam Methane Reforming (SMR), SMR+CCS, electrolysers.
Methane demand Methane demand

View File

@ -390,12 +390,12 @@ def build_energy_totals():
return clean_df return clean_df
def build_eea_co2(): def build_eea_co2(year=1990):
# see ../notebooks/compute_1990_Europe_emissions_for_targets.ipynb # see ../notebooks/compute_1990_Europe_emissions_for_targets.ipynb
#https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-14 #https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
#downloaded 190222 (modified by EEA last on 181130) #downloaded 201228 (modified by EEA last on 201221)
fn = "data/eea/UNFCCC_v21.csv" fn = "data/eea/UNFCCC_v23.csv"
df = pd.read_csv(fn, encoding="latin-1") df = pd.read_csv(fn, encoding="latin-1")
df.loc[df["Year"] == "1985-1987","Year"] = 1986 df.loc[df["Year"] == "1985-1987","Year"] = 1986
df["Year"] = df["Year"].astype(int) df["Year"] = df["Year"].astype(int)
@ -418,16 +418,14 @@ def build_eea_co2():
e['waste management'] = '5 - Waste management' e['waste management'] = '5 - Waste management'
e['other'] = '6 - Other Sector' e['other'] = '6 - Other Sector'
e['indirect'] = 'ind_CO2 - Indirect CO2' e['indirect'] = 'ind_CO2 - Indirect CO2'
e["total wL"] = "Total (with LULUCF, with indirect CO2)" e["total wL"] = "Total (with LULUCF)"
e["total woL"] = "Total (without LULUCF, with indirect CO2)" e["total woL"] = "Total (without LULUCF)"
pol = "CO2" #["All greenhouse gases - (CO2 equivalent)","CO2"] pol = "CO2" #["All greenhouse gases - (CO2 equivalent)","CO2"]
cts = ["CH","EUA","NO"] + eu28_eea cts = ["CH","EUA","NO"] + eu28_eea
year = 1990
emissions = df.loc[idx[cts,pol,year,e.values],"emissions"].unstack("Sector_name").rename(columns=pd.Series(e.index,e.values)).rename(index={"All greenhouse gases - (CO2 equivalent)" : "GHG"},level=1) emissions = df.loc[idx[cts,pol,year,e.values],"emissions"].unstack("Sector_name").rename(columns=pd.Series(e.index,e.values)).rename(index={"All greenhouse gases - (CO2 equivalent)" : "GHG"},level=1)
#only take level 0, since level 1 (pol) and level 2 (year) are trivial #only take level 0, since level 1 (pol) and level 2 (year) are trivial
@ -467,7 +465,7 @@ def build_eurostat_co2(year=1990):
return eurostat_co2 return eurostat_co2
def build_co2_totals(year=1990): def build_co2_totals(eea_co2, eurostat_co2, year=1990):
co2 = eea_co2.reindex(["EU28","NO","CH","BA","RS","AL","ME","MK"] + eu28) co2 = eea_co2.reindex(["EU28","NO","CH","BA","RS","AL","ME","MK"] + eu28)
@ -486,10 +484,6 @@ def build_co2_totals(year=1990):
#doesn't include non-energy emissions #doesn't include non-energy emissions
co2.loc[ct,'agriculture'] = eurostat_co2[ct,"+","+","Agriculture / Forestry"].sum() co2.loc[ct,'agriculture'] = eurostat_co2[ct,"+","+","Agriculture / Forestry"].sum()
co2.to_csv(snakemake.output.co2_name)
return co2 return co2
@ -547,7 +541,7 @@ if __name__ == "__main__":
snakemake.output['transport_name'] = "data/transport_data.csv" snakemake.output['transport_name'] = "data/transport_data.csv"
snakemake.input = Dict() snakemake.input = Dict()
snakemake.input['nuts3_shapes'] = 'resources/nuts3_shapes.geojson' snakemake.input['nuts3_shapes'] = '../pypsa-eur/resources/nuts3_shapes.geojson'
nuts3 = gpd.read_file(snakemake.input.nuts3_shapes).set_index('index') nuts3 = gpd.read_file(snakemake.input.nuts3_shapes).set_index('index')
population = nuts3['pop'].groupby(nuts3.country).sum() population = nuts3['pop'].groupby(nuts3.country).sum()
@ -566,6 +560,7 @@ if __name__ == "__main__":
eurostat_co2 = build_eurostat_co2() eurostat_co2 = build_eurostat_co2()
build_co2_totals() co2=build_co2_totals(eea_co2, eurostat_co2, year)
co2.to_csv(snakemake.output.co2_name)
build_transport_data() build_transport_data()

View File

@ -196,6 +196,24 @@ def calculate_costs(n,label,costs):
return costs return costs
def calculate_cumulative_cost():
planning_horizons = snakemake.config['scenario']['planning_horizons']
cumulative_cost = pd.DataFrame(index = df["costs"].sum().index,
columns=pd.Series(data=np.arange(0,0.1, 0.01), name='social discount rate'))
#discount cost and express them in money value of planning_horizons[0]
for r in cumulative_cost.columns:
cumulative_cost[r]=[df["costs"].sum()[index]/((1+r)**(index[-1]-planning_horizons[0])) for index in cumulative_cost.index]
#integrate cost throughout the transition path
for r in cumulative_cost.columns:
for cluster in cumulative_cost.index.get_level_values(level=0).unique():
for lv in cumulative_cost.index.get_level_values(level=1).unique():
for sector_opts in cumulative_cost.index.get_level_values(level=2).unique():
cumulative_cost.loc[(cluster, lv, sector_opts,'cumulative cost'),r] = np.trapz(cumulative_cost.loc[idx[cluster, lv, sector_opts,planning_horizons],r].values, x=planning_horizons)
return cumulative_cost
def calculate_nodal_capacities(n,label,nodal_capacities): def calculate_nodal_capacities(n,label,nodal_capacities):
#Beware this also has extraneous locations for country (e.g. biomass) or continent-wide (e.g. fossil gas/oil) stuff #Beware this also has extraneous locations for country (e.g. biomass) or continent-wide (e.g. fossil gas/oil) stuff
@ -564,16 +582,17 @@ if __name__ == "__main__":
snakemake.config = yaml.safe_load(f) snakemake.config = yaml.safe_load(f)
#overwrite some options #overwrite some options
snakemake.config["run"] = "test" snakemake.config["run"] = "version-8"
snakemake.config["scenario"]["lv"] = [1.0] snakemake.config["scenario"]["lv"] = [1.0]
snakemake.config["scenario"]["sector_opts"] = ["Co2L0-168H-T-H-B-I-solar3-dist1"] snakemake.config["scenario"]["sector_opts"] = ["3H-T-H-B-I-solar3-dist1"]
snakemake.config["planning_horizons"] = ['2020', '2030', '2040', '2050'] snakemake.config["planning_horizons"] = ['2020', '2030', '2040', '2050']
snakemake.input = Dict() snakemake.input = Dict()
snakemake.input['heat_demand_name'] = 'data/heating/daily_heat_demand.h5' snakemake.input['heat_demand_name'] = 'data/heating/daily_heat_demand.h5'
snakemake.input['costs'] = snakemake.config['costs_dir'] + "costs_{}.csv".format(snakemake.config['scenario']['planning_horizons'][0])
snakemake.output = Dict() snakemake.output = Dict()
for item in outputs: for item in outputs:
snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item) snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output['cumulative_cost'] = snakemake.config['summary_dir'] + '/{name}/csvs/cumulative_cost.csv'.format(name=snakemake.config['run'])
networks_dict = {(cluster, lv, opt+sector_opt, planning_horizon) : networks_dict = {(cluster, lv, opt+sector_opt, planning_horizon) :
snakemake.config['results_dir'] + snakemake.config['run'] + '/postnetworks/elec_s{simpl}_{cluster}_lv{lv}_{opt}_{sector_opt}_{planning_horizon}.nc'\ snakemake.config['results_dir'] + snakemake.config['run'] + '/postnetworks/elec_s{simpl}_{cluster}_lv{lv}_{opt}_{sector_opt}_{planning_horizon}.nc'\
.format(simpl=simpl, .format(simpl=simpl,
@ -592,6 +611,7 @@ if __name__ == "__main__":
print(networks_dict) print(networks_dict)
Nyears = 1 Nyears = 1
costs_db = prepare_costs(snakemake.input.costs, costs_db = prepare_costs(snakemake.input.costs,
snakemake.config['costs']['USD2013_to_EUR2013'], snakemake.config['costs']['USD2013_to_EUR2013'],
snakemake.config['costs']['discountrate'], snakemake.config['costs']['discountrate'],
@ -603,3 +623,10 @@ if __name__ == "__main__":
df["metrics"].loc["total costs"] = df["costs"].sum() df["metrics"].loc["total costs"] = df["costs"].sum()
to_csv(df) to_csv(df)
if snakemake.config["foresight"]=='myopic':
cumulative_cost=calculate_cumulative_cost()
cumulative_cost.to_csv(snakemake.config['summary_dir'] + '/' + snakemake.config['run'] + '/csvs/cumulative_cost.csv')

View File

@ -1,6 +1,6 @@
import numpy as np
import pandas as pd import pandas as pd
#allow plotting without Xwindows #allow plotting without Xwindows
@ -9,7 +9,7 @@ matplotlib.use('Agg')
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from prepare_sector_network import co2_emissions_year
#consolidate and rename #consolidate and rename
def rename_techs(label): def rename_techs(label):
@ -237,7 +237,137 @@ def plot_balances():
fig.savefig(snakemake.output.balances[:-10] + k + ".pdf",transparent=True) fig.savefig(snakemake.output.balances[:-10] + k + ".pdf",transparent=True)
def historical_emissions(cts):
"""
read historical emissions to add them to the carbon budget plot
"""
#https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
#downloaded 201228 (modified by EEA last on 201221)
fn = "data/eea/UNFCCC_v23.csv"
df = pd.read_csv(fn, encoding="latin-1")
df.loc[df["Year"] == "1985-1987","Year"] = 1986
df["Year"] = df["Year"].astype(int)
df = df.set_index(['Year', 'Sector_name', 'Country_code', 'Pollutant_name']).sort_index()
e = pd.Series()
e["electricity"] = '1.A.1.a - Public Electricity and Heat Production'
e['residential non-elec'] = '1.A.4.b - Residential'
e['services non-elec'] = '1.A.4.a - Commercial/Institutional'
e['rail non-elec'] = "1.A.3.c - Railways"
e["road non-elec"] = '1.A.3.b - Road Transportation'
e["domestic navigation"] = "1.A.3.d - Domestic Navigation"
e['international navigation'] = '1.D.1.b - International Navigation'
e["domestic aviation"] = '1.A.3.a - Domestic Aviation'
e["international aviation"] = '1.D.1.a - International Aviation'
e['total energy'] = '1 - Energy'
e['industrial processes'] = '2 - Industrial Processes and Product Use'
e['agriculture'] = '3 - Agriculture'
e['LULUCF'] = '4 - Land Use, Land-Use Change and Forestry'
e['waste management'] = '5 - Waste management'
e['other'] = '6 - Other Sector'
e['indirect'] = 'ind_CO2 - Indirect CO2'
e["total wL"] = "Total (with LULUCF)"
e["total woL"] = "Total (without LULUCF)"
pol = ["CO2"] # ["All greenhouse gases - (CO2 equivalent)"]
cts
if "GB" in cts:
cts.remove("GB")
cts.append("UK")
year = np.arange(1990,2018).tolist()
idx = pd.IndexSlice
co2_totals = df.loc[idx[year,e.values,cts,pol],"emissions"].unstack("Year").rename(index=pd.Series(e.index,e.values))
co2_totals = (1/1e6)*co2_totals.groupby(level=0, axis=0).sum() #Gton CO2
co2_totals.loc['industrial non-elec'] = co2_totals.loc['total energy'] - co2_totals.loc[['electricity', 'services non-elec','residential non-elec', 'road non-elec',
'rail non-elec', 'domestic aviation', 'international aviation', 'domestic navigation',
'international navigation']].sum()
emissions = co2_totals.loc["electricity"]
if "T" in opts:
emissions += co2_totals.loc[[i+ " non-elec" for i in ["rail","road"]]].sum()
if "H" in opts:
emissions += co2_totals.loc[[i+ " non-elec" for i in ["residential","services"]]].sum()
if "I" in opts:
emissions += co2_totals.loc[["industrial non-elec","industrial processes",
"domestic aviation","international aviation",
"domestic navigation","international navigation"]].sum()
return emissions
def plot_carbon_budget_distribution():
"""
Plot historical carbon emissions in the EU and decarbonization path
"""
import matplotlib.gridspec as gridspec
import seaborn as sns; sns.set()
sns.set_style('ticks')
plt.style.use('seaborn-ticks')
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.labelsize'] = 20
plt.rcParams['ytick.labelsize'] = 20
plt.figure(figsize=(10, 7))
gs1 = gridspec.GridSpec(1, 1)
ax1 = plt.subplot(gs1[0,0])
ax1.set_ylabel('CO$_2$ emissions (Gt per year)',fontsize=22)
ax1.set_ylim([0,5])
ax1.set_xlim([1990,snakemake.config['scenario']['planning_horizons'][-1]+1])
path_cb = snakemake.config['results_dir'] + snakemake.config['run'] + '/csvs/'
countries=pd.read_csv(path_cb + 'countries.csv', index_col=1)
cts=countries.index.to_list()
e_1990 = co2_emissions_year(cts, opts, year=1990)
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv',
index_col=0)
ax1.plot(e_1990*CO2_CAP[o],linewidth=3,
color='dodgerblue', label=None)
emissions = historical_emissions(cts)
ax1.plot(emissions, color='black', linewidth=3, label=None)
#plot commited and uder-discussion targets
#(notice that historical emissions include all countries in the
# network, but targets refer to EU)
ax1.plot([2020],[0.8*emissions[1990]],
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black')
ax1.plot([2030],[0.45*emissions[1990]],
marker='*', markersize=12, markerfacecolor='white',
markeredgecolor='black')
ax1.plot([2030],[0.6*emissions[1990]],
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black')
ax1.plot([2050, 2050],[x*emissions[1990] for x in [0.2, 0.05]],
color='gray', linewidth=2, marker='_', alpha=0.5)
ax1.plot([2050],[0.01*emissions[1990]],
marker='*', markersize=12, markerfacecolor='white',
linewidth=0, markeredgecolor='black',
label='EU under-discussion target', zorder=10,
clip_on=False)
ax1.plot([2050],[0.125*emissions[1990]],'ro',
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black', label='EU commited target')
ax1.legend(fancybox=True, fontsize=18, loc=(0.01,0.01),
facecolor='white', frameon=True)
path_cb_plot = snakemake.config['results_dir'] + snakemake.config['run'] + '/graphs/'
plt.savefig(path_cb_plot+'carbon_budget_plot.pdf', dpi=300)
if __name__ == "__main__": if __name__ == "__main__":
# Detect running outside of snakemake and mock snakemake for testing # Detect running outside of snakemake and mock snakemake for testing
@ -249,12 +379,15 @@ if __name__ == "__main__":
snakemake.config = yaml.safe_load(f) snakemake.config = yaml.safe_load(f)
snakemake.input = Dict() snakemake.input = Dict()
snakemake.output = Dict() snakemake.output = Dict()
snakemake.wildcards = Dict()
#snakemake.wildcards['sector_opts']='3H-T-H-B-I-solar3-dist1-cb48be3'
for item in ["costs", "energy"]: for item in ["costs", "energy"]:
snakemake.input[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item) snakemake.input[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/graphs/{item}.pdf'.format(name=snakemake.config['run'],item=item) snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/graphs/{item}.pdf'.format(name=snakemake.config['run'],item=item)
snakemake.input["balances"] = snakemake.config['summary_dir'] + '/test/csvs/supply_energy.csv' snakemake.input["balances"] = snakemake.config['summary_dir'] + '/{name}/csvs/supply_energy.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output["balances"] = snakemake.config['summary_dir'] + '/test/graphs/balances-energy.csv' snakemake.output["balances"] = snakemake.config['summary_dir'] + '/{name}/graphs/balances-energy.csv'.format(name=snakemake.config['run'],item=item)
n_header = 4 n_header = 4
@ -263,3 +396,9 @@ if __name__ == "__main__":
plot_energy() plot_energy()
plot_balances() plot_balances()
for sector_opts in snakemake.config['scenario']['sector_opts']:
opts=sector_opts.split('-')
for o in opts:
if "cb" in o:
plot_carbon_budget_distribution()

View File

@ -20,6 +20,8 @@ import pytz
from vresutils.costdata import annuity from vresutils.costdata import annuity
from scipy.stats import beta
from build_energy_totals import build_eea_co2, build_eurostat_co2, build_co2_totals
#First tell PyPSA that links can have multiple outputs by #First tell PyPSA that links can have multiple outputs by
#overriding the component_attrs. This can be done for #overriding the component_attrs. This can be done for
@ -45,6 +47,83 @@ override_component_attrs["Generator"].loc["lifetime"] = ["float","years",np.nan,
override_component_attrs["Store"].loc["build_year"] = ["integer","year",np.nan,"build year","Input (optional)"] override_component_attrs["Store"].loc["build_year"] = ["integer","year",np.nan,"build year","Input (optional)"]
override_component_attrs["Store"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"] override_component_attrs["Store"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"]
def co2_emissions_year(cts, opts, year):
"""
calculate co2 emissions in one specific year (e.g. 1990 or 2018).
"""
eea_co2 = build_eea_co2(year)
#TODO: read Eurostat data from year>2014, this only affects the estimation of
# CO2 emissions for "BA","RS","AL","ME","MK"
if year > 2014:
eurostat_co2 = build_eurostat_co2(year=2014)
else:
eurostat_co2 = build_eurostat_co2(year)
co2_totals=build_co2_totals(eea_co2, eurostat_co2, year)
co2_emissions = co2_totals.loc[cts, "electricity"].sum()
if "T" in opts:
co2_emissions += co2_totals.loc[cts, [i+ " non-elec" for i in ["rail","road"]]].sum().sum()
if "H" in opts:
co2_emissions += co2_totals.loc[cts, [i+ " non-elec" for i in ["residential","services"]]].sum().sum()
if "I" in opts:
co2_emissions += co2_totals.loc[cts, ["industrial non-elec","industrial processes",
"domestic aviation","international aviation",
"domestic navigation","international navigation"]].sum().sum()
co2_emissions *=0.001 #MtCO2 to GtCO2
return co2_emissions
def build_carbon_budget(o):
#distribute carbon budget following beta or exponential transition path
if "be" in o:
#beta decay
carbon_budget = float(o[o.find("cb")+2:o.find("be")])
be=float(o[o.find("be")+2:])
if "ex" in o:
#exponential decay
carbon_budget = float(o[o.find("cb")+2:o.find("ex")])
r=float(o[o.find("ex")+2:])
pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0)
pop_layout["ct"] = pop_layout.index.str[:2]
cts = pop_layout.ct.value_counts().index
e_1990 = co2_emissions_year(cts, opts, year=1990)
#emissions at the beginning of the path (last year available 2018)
e_0 = co2_emissions_year(cts, opts, year=2018)
#emissions in 2019 and 2020 assumed equal to 2018 and substracted
carbon_budget -= 2*e_0
planning_horizons = snakemake.config['scenario']['planning_horizons']
CO2_CAP = pd.DataFrame(index = pd.Series(data=planning_horizons,
name='planning_horizon'),
columns=pd.Series(data=[],
name='paths',
dtype='float'))
t_0 = planning_horizons[0]
if "be" in o:
#beta decay
t_f = t_0 + (2*carbon_budget/e_0).round(0) # final year in the path
#emissions (relative to 1990)
CO2_CAP[o] = [(e_0/e_1990)*(1-beta.cdf((t-t_0)/(t_f-t_0), be, be)) for t in planning_horizons]
if "ex" in o:
#exponential decay without delay
T=carbon_budget/e_0
m=(1+np.sqrt(1+r*T))/T
CO2_CAP[o] = [(e_0/e_1990)*(1+(m+r)*(t-t_0))*np.exp(-m*(t-t_0)) for t in planning_horizons]
CO2_CAP.to_csv(path_cb + 'carbon_budget_distribution.csv', sep=',',
line_terminator='\n', float_format='%.3f')
countries=pd.Series(data=cts)
countries.to_csv(path_cb + 'countries.csv', sep=',',
line_terminator='\n', float_format='%.3f')
def add_lifetime_wind_solar(n): def add_lifetime_wind_solar(n):
""" """
Add lifetime for solar and wind generators Add lifetime for solar and wind generators
@ -1775,15 +1854,15 @@ def get_parameter(item):
return item return item
#%%
if __name__ == "__main__": if __name__ == "__main__":
# Detect running outside of snakemake and mock snakemake for testing # Detect running outside of snakemake and mock snakemake for testing
if 'snakemake' not in globals(): if 'snakemake' not in globals():
from vresutils.snakemake import MockSnakemake from vresutils.snakemake import MockSnakemake
snakemake = MockSnakemake( snakemake = MockSnakemake(
wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0',
opts='', planning_horizons='2030', co2_budget_name="go", opts='', planning_horizons='2020',
sector_opts='Co2L0-120H-T-H-B-I-solar3-dist1'), sector_opts='120H-T-H-B-I-solar3-dist1-cb48be3'),
input=dict( network='../pypsa-eur/networks/{network}_s{simpl}_{clusters}_ec_lv{lv}_{opts}.nc', input=dict( network='../pypsa-eur/networks/{network}_s{simpl}_{clusters}_ec_lv{lv}_{opts}.nc',
energy_totals_name='resources/energy_totals.csv', energy_totals_name='resources/energy_totals.csv',
co2_totals_name='resources/co2_totals.csv', co2_totals_name='resources/co2_totals.csv',
@ -1819,10 +1898,10 @@ if __name__ == "__main__":
solar_thermal_total="resources/solar_thermal_total_{network}_s{simpl}_{clusters}.nc", solar_thermal_total="resources/solar_thermal_total_{network}_s{simpl}_{clusters}.nc",
solar_thermal_urban="resources/solar_thermal_urban_{network}_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_{network}_s{simpl}_{clusters}.nc",
solar_thermal_rural="resources/solar_thermal_rural_{network}_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_{network}_s{simpl}_{clusters}.nc",
retro_cost_energy = "resources/retro_cost_{network}_s{simpl}_{clusters}.csv", retro_cost_energy = "resources/retro_cost_{network}_s{simpl}_{clusters}.csv",
floor_area = "resources/floor_area_{network}_s{simpl}_{clusters}.csv" floor_area = "resources/floor_area_{network}_s{simpl}_{clusters}.csv"
), ),
output=['pypsa-eur-sec/results/test/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{co2_budget_name}_{planning_horizons}.nc'] output=['results/version-cb48be3/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc']
) )
import yaml import yaml
with open('config.yaml', encoding='utf8') as f: with open('config.yaml', encoding='utf8') as f:
@ -1926,6 +2005,20 @@ if __name__ == "__main__":
limit = get_parameter(snakemake.config["co2_budget"]) limit = get_parameter(snakemake.config["co2_budget"])
print("CO2 limit set to",limit) print("CO2 limit set to",limit)
for o in opts:
if "cb" in o:
path_cb = snakemake.config['results_dir'] + snakemake.config['run'] + '/csvs/'
if not os.path.exists(path_cb):
os.makedirs(path_cb)
try:
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv', index_col=0)
except:
build_carbon_budget(o)
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv', index_col=0)
limit=CO2_CAP.loc[investment_year]
print("overriding CO2 limit with scenario limit",limit)
for o in opts: for o in opts:
if "Co2L" in o: if "Co2L" in o:
limit = o[o.find("Co2L")+4:] limit = o[o.find("Co2L")+4:]