From b7b7407756401917a851b2f5dac21843b82ea5f7 Mon Sep 17 00:00:00 2001 From: martavp Date: Sat, 26 Dec 2020 17:47:32 +0100 Subject: [PATCH 01/21] Adapt nomenclature from "YearCommissioned" to "DataIn" This was breaking add_existing_baseyear.py and it is now fixed. Column name for commissioning year in powerplantmatching has changed. Now "DataIn" is used as column name, also when renewable capacities per country are added to the power plants dataframe --- scripts/add_existing_baseyear.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 852c5bb0..09b47da6 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -125,7 +125,7 @@ def add_existing_renewables(df_agg): if capacity > 0.: df_agg.at[name,"Fueltype"] = tech df_agg.at[name,"Capacity"] = capacity - df_agg.at[name,"YearCommissioned"] = year + df_agg.at[name,"DateIn"] = year df_agg.at[name,"cluster_bus"] = node def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, baseyear): @@ -182,7 +182,7 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas add_existing_renewables(df_agg) df_agg["grouping_year"] = np.take(grouping_years, - np.digitize(df_agg.YearCommissioned, + np.digitize(df_agg.DateIn, grouping_years, right=True)) @@ -249,7 +249,7 @@ def add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years grouping_years : intervals to group existing capacities - linear decomissioning of heating capacities from 2020 to 2045 is + linear decommissioning of heating capacities from 2020 to 2045 is currently assumed heating capacities split between residential and services proportional @@ -408,18 +408,18 @@ if __name__ == "__main__": if 'snakemake' not in globals(): from vresutils.snakemake import MockSnakemake snakemake = MockSnakemake( - wildcards=dict(network='elec', simpl='', clusters='39', lv='1.0', - sector_opts='Co2L0-168H-T-H-B-I-solar3-dist1', - co2_budget_name='b30b3', + wildcards=dict(network='elec', simpl='', clusters='45', lv='1.0', + sector_opts='Co2L0-3H-T-H-B-I-solar3-dist1', planning_horizons='2020'), - input=dict(network='pypsa-eur-sec/results/test/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{co2_budget_name}_{planning_horizons}.nc', + input=dict(network='pypsa-eur-sec/results/version-2/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc', powerplants='pypsa-eur/resources/powerplants.csv', busmap_s='pypsa-eur/resources/busmap_{network}_s{simpl}.csv', busmap='pypsa-eur/resources/busmap_{network}_s{simpl}_{clusters}.csv', - costs='pypsa-eur-sec/data/costs/costs_{planning_horizons}.csv', + costs='technology_data/outputs/costs_{planning_horizons}.csv', cop_air_total="pypsa-eur-sec/resources/cop_air_total_{network}_s{simpl}_{clusters}.nc", - cop_soil_total="pypsa-eur-sec/resources/cop_soil_total_{network}_s{simpl}_{clusters}.nc"), - output=['pypsa-eur-sec/results/test/prenetworks_brownfield/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'], + cop_soil_total="pypsa-eur-sec/resources/cop_soil_total_{network}_s{simpl}_{clusters}.nc", + clustered_pop_layout="pypsa-eur-sec/resources/pop_layout_{network}_s{simpl}_{clusters}.csv",), + output=['pypsa-eur-sec/results/version-2/prenetworks_brownfield/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'], ) import yaml with open('config.yaml', encoding='utf8') as f: From c623b82b3967f3d841125a47d3f3b024febbac7f Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 28 Dec 2020 15:39:05 +0100 Subject: [PATCH 02/21] Make explicit solver_dir=tmpdir Running the rule solve_network in the university cluster, I was getting a "No space left on device" error. Making solve_dir=tmpdir by default avoids this error and makes it easier to identify any problem with temp files --- scripts/solve_network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 795c3327..85251caa 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -272,6 +272,7 @@ def solve_network(n, config=None, solver_log=None, opts=None): solver_name=solver_name, solver_logfile=solver_log, solver_options=solver_options, + solver_dir=tmpdir, extra_functionality=extra_functionality, formulation=solve_opts['formulation']) #extra_postprocessing=extra_postprocessing From 6a7b1d545009fb55bc9f53d5f26de1fd9718899d Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 12:14:08 +0100 Subject: [PATCH 03/21] Fix unicode error due to dash before sawdust A quick fix to https://github.com/PyPSA/pypsa-eur-sec/issues/79 --- config.default.yaml | 2 +- scripts/build_biomass_potentials.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index d43d0e29..7907859a 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -69,7 +69,7 @@ biomass: year: 2030 scenario: "Med" classes: - solid biomass: ['Primary agricultural residues', 'Forestry energy residue', 'Secondary forestry residues', 'Secondary Forestry residues – sawdust', 'Forestry residues from landscape care biomass', 'Municipal waste'] + solid biomass: ['Primary agricultural residues', 'Forestry energy residue', 'Secondary forestry residues', 'Secondary Forestry residues sawdust', 'Forestry residues from landscape care biomass', 'Municipal waste'] not included: ['Bioethanol sugar beet biomass', 'Rapeseeds for biodiesel', 'sunflower and soya for Biodiesel', 'Starchy crops biomass', 'Grassy crops biomass', 'Willow biomass', 'Poplar biomass potential', 'Roundwood fuelwood', 'Roundwood Chips & Pellets'] biogas: ['Manure biomass potential', 'Sludge biomass'] diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index c959680f..44fd04b5 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -1,3 +1,4 @@ +# coding: utf-8 import pandas as pd @@ -57,7 +58,12 @@ if __name__ == "__main__": snakemake.input['jrc_potentials'] = "data/biomass/JRC Biomass Potentials.xlsx" snakemake.output = Dict() snakemake.output['biomass_potentials'] = 'data/biomass_potentials.csv' + snakemake.output['biomass_potentials_all']='resources/biomass_potentials_all.csv' with open('config.yaml', encoding='utf8') as f: snakemake.config = yaml.safe_load(f) - + + 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') + build_biomass_potentials() From ceba265c0a22793f1ee6c6e05d9afc40c1b3b3fd Mon Sep 17 00:00:00 2001 From: martavp Date: Tue, 29 Dec 2020 11:31:00 +0100 Subject: [PATCH 04/21] build_eea_co2() now reads version23 of UNFCCC inventory. **This requires updating to UNFCCC_v23.csv in the data bundle** This enables that the same function is used to read emissions in 2018, which are assumed to remain constant in 2019 and 2020 and subtracted from carbon budget (estimated from 2018 on). I checked and 1990 emissions calculated with UNFCCC_v23 are very similar to those calculated with UNFCCC_v21 (<1% differences in all values at EU level). Some countries show higher deviations, mainly in domestic aviation and navigation. I guess because those values started to be reported later and v23 should include more accurate values. --- scripts/build_energy_totals.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 0cfa709b..4d80abb9 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -390,12 +390,12 @@ def build_energy_totals(): return clean_df -def build_eea_co2(): +def build_eea_co2(year=1990): # 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 - #downloaded 190222 (modified by EEA last on 181130) - fn = "data/eea/UNFCCC_v21.csv" + #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) @@ -418,16 +418,14 @@ def build_eea_co2(): e['waste management'] = '5 - Waste management' e['other'] = '6 - Other Sector' e['indirect'] = 'ind_CO2 - Indirect CO2' - e["total wL"] = "Total (with LULUCF, with indirect CO2)" - e["total woL"] = "Total (without LULUCF, with indirect CO2)" + e["total wL"] = "Total (with LULUCF)" + e["total woL"] = "Total (without LULUCF)" pol = "CO2" #["All greenhouse gases - (CO2 equivalent)","CO2"] 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) #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 -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) @@ -486,10 +484,6 @@ def build_co2_totals(year=1990): #doesn't include non-energy emissions co2.loc[ct,'agriculture'] = eurostat_co2[ct,"+","+","Agriculture / Forestry"].sum() - - - co2.to_csv(snakemake.output.co2_name) - return co2 @@ -547,7 +541,7 @@ if __name__ == "__main__": snakemake.output['transport_name'] = "data/transport_data.csv" 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') population = nuts3['pop'].groupby(nuts3.country).sum() @@ -566,6 +560,7 @@ if __name__ == "__main__": 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() From 4a681749d7d0a48ce46af036aa6649819afcfe79 Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 15:55:08 +0100 Subject: [PATCH 05/21] Add function build_carbon_budget() For the myopic method, based on the carbon budget indicated in the config file (sector_opts), a CO2 limit is calculated for every planning_horizon following an exponential or beta decay. A file with CO2 limit in every planning_horizon and a plot showing historical and planned CO2 emissions are saved in the results --- Snakefile | 1 + config.default.yaml | 4 + scripts/prepare_sector_network.py | 226 +++++++++++++++++++++++++++++- 3 files changed, 226 insertions(+), 5 deletions(-) diff --git a/Snakefile b/Snakefile index ee0fb8cf..0ac8bd99 100644 --- a/Snakefile +++ b/Snakefile @@ -292,6 +292,7 @@ rule build_retro_cost: output: retro_cost="resources/retro_cost_{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" diff --git a/config.default.yaml b/config.default.yaml index d43d0e29..ec55ead5 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -24,11 +24,15 @@ scenario: # 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 + # 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 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 # CO2 budget as a fraction of 1990 emissions # 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: 2020: 0.7011648746 2025: 0.5241935484 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 34a02e0f..088b124f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -20,6 +20,8 @@ import pytz 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 #overriding the component_attrs. This can be done for @@ -45,6 +47,206 @@ 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["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"] + +def co2_emissions_year(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) + + 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() + + 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 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: + #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:]) + + e_1990 = co2_emissions_year(year=1990) + + #emissions at the beginning of the path (last year available 2018) + e_0 = co2_emissions_year(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') + + """ + 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) + def add_lifetime_wind_solar(n): """ Add lifetime for solar and wind generators @@ -1775,15 +1977,15 @@ def get_parameter(item): return item -#%% + if __name__ == "__main__": # Detect running outside of snakemake and mock snakemake for testing if 'snakemake' not in globals(): from vresutils.snakemake import MockSnakemake snakemake = MockSnakemake( wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', - opts='', planning_horizons='2030', co2_budget_name="go", - sector_opts='Co2L0-120H-T-H-B-I-solar3-dist1'), + opts='', planning_horizons='2020', + sector_opts='120H-T-H-B-I-solar3-dist1-cb40ex0'), 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', @@ -1819,10 +2021,10 @@ if __name__ == "__main__": 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_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" ), - output=['pypsa-eur-sec/results/test/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{co2_budget_name}_{planning_horizons}.nc'] + output=['results/version-8/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'] ) import yaml with open('config.yaml', encoding='utf8') as f: @@ -1926,6 +2128,20 @@ if __name__ == "__main__": limit = get_parameter(snakemake.config["co2_budget"]) 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: if "Co2L" in o: limit = o[o.find("Co2L")+4:] From fcc54bada384dffba008c3515cce6e928a0d1b40 Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 15:56:34 +0100 Subject: [PATCH 06/21] Calculate and save cumulative costs for the myopic approach Creates an additional file in results/csvs including the cumulative costs with different social discount rates --- scripts/make_summary.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 4425ad5c..43f64b2c 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -196,7 +196,25 @@ def calculate_costs(n,label,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): #Beware this also has extraneous locations for country (e.g. biomass) or continent-wide (e.g. fossil gas/oil) stuff for c in n.iterate_components(n.branch_components|n.controllable_one_port_components^{"Load"}): @@ -564,16 +582,17 @@ if __name__ == "__main__": snakemake.config = yaml.safe_load(f) #overwrite some options - snakemake.config["run"] = "test" + snakemake.config["run"] = "version-8" 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.input = Dict() 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() for item in outputs: 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) : snakemake.config['results_dir'] + snakemake.config['run'] + '/postnetworks/elec_s{simpl}_{cluster}_lv{lv}_{opt}_{sector_opt}_{planning_horizon}.nc'\ .format(simpl=simpl, @@ -592,6 +611,7 @@ if __name__ == "__main__": print(networks_dict) Nyears = 1 + costs_db = prepare_costs(snakemake.input.costs, snakemake.config['costs']['USD2013_to_EUR2013'], snakemake.config['costs']['discountrate'], @@ -603,3 +623,10 @@ if __name__ == "__main__": df["metrics"].loc["total costs"] = df["costs"].sum() 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') + + + \ No newline at end of file From d7e9dc2466d3b9808003b6a61a7f1458d3ccfa0d Mon Sep 17 00:00:00 2001 From: martavp Date: Fri, 1 Jan 2021 17:53:59 +0100 Subject: [PATCH 07/21] update documentation and release notes --- doc/installation.rst | 13 ++++++++++--- doc/myopic.rst | 25 ++++++++++++++----------- doc/release_notes.rst | 3 +++ doc/supply_demand.rst | 4 ++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index 997f5221..f76a9104 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -16,7 +16,7 @@ its dependencies. Clone the repository: .. 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: @@ -32,7 +32,7 @@ Next install the technology assumptions database `technology-data xarray version >= 0.15.1, you will need the latest master branch of 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 `_ Data requirements ================= diff --git a/doc/myopic.rst b/doc/myopic.rst index 6faaaeff..14f651dd 100644 --- a/doc/myopic.rst +++ b/doc/myopic.rst @@ -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 approach was initially developed and used in the paper `Early decarbonisation of the European Energy system pays off (2020) `__ 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 `__ 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) `__ 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 `__ 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. @@ -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] +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. - -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. +See details in Supplementary Note 1 of the paper `Early decarbonisation of the European Energy system pays off (2020) `__ Rules overview ================= @@ -74,17 +77,17 @@ Rules overview 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``. -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``. -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 @@ -110,8 +113,8 @@ Then, the resulting network is saved in ``results/run_name/networks/prenetworks- 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``. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9d9acc4c..66594614 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -2,6 +2,9 @@ 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) ========================================= diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index 823bdd93..002bd16c 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -106,7 +106,7 @@ Thermal energy storage using hot water tanks Small for decentral applications. -Big pit storage for district heating. +Big water pit storage for district heating. Hydrogen demand @@ -122,7 +122,7 @@ Industry (ammonia, precursor to hydrocarbons for chemicals and iron/steel). Hydrogen supply ================= -SMR, SMR+CCS, electrolysers. +Steam Methane Reforming (SMR), SMR+CCS, electrolysers. Methane demand From 0d64cf5b3542b368b9e21a2fb49b83a4abc0383c Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 4 Jan 2021 10:07:29 +0100 Subject: [PATCH 08/21] Allow decimals in sector_opts --- Snakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snakefile b/Snakefile index 0ac8bd99..6e484aae 100644 --- a/Snakefile +++ b/Snakefile @@ -8,7 +8,7 @@ wildcard_constraints: clusters="[0-9]+m?", sectors="[+a-zA-Z0-9]+", opts="[-+a-zA-Z0-9]*", - sector_opts="[-+a-zA-Z0-9]*" + sector_opts="[-+a-zA-Z0-9\.]*" From 918d803c0d9b56f7f67898c93b7b7a29a9a88401 Mon Sep 17 00:00:00 2001 From: martavp Date: Tue, 12 Jan 2021 11:57:22 +0100 Subject: [PATCH 09/21] Add commented line regarding hack for unicode error in snakemake --- scripts/build_biomass_potentials.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index 44fd04b5..a918eefb 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -62,6 +62,7 @@ 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') From faebbc493b78b6ec386e19bae2b1b43ef83e0c2a Mon Sep 17 00:00:00 2001 From: martavp Date: Tue, 12 Jan 2021 12:01:55 +0100 Subject: [PATCH 10/21] Add comment to config file explaining carbon budget key --- config.default.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index ec55ead5..0a2f6268 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -27,6 +27,8 @@ scenario: # 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 growt 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 From e180931ce1aeed2df8a340836cda75ff35f00029 Mon Sep 17 00:00:00 2001 From: martavp Date: Thu, 14 Jan 2021 10:06:29 +0100 Subject: [PATCH 11/21] Move ploting of carbon budget distribution to plot_summary.py --- scripts/plot_summary.py | 151 ++++++++++++++++++++++++++++-- scripts/prepare_sector_network.py | 151 +++--------------------------- 2 files changed, 159 insertions(+), 143 deletions(-) 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 088b124f..a2c2795e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -48,7 +48,7 @@ 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). """ @@ -62,10 +62,6 @@ def co2_emissions_year(year): 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,70 +76,6 @@ 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 @@ -155,11 +87,16 @@ def build_carbon_budget(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'] @@ -183,69 +120,9 @@ def build_carbon_budget(o): 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) + 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): """ @@ -1985,7 +1862,7 @@ if __name__ == "__main__": snakemake = MockSnakemake( wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', opts='', planning_horizons='2020', - sector_opts='120H-T-H-B-I-solar3-dist1-cb40ex0'), + 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', energy_totals_name='resources/energy_totals.csv', co2_totals_name='resources/co2_totals.csv', @@ -2024,7 +1901,7 @@ 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-8/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: From 0eb69365ebc61264d49a8380fa6cab2f0c4e27b5 Mon Sep 17 00:00:00 2001 From: martavp Date: Thu, 14 Jan 2021 10:21:11 +0100 Subject: [PATCH 12/21] Update Snakefile Allow white space in sector_opts, e.g. to investigate sensitivity to cost of 'H2 Electrolysis' --- Snakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snakefile b/Snakefile index 6e484aae..31370603 100644 --- a/Snakefile +++ b/Snakefile @@ -8,7 +8,7 @@ wildcard_constraints: clusters="[0-9]+m?", sectors="[+a-zA-Z0-9]+", opts="[-+a-zA-Z0-9]*", - sector_opts="[-+a-zA-Z0-9\.]*" + sector_opts="[-+a-zA-Z0-9\.\s]*" From 2555b66ba9854cb70f5d145491aba62243bc7023 Mon Sep 17 00:00:00 2001 From: martavp Date: Thu, 14 Jan 2021 13:49:17 +0100 Subject: [PATCH 13/21] allowing a factor to alter the cost or p_nom_max via the config file This substitutes the previous way of doing it. Now, to multiply the reference p_nom_max by 3, one should include in the config file 'solar+p3' (instead of the previous solarx3) --- config.default.yaml | 5 +++-- doc/release_notes.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 0a2f6268..fbf57c99 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -15,14 +15,15 @@ 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 + # solar+c0.5 reduces the capital cost of solar to 50\% of reference value + # 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 diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 66594614..8076d778 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -5,6 +5,7 @@ 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 or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{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. PyPSA-Eur-Sec 0.4.0 (11th December 2020) ========================================= From fd6239db5c6e5f0a711f8fa99e9048b0d023f3b4 Mon Sep 17 00:00:00 2001 From: martavp Date: Thu, 14 Jan 2021 13:51:10 +0100 Subject: [PATCH 14/21] allowing factor to alter the cost or p_nom_max --- scripts/prepare_sector_network.py | 42 +++++++++++++++++++------------ 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a2c2795e..fd8321c4 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1812,13 +1812,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) @@ -1862,7 +1855,7 @@ if __name__ == "__main__": snakemake = MockSnakemake( wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', opts='', planning_horizons='2020', - sector_opts='120H-T-H-B-I-solar3-dist1-cb48be3'), + 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', @@ -2028,15 +2021,7 @@ 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: - 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 print("limiting new HVAC and HVDC extensions to",maxext,"MW") @@ -2047,6 +2032,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) + if oo[0].startswith(tuple(suptechs)): + carrier = oo[0] + 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"} if attr=='p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"} + for c in n.iterate_components(comps): + 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']: From c638cc98edebcad322c6d40e563076248eb51353 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Jan 2021 12:29:52 +0100 Subject: [PATCH 15/21] add 'snakemake workflow' keyword to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 1b14c5525c126e9bfe047b6a062fb8af68e297e6 Mon Sep 17 00:00:00 2001 From: martavp <30744159+martavp@users.noreply.github.com> Date: Fri, 15 Jan 2021 12:56:21 +0100 Subject: [PATCH 16/21] correct spelling mistake --- config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.default.yaml b/config.default.yaml index 0a2f6268..230765c4 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -28,7 +28,7 @@ scenario: # 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 growt rate 0 + # 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 From 39a5134ab9a8e137b7ea9f9fd087fb2e03647005 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 25 Jan 2021 11:10:13 +0100 Subject: [PATCH 17/21] doc: Add latest EEA emissions stats UNFCCC_v23.csv to data bundle --- doc/data.csv | 2 +- doc/installation.rst | 4 ++-- doc/release_notes.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 8076d778..1971ed77 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -131,4 +131,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 From 4fd164f73ca77f2e2ec8ea9faeac2ffbfa16be61 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 25 Jan 2021 14:17:31 +0100 Subject: [PATCH 18/21] config.yaml: Remove battery and H2 Stores from PyPSA-Eur I.e. what's taken over from PyPSA-Eur in config["pypsa_eur"] from "Store" is []. PyPSA-Eur-Sec adds its own batteries and H2 Stores. --- config.default.yaml | 7 +-- doc/release_notes.rst | 7 ++- scripts/prepare_sector_network.py | 72 +++++++++++++++---------------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index c0143d01..6a1d77cb 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -25,10 +25,10 @@ scenario: # solar+c0.5 reduces the capital cost of solar to 50\% of reference value # 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 + # 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 + # 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 @@ -71,6 +71,7 @@ pypsa_eur: "Link": ["DC"] "Generator": ["onwind", "offwind-ac", "offwind-dc", "solar", "ror"] "StorageUnit": ["PHS","hydro"] + "Store": [] biomass: year: 2030 diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 1971ed77..4ee7cb9e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,8 +4,11 @@ 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 or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{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. +* 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 or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{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. +* Bugfix: Fix reading in of ``pypsa-eur/resources/powerplants.csv`` to PyPSA-Eur Version 0.3.0 (use ``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) ========================================= diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fd8321c4..316eb672 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -53,14 +53,14 @@ 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) co2_emissions = co2_totals.loc[cts, "electricity"].sum() @@ -79,51 +79,51 @@ def co2_emissions_year(cts, opts, year): 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:]) - + 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) - + 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) + 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') + + 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=',', + 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 @@ -1854,7 +1854,7 @@ if __name__ == "__main__": from vresutils.snakemake import MockSnakemake snakemake = MockSnakemake( wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0', - opts='', planning_horizons='2020', + 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', @@ -1999,19 +1999,19 @@ 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:] @@ -2032,13 +2032,13 @@ 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] attr_lookup = {"p": "p_nom_max", "c": "capital_cost"} @@ -2051,12 +2051,12 @@ if __name__ == "__main__": comps = {"Generator", "Link", "StorageUnit"} if attr=='p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"} for c in n.iterate_components(comps): if carrier=='solar': - sel = c.df.carrier.str.contains(carrier) & ~c.df.carrier.str.contains("solar rooftop") + 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']: From 54a28db84f7fc2ccc4f99674ec01b9b52bf0a8f8 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Tue, 26 Jan 2021 10:55:38 +0100 Subject: [PATCH 19/21] Change name of liquid hydrocarbon bus from Fischer-Tropsch to oil Reasoning: we can also have fossil and biomass liquid hydrocarbons, as well as production from the Fischer-Tropsch process, particularly for simulations before 2050. --- config.default.yaml | 5 ++- scripts/prepare_sector_network.py | 69 ++++++++++++++++--------------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 6a1d77cb..0ad40df8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -102,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 @@ -334,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" @@ -369,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/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 316eb672..c8dc9c28 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -676,7 +676,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, @@ -1056,14 +1056,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() @@ -1141,14 +1141,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]) @@ -1664,27 +1664,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"]: @@ -1694,7 +1697,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", @@ -1707,7 +1710,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'], @@ -1718,13 +1721,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.) @@ -1734,9 +1737,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", From cd8bf1edfc19c167417a7911ea9168dce3c9c194 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Fri, 29 Jan 2021 16:34:52 +0100 Subject: [PATCH 20/21] doc: Include renaming of liquid hydrocarbon bus in release notes --- doc/release_notes.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4ee7cb9e..991b1ca6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,9 +4,11 @@ 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 or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{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. -* Bugfix: Fix reading in of ``pypsa-eur/resources/powerplants.csv`` to PyPSA-Eur Version 0.3.0 (use ``DateIn`` instead of old ``YearDecommissioned``). + +* 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. From 31ed6ebc1657026042ac891f0f7f6ee75c73a958 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Tue, 2 Feb 2021 13:16:17 +0100 Subject: [PATCH 21/21] Correct heat output of central gas and solid biomass CHP with CC The heat output from the carbon capture (CC) was being subtracted from the CHP rather than the heat input. Since the heat output and heat input are the same in the DEA technology database (but at different temperatures), this bug has no consequence, but still better to correct it. --- scripts/prepare_sector_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c8dc9c28..9634ed07 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1340,7 +1340,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']) @@ -1559,7 +1559,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'])