diff --git a/README.md b/README.md index ac1a9921..0ec3de71 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ the energy system and includes all greenhouse gas emitters except waste management, agriculture, forestry and land use. Please see the [documentation](https://pypsa-eur-sec.readthedocs.io/) -for installation instructions and other useful information. +for installation instructions and other useful information about the snakemake workflow. This diagram gives an overview of the sectors and the links between them: diff --git a/config.default.yaml b/config.default.yaml index 946977f9..0ad40df8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -15,19 +15,21 @@ scenario: lv: [1.0,1.5] # allowed transmission line volume expansion, can be any float >= 1.0 (today) or "opt" clusters: [45,50] # number of nodes in Europe, any integer between 37 (1 node per country-zone) and several hundred opts: [''] # only relevant for PyPSA-Eur - sector_opts: [Co2L0-3H-T-H-B-I-solar3-dist1] # this is where the main scenario settings are + sector_opts: [Co2L0-3H-T-H-B-I-solar+p3-dist1] # this is where the main scenario settings are # to really understand the options here, look in scripts/prepare_sector_network.py # Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%); # Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions # xH is the temporal resolution; 3H is 3-hourly, i.e. one snapshot every 3 hours # single letters are sectors: T for land transport, H for building heating, # B for biomass supply, I for industry, shipping and aviation - # 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 # solar+c0.5 reduces the capital cost of solar to 50\% of reference value - # for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative - # emissions throughout the transition path in the timeframe determined by the + # solar+p3 multiplies the available installable potential by factor 3 + # 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 # for example, set to [2020, 2030, 2040, 2050] for myopic foresight @@ -69,6 +71,7 @@ pypsa_eur: "Link": ["DC"] "Generator": ["onwind", "offwind-ac", "offwind-dc", "solar", "ror"] "StorageUnit": ["PHS","hydro"] + "Store": [] biomass: year: 2030 @@ -99,7 +102,7 @@ sector: 'bev_dsm' : True #turns on EV battery 'bev_availability' : 0.5 #How many cars do smart charging 'v2g' : True #allows feed-in to grid from EV battery - #what is not EV or FCEV is fossil-fuelled + #what is not EV or FCEV is oil-fuelled ICE 'land_transport_fuel_cell_share': # 1 means all FCEVs 2020: 0 2030: 0.05 @@ -331,7 +334,7 @@ plotting: "Fischer-Tropsch" : "#44DD33" "kerosene for aviation": "#44BB11" "naphtha for industry" : "#44FF55" - "land transport fossil" : "#44DD33" + "land transport oil" : "#44DD33" "water tanks" : "#BBBBBB" "hot water storage" : "#BBBBBB" "hot water charging" : "#BBBBBB" @@ -366,6 +369,7 @@ plotting: "process emissions to stored" : "#444444" "process emissions to atmosphere" : "#888888" "process emissions" : "#222222" + "oil emissions" : "#666666" "land transport fuel cell" : "#AAAAAA" "biogas" : "#800000" "solid biomass" : "#DAA520" diff --git a/doc/data.csv b/doc/data.csv index c8cb4b1f..e8c19518 100644 --- a/doc/data.csv +++ b/doc/data.csv @@ -2,7 +2,7 @@ description,file/folder,licence,source JRC IDEES database,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees urban/rural fraction,urban_percent.csv,unknown,unknown JRC biomass potentials,biomass/,unknown,https://doi.org/10.2790/39014 -EEA emission statistics,eea/,unknown,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-14 +EEA emission statistics,eea/UNFCCC_v23.csv,EEA standard re-use policy,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16 Eurostat Energy Balances,eurostat-energy_balances-*/,Eurostat,https://ec.europa.eu/eurostat/web/energy/data/energy-balances Swiss energy statistics from Swiss Federal Office of Energy,switzerland-sfoe/,unknown,http://www.bfe.admin.ch/themen/00526/00541/00542/02167/index.html?dossier_id=02169 BASt emobility statistics,emobility/,unknown,http://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/Stundenwerte.html?nn=626916 diff --git a/doc/installation.rst b/doc/installation.rst index f76a9104..728061c7 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -73,8 +73,8 @@ To download and extract the data bundle on the command line: .. code:: bash - projects/pypsa-eur-sec/data % wget "https://nworbmot.org/pypsa-eur-sec-data-bundle-201012.tar.gz" - projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle-201012.tar.gz + projects/pypsa-eur-sec/data % wget "https://nworbmot.org/pypsa-eur-sec-data-bundle-210125.tar.gz" + projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle-210125.tar.gz The data licences and sources are given in the following table. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 1e43b92c..991b1ca6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,8 +4,13 @@ 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. -*Added an option to alter the capital cost of carriers by a factor via ``carrier+factor`` in the ``{opts}`` wildcard. This can be useful for exploring uncertain cost parameters. Example: ``solar+0.5`` reduces the capital cost of solar to 50\% of original values. + +* For the myopic investment option, a carbon budget and a type of decay (exponential or beta) can be selected in the ``config.yaml`` file to distribute the budget across the ``planning_horizons``. For example, ``cb40ex0`` in the ``{sector_opts}`` wildcard will distribute a carbon budget of 40 GtCO2 following an exponential decay with initial growth rate 0. +* Added an option to alter the capital cost or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{sector_opts}`` wildcard. This can be useful for exploring uncertain cost parameters. Example: ``solar+c0.5`` reduces the ``capital_cost`` of solar to 50\% of original values. Similarly ``solar+p3`` multiplies the ``p_nom_max`` by 3. +* Rename the bus for European liquid hydrocarbons from ``Fischer-Tropsch`` to ``EU oil``, since it can be supplied not just with the Fischer-Tropsch process, but also with fossil oil. +* Bugfix: Fix reading in of ``pypsa-eur/resources/powerplants.csv`` to PyPSA-Eur Version 0.3.0 (use column attribute name ``DateIn`` instead of old ``YearDecommissioned``). +* Bugfix: Make sure that ``Store`` components (battery and H2) are also removed from PyPSA-Eur, so they can be added later by PyPSA-Eur-Sec. + PyPSA-Eur-Sec 0.4.0 (11th December 2020) ========================================= @@ -131,4 +136,4 @@ To make a new release of the data bundle, make an archive of the files in ``data .. code:: bash - data % tar pczf pypsa-eur-sec-data-bundle-date.tar.gz eea switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility urban_percent.csv timezone_mappings.csv heat_load_profile_DK_AdamJensen.csv WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv + data % tar pczf pypsa-eur-sec-data-bundle-YYMMDD.tar.gz eea/UNFCCC_v23.csv switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility urban_percent.csv timezone_mappings.csv heat_load_profile_DK_AdamJensen.csv WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index 44fd04b5..6743572d 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -62,6 +62,9 @@ if __name__ == "__main__": with open('config.yaml', encoding='utf8') as f: snakemake.config = yaml.safe_load(f) + + # This is a hack, to be replaced once snakemake is unicode-conform + if 'Secondary Forestry residues sawdust' in snakemake.config['biomass']['classes']['solid biomass']: snakemake.config['biomass']['classes']['solid biomass'].remove('Secondary Forestry residues sawdust') snakemake.config['biomass']['classes']['solid biomass'].append('Secondary Forestry residues – sawdust') diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 7f37159c..fe28cfed 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -1,6 +1,6 @@ - +import numpy as np import pandas as pd #allow plotting without Xwindows @@ -9,7 +9,7 @@ matplotlib.use('Agg') import matplotlib.pyplot as plt - +from prepare_sector_network import co2_emissions_year #consolidate and rename def rename_techs(label): @@ -237,7 +237,137 @@ def plot_balances(): 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__": # Detect running outside of snakemake and mock snakemake for testing @@ -249,13 +379,16 @@ if __name__ == "__main__": snakemake.config = yaml.safe_load(f) snakemake.input = Dict() snakemake.output = Dict() - + snakemake.wildcards = Dict() + #snakemake.wildcards['sector_opts']='3H-T-H-B-I-solar3-dist1-cb48be3' + for item in ["costs", "energy"]: 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.input["balances"] = snakemake.config['summary_dir'] + '/test/csvs/supply_energy.csv' - snakemake.output["balances"] = snakemake.config['summary_dir'] + '/test/graphs/balances-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'] + '/{name}/graphs/balances-energy.csv'.format(name=snakemake.config['run'],item=item) + + n_header = 4 plot_costs() @@ -263,3 +396,9 @@ if __name__ == "__main__": plot_energy() 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() \ No newline at end of file diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a875a635..68494158 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -48,24 +48,24 @@ override_component_attrs["Store"].loc["build_year"] = ["integer","year",np.nan," override_component_attrs["Store"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"] -def co2_emissions_year(year): + +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 + + #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) - - 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 + co2_emissions = co2_totals.loc[cts, "electricity"].sum() @@ -80,173 +80,57 @@ def co2_emissions_year(year): co2_emissions *=0.001 #MtCO2 to GtCO2 return co2_emissions -def historical_emissions(): - """ - 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)"] - - - 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.to_list() - 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 build_carbon_budget(o): #distribute carbon budget following beta or exponential transition path - if "be" in o: + 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: + 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:]) - - e_1990 = co2_emissions_year(year=1990) - + + + 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(year=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, + CO2_CAP = pd.DataFrame(index = pd.Series(data=planning_horizons, name='planning_horizon'), columns=pd.Series(data=[], - name='paths', + 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 + 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: + + 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') - - """ - Plot historical carbon emissions in the EU and decarbonization path - """ - import matplotlib.pyplot as plt - 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,planning_horizons[-1]+1]) - ax1.plot(e_1990*CO2_CAP[o],linewidth=3, - color='dodgerblue', label=None) - - emissions = historical_emissions() - 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/' - if not os.path.exists(path_cb_plot): - os.makedirs(path_cb_plot) - print('carbon budget distribution saved to ' + path_cb_plot + 'carbon_budget_plot.pdf') - plt.savefig(path_cb_plot+'carbon_budget_plot.pdf', dpi=300) - + 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): """ Add lifetime for solar and wind generators @@ -799,7 +683,7 @@ def add_generation(network): capital_cost=0.) #could correct to e.g. 0.2 EUR/kWh * annuity and O&M network.add("Generator", - "EU fossil " + carrier, + "EU " + carrier, bus="EU " + carrier, p_nom_extendable=True, carrier=carrier, @@ -1179,14 +1063,14 @@ def add_land_transport(network): fuel_cell_share = get_parameter(options["land_transport_fuel_cell_share"]) electric_share = get_parameter(options["land_transport_electric_share"]) - fossil_share = 1 - fuel_cell_share - electric_share + ice_share = 1 - fuel_cell_share - electric_share print("shares of FCEV, EV and ICEV are", fuel_cell_share, electric_share, - fossil_share) + ice_share) - if fossil_share < 0: + if ice_share < 0: print("Error, more FCEV and EV share than 1.") sys.exit() @@ -1264,14 +1148,14 @@ def add_land_transport(network): p_set=fuel_cell_share/options['transport_fuel_cell_efficiency']*transport[nodes]) - if fossil_share > 0: + if ice_share > 0: network.madd("Load", nodes, - suffix=" land transport fossil", - bus="Fischer-Tropsch", - carrier="land transport fossil", - p_set=fossil_share/options['transport_internal_combustion_efficiency']*transport[nodes]) + suffix=" land transport oil", + bus="EU oil", + carrier="land transport oil", + p_set=ice_share/options['transport_internal_combustion_efficiency']*transport[nodes]) @@ -1463,7 +1347,7 @@ def add_heat(network): capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency'] + costs.at['biomass CHP capture','fixed']*costs.at['gas','CO2 intensity'], marginal_cost=costs.at['central gas CHP','VOM'], efficiency=costs.at['central gas CHP','efficiency'] - costs.at['gas','CO2 intensity']*(costs.at['biomass CHP capture','electricity-input'] + costs.at['biomass CHP capture','compression-electricity-input']), - efficiency2=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_b'] + costs.at['gas','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-output']), + efficiency2=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_b'] + costs.at['gas','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-input']), efficiency3=costs.at['gas','CO2 intensity']*(1-costs.at['biomass CHP capture','capture_rate']), efficiency4=costs.at['gas','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'], lifetime=costs.at['central gas CHP','lifetime']) @@ -1682,7 +1566,7 @@ def add_biomass(network): capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency'] + costs.at['biomass CHP capture','fixed']*costs.at['solid biomass','CO2 intensity'], marginal_cost=costs.at['central solid biomass CHP','VOM'], efficiency=costs.at['central solid biomass CHP','efficiency'] - costs.at['solid biomass','CO2 intensity']*(costs.at['biomass CHP capture','electricity-input'] + costs.at['biomass CHP capture','compression-electricity-input']), - efficiency2=costs.at['central solid biomass CHP','efficiency-heat'] + costs.at['solid biomass','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-output']), + efficiency2=costs.at['central solid biomass CHP','efficiency-heat'] + costs.at['solid biomass','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-input']), efficiency3=-costs.at['solid biomass','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'], efficiency4=costs.at['solid biomass','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'], lifetime=costs.at['central solid biomass CHP','lifetime']) @@ -1787,27 +1671,30 @@ def add_industry(network): carrier="H2 for shipping", p_set = nodal_energy_totals.loc[nodes,["total international navigation","total domestic navigation"]].sum(axis=1)*1e6*options['shipping_average_efficiency']/costs.at["fuel cell","efficiency"]/8760.) - network.madd("Bus", - ["Fischer-Tropsch"], - location="EU", - carrier="Fischer-Tropsch") + if "EU oil" not in network.buses.index: + network.madd("Bus", + ["EU oil"], + location="EU", + carrier="oil") #use madd to get carrier inserted - network.madd("Store", - ["Fischer-Tropsch Store"], - bus="Fischer-Tropsch", - e_nom_extendable=True, - e_cyclic=True, - carrier="Fischer-Tropsch", - capital_cost=0.) #could correct to e.g. 0.001 EUR/kWh * annuity and O&M + if "EU oil Store" not in network.stores.index: + network.madd("Store", + ["EU oil Store"], + bus="EU oil", + e_nom_extendable=True, + e_cyclic=True, + carrier="oil", + capital_cost=0.) #could correct to e.g. 0.001 EUR/kWh * annuity and O&M - network.add("Generator", - "fossil oil", - bus="Fischer-Tropsch", - p_nom_extendable=True, - carrier="oil", - capital_cost=0., - marginal_cost=costs.at["oil",'fuel']) + if "EU oil" not in network.generators.index: + network.add("Generator", + "EU oil", + bus="EU oil", + p_nom_extendable=True, + carrier="oil", + capital_cost=0., + marginal_cost=costs.at["oil",'fuel']) if options["oil_boilers"]: @@ -1817,7 +1704,7 @@ def add_industry(network): network.madd("Link", nodes_heat[name] + " " + name + " oil boiler", p_nom_extendable=True, - bus0=["Fischer-Tropsch"] * len(nodes_heat[name]), + bus0="EU oil", bus1=nodes_heat[name] + " " + name + " heat", bus2="co2 atmosphere", carrier=name + " oil boiler", @@ -1830,7 +1717,7 @@ def add_industry(network): network.madd("Link", nodes + " Fischer-Tropsch", bus0=nodes + " H2", - bus1="Fischer-Tropsch", + bus1="EU oil", bus2="co2 stored", carrier="Fischer-Tropsch", efficiency=costs.at["Fischer-Tropsch",'efficiency'], @@ -1841,13 +1728,13 @@ def add_industry(network): network.madd("Load", ["naphtha for industry"], - bus="Fischer-Tropsch", + bus="EU oil", carrier="naphtha for industry", p_set = industrial_demand.loc[nodes,"naphtha"].sum()/8760.) network.madd("Load", ["kerosene for aviation"], - bus="Fischer-Tropsch", + bus="EU oil", carrier="kerosene for aviation", p_set = nodal_energy_totals.loc[nodes,["total international aviation","total domestic aviation"]].sum(axis=1).sum()*1e6/8760.) @@ -1857,9 +1744,9 @@ def add_industry(network): co2 = network.loads.loc[["naphtha for industry","kerosene for aviation"],"p_set"].sum()*costs.at["oil",'CO2 intensity'] - industrial_demand.loc[nodes,"process emission from feedstock"].sum()/8760. network.madd("Load", - ["Fischer-Tropsch emissions"], + ["oil emissions"], bus="co2 atmosphere", - carrier="Fischer-Tropsch emissions", + carrier="oil emissions", p_set=-co2) network.madd("Load", @@ -1935,13 +1822,6 @@ def add_waste_heat(network): network.links.loc[urban_central + " H2 Fuel Cell","bus2"] = urban_central + " urban central heat" network.links.loc[urban_central + " H2 Fuel Cell","efficiency2"] = 0.95 - network.links.loc[urban_central + " H2 Fuel Cell","efficiency"] - -def restrict_technology_potential(n,tech,limit): - print("restricting potentials (p_nom_max) for {} to {} of technical potential".format(tech,limit)) - gens = n.generators.index[n.generators.carrier.str.contains(tech)] - #beware if limit is 0 and p_nom_max is np.inf, 0*np.inf is nan - n.generators.loc[gens,"p_nom_max"] *=limit - def decentral(n): n.lines.drop(n.lines.index,inplace=True) n.links.drop(n.links.index[n.links.carrier.isin(["DC","B2B"])],inplace=True) @@ -1984,8 +1864,9 @@ if __name__ == "__main__": from vresutils.snakemake import MockSnakemake snakemake = MockSnakemake( wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', - opts='', planning_horizons='2050', - sector_opts='Co2L0-180H-T-H-B-I-solar3-dist1-solar+c0.5-Sabatier+c0.5'), + opts='', planning_horizons='2020', + sector_opts='120H-T-H-B-I-onwind+p3-dist1-cb48be3'), + input=dict( network='../pypsa-eur/networks/{network}_s{simpl}_{clusters}_ec_lv{lv}_{opts}.nc', energy_totals_name='resources/energy_totals.csv', co2_totals_name='resources/co2_totals.csv', @@ -2024,7 +1905,8 @@ if __name__ == "__main__": retro_cost_energy = "resources/retro_cost_{network}_s{simpl}_{clusters}.csv", floor_area = "resources/floor_area_{network}_s{simpl}_{clusters}.csv" ), - output=['results/version-0/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'] + output=['results/version-cb48be3/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'] + ) import yaml with open('config.yaml', encoding='utf8') as f: @@ -2129,19 +2011,21 @@ if __name__ == "__main__": print("CO2 limit set to",limit) for o in opts: - if "cb" in o: + + 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: + 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] + + limit=CO2_CAP.loc[investment_year] print("overriding CO2 limit with scenario limit",limit) - + + for o in opts: if "Co2L" in o: limit = o[o.find("Co2L")+4:] @@ -2150,14 +2034,8 @@ if __name__ == "__main__": print("adding CO2 budget limit as per unit of 1990 levels of",limit) add_co2limit(n, Nyears, limit) - + for o in opts: - for tech in ["solar","onwind","offwind"]: - if tech in o and "+" not in o: - limit = o[o.find(tech)+len(tech):] - limit = float(limit.replace("p",".").replace("m","-")) - print("changing potential for",tech,"by factor",limit) - restrict_technology_potential(n,tech,limit) if o[:10] == 'linemaxext': maxext = float(o[10:])*1e3 @@ -2169,28 +2047,31 @@ if __name__ == "__main__": if snakemake.config["sector"]['electricity_distribution_grid']: insert_electricity_distribution_grid(n) - for o in opts: if "+" in o: oo = o.split("+") - carrier_list=np.hstack((n.generators.carrier.unique(), n.links.carrier.unique(), - n.stores.carrier.unique(), n.storage_units.carrier.unique())) - suptechs = map(lambda c: c.split("-", 2)[0], carrier_list) + carrier_list=np.hstack((n.generators.carrier.unique(), n.links.carrier.unique(), + n.stores.carrier.unique(), n.storage_units.carrier.unique())) + suptechs = map(lambda c: c.split("-", 2)[0], carrier_list) if oo[0].startswith(tuple(suptechs)): carrier = oo[0] - # handles only p_nom_max as stores and lines have no potentials attr_lookup = {"p": "p_nom_max", "c": "capital_cost"} attr = attr_lookup[oo[1][0]] factor = float(oo[1][1:]) + #beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan if carrier == "AC": # lines do not have carrier n.lines[attr] *= factor else: - comps = {"Generator", "Link", "StorageUnit", "Store"} + comps = {"Generator", "Link", "StorageUnit"} if attr=='p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"} for c in n.iterate_components(comps): - sel = c.df.carrier.str.contains(carrier) + if carrier=='solar': + sel = c.df.carrier.str.contains(carrier) & ~c.df.carrier.str.contains("solar rooftop") + else: + sel = c.df.carrier.str.contains(carrier) c.df.loc[sel,attr] *= factor print("changing", attr ,"for",carrier,"by factor",factor) - + + if snakemake.config["sector"]['gas_distribution_grid']: insert_gas_distribution_costs(n) if snakemake.config["sector"]['electricity_grid_connection']: