From 2c49b8e3c33edc43da14d5d8cfbd3480c01a1bf1 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 2 Dec 2020 11:51:27 +0100 Subject: [PATCH 01/44] config: Move heat pump sink temp from script to config.yaml --- config.default.yaml | 4 +++- scripts/build_cop_profiles.py | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 54470178..016913c9 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -99,7 +99,9 @@ sector: 'transport_fuel_cell_efficiency': 0.5 'transport_internal_combustion_efficiency': 0.3 'shipping_average_efficiency' : 0.4 #For conversion of fuel oil to propulsion in 2011 - 'time_dep_hp_cop' : True + 'time_dep_hp_cop' : True #time dependent heat pump coefficient of performance + 'heat_pump_sink_T' : 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py + # conservatively high to cover hot water and space heating in poorly-insulated buildings 'retrofitting' : 'retro_exogen': True # space heat demand savings exogenously 'dE': # reduction of space heat demand (applied before losses in DH) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index d469489c..fddd86f6 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -9,16 +9,13 @@ import xarray as xr cop_f = {"air" : lambda d_t: 6.81 -0.121*d_t + 0.000630*d_t**2, "soil" : lambda d_t: 8.77 -0.150*d_t + 0.000734*d_t**2} -sink_T = 55. # Based on DTU / large area radiators - - for area in ["total", "urban", "rural"]: for source in ["air", "soil"]: source_T = xr.open_dataarray(snakemake.input["temp_{}_{}".format(source,area)]) - delta_T = sink_T - source_T + delta_T = snakemake.config['sector']['heat_pump_sink_T'] - source_T cop = cop_f[source](delta_T) From 0187d4d1d46c02cf995c5818bc768689f5e5799a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 2 Dec 2020 13:34:33 +0100 Subject: [PATCH 02/44] make retro script work with newer version of pandas --- scripts/build_retro_cost.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/scripts/build_retro_cost.py b/scripts/build_retro_cost.py index a70ab0cc..af44c91a 100644 --- a/scripts/build_retro_cost.py +++ b/scripts/build_retro_cost.py @@ -23,7 +23,6 @@ Structure: import pandas as pd import matplotlib.pyplot as plt -pd.options.mode.chained_assignment = None #%% ************ FUCNTIONS *************************************************** @@ -175,9 +174,9 @@ def prepare_building_stock_data(): area = building_data[(building_data.type == 'Heated area [Mm²]') & (building_data.subsector != "Total")] area_tot = area.groupby(["country", "sector"]).sum() - area["weight"] = area.apply(lambda x: x.value / + area = pd.concat([area, area.apply(lambda x: x.value / area_tot.value.loc[(x.country, x.sector)], - axis=1) + axis=1).rename("weight")],axis=1) area = area.groupby(['country', 'sector', 'subsector', 'bage']).sum() area_tot.rename(index=country_iso_dic, inplace=True) @@ -192,9 +191,9 @@ def prepare_building_stock_data(): pop_layout["ct"] = pop_layout.index.str[:2] ct_total = pop_layout.total.groupby(pop_layout["ct"]).sum() - area_per_pop = area_tot.unstack().apply(lambda x: x / ct_total[x.index]) + area_per_pop = area_tot.unstack().reindex(index=ct_total.index).apply(lambda x: x / ct_total[x.index]) missing_area_ct = ct_total.index.difference(area_tot.index.levels[0]) - for ct in missing_area_ct: + for ct in (missing_area_ct & ct_total.index): averaged_data = pd.DataFrame( area_per_pop.value.reindex(map_for_missings[ct]).mean() * ct_total[ct], @@ -233,7 +232,7 @@ def prepare_building_stock_data(): # smallest possible today u values for windows 0.8 (passive house standard) # maybe the u values for the glass and not the whole window including frame # for those types assumed in the dataset - u_values[(u_values.type=="Windows") & (u_values.value<0.8)]["value"] = 0.8 + u_values.loc[(u_values.type=="Windows") & (u_values.value<0.8), "value"] = 0.8 # drop unnecessary columns u_values.drop(['topic', 'feature','detail', 'estimated','unit'], axis=1, inplace=True, errors="ignore") @@ -314,8 +313,12 @@ def calculate_cost_energy_curve(u_values, l_strength, l_weight, average_surface_ for l in l_strength: u_values[l] = calculate_new_u(u_values, l, l_weight) - energy_saved[l] = calculate_dE(u_values, l, average_surface_w) - costs[l] = calculate_costs(u_values, l, cost_retro, average_surface) + energy_saved = pd.concat([energy_saved, + calculate_dE(u_values, l, average_surface_w).rename(l)], + axis=1) + costs = pd.concat([costs, + calculate_costs(u_values, l, cost_retro, average_surface).rename(l)], + axis=1) # energy and costs per country, sector, subsector and year e_tot = energy_saved.groupby(['country', 'sector', 'subsector', 'bage']).sum() @@ -334,11 +337,13 @@ def calculate_cost_energy_curve(u_values, l_strength, l_weight, average_surface_ axis=1, keys=["dE", "cost"]) res.rename(index=country_iso_dic, inplace=True) - res = res.loc[countries] + res = res.reindex(index=countries, level=0) + # reset index because otherwise not considered countries still in index.levels[0] + res = res.reset_index().set_index(["country", "sector"]) # map missing countries - for ct in map_for_missings.keys(): - averaged_data = pd.DataFrame(res.loc[map_for_missings[ct], :].mean(level=1)) + for ct in pd.Index(map_for_missings.keys()) & countries: + averaged_data = res.reindex(index=map_for_missings[ct], level=0).mean(level=1) index = pd.MultiIndex.from_product([[ct], averaged_data.index.to_list()]) averaged_data.index = index if ct not in res.index.levels[0]: @@ -436,12 +441,14 @@ if __name__ == "__main__": # for missing weighting of surfaces of building types assume MultiFamily houses u_values["assumed_subsector"] = u_values.subsector - u_values.assumed_subsector[ - ~u_values.subsector.isin(average_surface.index)] = 'Multifamily houses' + u_values.loc[~u_values.subsector.isin(average_surface.index), + "assumed_subsector"] = 'Multifamily houses' dE_and_cost = calculate_cost_energy_curve(u_values, l_strength, l_weight, average_surface_w, average_surface, area, country_iso_dic, countries) + # reset index because otherwise not considered countries still in index.levels[0] + dE_and_cost = dE_and_cost.reset_index().set_index(["country", "sector"]) # weights costs after construction index if construction_index: From c861ab940d673a85fe9863007b96dde8c1467e8e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 2 Dec 2020 14:04:38 +0100 Subject: [PATCH 03/44] add data sources for building retrofitting --- doc/data.csv | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/data.csv b/doc/data.csv index dd66b491..c8cb4b1f 100644 --- a/doc/data.csv +++ b/doc/data.csv @@ -17,3 +17,10 @@ IRENA existing VRE capacities,existing_infrastructure/{solar|onwind|offwind}_cap USGS ammonia production,myb1-2017-nitro.xls,unknown,https://www.usgs.gov/centers/nmic/nitrogen-statistics-and-information hydrogen salt cavern potentials,hydrogen_salt_cavern_potentials.csv,CC BY 4.0,https://doi.org/10.1016/j.ijhydene.2019.12.161 hotmaps industrial site database,Industrial_Database.csv,CC BY 4.0,https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database +Hotmaps building stock data,data_building_stock.csv,CC BY 4.0,https://gitlab.com/hotmaps/building-stock +U-values Poland,u_values_poland.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory +Floor area missing in hotmaps building stock data,floor_area_missing.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory +Comparative level investment,comparative_level_investment.csv,Eurostat,https://ec.europa.eu/eurostat/statistics-explained/index.php?title=Comparative_price_levels_for_investment +Electricity taxes,electricity_taxes_eu.csv,Eurostat,https://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_pc_204&lang=en +Average surface components,average_surface_components.csv,unknown,http://webtool.building-typology.eu/#bm +Retrofitting thermal envelope costs for Germany,retro_cost_germany.csv,unkown,https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/ From 47a618f38591345b2aabb3adb7e5a272ddc0e266 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 7 Dec 2020 10:48:24 +0100 Subject: [PATCH 04/44] Replace large CHP 2-link feasibility diagram with single link In almost 99.5% of cases the CHP dispatches along the backpressure line where heat output is proportional to electricity output. So we can switch to a single link to avoid the burden of modelling the full electricity-heat feasibility space of CHPs. This only applies to large CHPs in district heating networks. --- scripts/make_summary.py | 3 + scripts/prepare_sector_network.py | 125 +++++++++--------------------- scripts/solve_network.py | 3 + 3 files changed, 43 insertions(+), 88 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 53ca8624..4425ad5c 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -28,10 +28,13 @@ opt_name = {"Store": "e", "Line" : "s", "Transformer" : "s"} override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()}) override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"] override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"] +override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"] override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"] +override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"] override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"] +override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"] override_component_attrs["StorageUnit"].loc["p_dispatch"] = ["series","MW",0.,"Storage discharging.","Output"] override_component_attrs["StorageUnit"].loc["p_store"] = ["series","MW",0.,"Storage charging.","Output"] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 09d515da..25db733e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -30,10 +30,13 @@ from vresutils.costdata import annuity override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()}) override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"] override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"] +override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"] override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"] +override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"] override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"] +override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"] override_component_attrs["Link"].loc["build_year"] = ["integer","year",np.nan,"build year","Input (optional)"] override_component_attrs["Link"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"] @@ -1226,63 +1229,35 @@ def add_heat(network): if name == "urban central": #add gas CHP; biomass CHP is added in biomass section network.madd("Link", - nodes[name] + " urban central gas CHP electric", + nodes[name] + " urban central gas CHP", bus0="EU gas", bus1=nodes[name], - bus2="co2 atmosphere", - carrier="urban central gas CHP electric", + bus2=nodes[name] + " urban central heat", + bus3="co2 atmosphere", + carrier="urban central gas CHP", p_nom_extendable=True, - capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency'], + capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b']), marginal_cost=costs.at['central gas CHP','VOM'], - efficiency=costs.at['central gas CHP','efficiency'], - efficiency2=costs.at['gas','CO2 intensity'], - c_b=costs.at['central gas CHP','c_b'], - c_v=costs.at['central gas CHP','c_v'], - p_nom_ratio=costs.at['central gas CHP','p_nom_ratio'], + efficiency=costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b']), + efficiency2=costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b'])/costs.at['central gas CHP','c_b'], + efficiency3=costs.at['gas','CO2 intensity'], lifetime=costs.at['central gas CHP','lifetime']) network.madd("Link", - nodes[name] + " urban central gas CHP heat", - bus0="EU gas", - bus1=nodes[name] + " urban central heat", - bus2="co2 atmosphere", - carrier="urban central gas CHP heat", - p_nom_extendable=True, - marginal_cost=costs.at['central gas CHP','VOM'], - efficiency=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_v'], - efficiency2=costs.at['gas','CO2 intensity'], - lifetime=costs.at['central gas CHP','lifetime']) - - network.madd("Link", - nodes[name] + " urban central gas CHP CCS electric", + nodes[name] + " urban central gas CHP CCS", bus0="EU gas", bus1=nodes[name], - bus2="co2 atmosphere", - bus3="co2 stored", - carrier="urban central gas CHP CCS electric", + bus2=nodes[name] + " urban central heat", + bus3="co2 atmosphere", + bus4="co2 stored", + carrier="urban central gas CHP CCS", p_nom_extendable=True, - capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency'], + capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b']), marginal_cost=costs.at['central gas CHP CCS','VOM'], - efficiency=costs.at['central gas CHP CCS','efficiency'], - efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), - efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"], - c_b=costs.at['central gas CHP CCS','c_b'], - c_v=costs.at['central gas CHP CCS','c_v'], - p_nom_ratio=costs.at['central gas CHP CCS','p_nom_ratio'], - lifetime=costs.at['central gas CHP CCS','lifetime']) - - network.madd("Link", - nodes[name] + " urban central gas CHP CCS heat", - bus0="EU gas", - bus1=nodes[name] + " urban central heat", - bus2="co2 atmosphere", - bus3="co2 stored", - carrier="urban central gas CHP CCS heat", - p_nom_extendable=True, - marginal_cost=costs.at['central gas CHP CCS','VOM'], - efficiency=costs.at['central gas CHP CCS','efficiency']/costs.at['central gas CHP CCS','c_v'], - efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), - efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"], + efficiency=costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b']), + efficiency2=costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b'])/costs.at['central gas CHP CCS','c_b'], + efficiency3=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), + efficiency4=costs.at['gas','CO2 intensity']*options["ccs_fraction"], lifetime=costs.at['central gas CHP CCS','lifetime']) else: @@ -1475,61 +1450,35 @@ def add_biomass(network): urban_central = urban_central.str[:-len(" urban central heat")] network.madd("Link", - urban_central + " urban central solid biomass CHP electric", + urban_central + " urban central solid biomass CHP", bus0="EU solid biomass", bus1=urban_central, - carrier="urban central solid biomass CHP electric", + bus2=urban_central + " urban central heat", + carrier="urban central solid biomass CHP", p_nom_extendable=True, - capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency'], + capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b']), marginal_cost=costs.at['central solid biomass CHP','VOM'], - efficiency=costs.at['central solid biomass CHP','efficiency'], - c_b=costs.at['central solid biomass CHP','c_b'], - c_v=costs.at['central solid biomass CHP','c_v'], - p_nom_ratio=costs.at['central solid biomass CHP','p_nom_ratio'], - lifetime=costs.at['central solid biomass CHP','lifetime']) - - - network.madd("Link", - urban_central + " urban central solid biomass CHP heat", - bus0="EU solid biomass", - bus1=urban_central + " urban central heat", - carrier="urban central solid biomass CHP heat", - p_nom_extendable=True, - marginal_cost=costs.at['central solid biomass CHP','VOM'], - efficiency=costs.at['central solid biomass CHP','efficiency']/costs.at['central solid biomass CHP','c_v'], + efficiency=costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b']), + efficiency2=costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b'])/costs.at['central solid biomass CHP','c_b'], lifetime=costs.at['central solid biomass CHP','lifetime']) network.madd("Link", - urban_central + " urban central solid biomass CHP CCS electric", + urban_central + " urban central solid biomass CHP CCS", bus0="EU solid biomass", bus1=urban_central, - bus2="co2 atmosphere", - bus3="co2 stored", - carrier="urban central solid biomass CHP CCS electric", + bus2=urban_central + " urban central heat", + bus3="co2 atmosphere", + bus4="co2 stored", + carrier="urban central solid biomass CHP CCS", p_nom_extendable=True, - capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency'], + capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b']), marginal_cost=costs.at['central solid biomass CHP CCS','VOM'], - efficiency=costs.at['central solid biomass CHP CCS','efficiency'], - efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - c_b=costs.at['central solid biomass CHP','c_b'], - c_v=costs.at['central solid biomass CHP','c_v'], - p_nom_ratio=costs.at['central solid biomass CHP','p_nom_ratio'], + efficiency=costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b']), + efficiency2=costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b'])/costs.at['central solid biomass CHP CCS','c_b'], + efficiency3=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], + efficiency4=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], lifetime=costs.at['central solid biomass CHP CCS','lifetime']) - network.madd("Link", - urban_central + " urban central solid biomass CHP CCS heat", - bus0="EU solid biomass", - bus1=urban_central + " urban central heat", - bus2="co2 atmosphere", - bus3="co2 stored", - carrier="urban central solid biomass CHP CCS heat", - p_nom_extendable=True, - marginal_cost=costs.at['central solid biomass CHP CCS','VOM'], - efficiency=costs.at['central solid biomass CHP CCS','efficiency']/costs.at['central solid biomass CHP CCS','c_v'], - efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - lifetime=costs.at['central solid biomass CHP CCS','lifetime']) def add_industry(network): diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 51086038..795c3327 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -28,10 +28,13 @@ from vresutils.benchmark import memory_logger override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()}) override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"] override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"] +override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"] override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"] +override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"] override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"] override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"] +override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"] From 098281b432c69d51c502a9d515494867059191bf Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 7 Dec 2020 12:32:53 +0100 Subject: [PATCH 05/44] Correct central CHP from extraction to back pressure The assumptions for c_b and c_v and eta were arranged assuming extraction plants (like the coal CHP in DEA). However, if you look in DEA assumptions at "09b Wood Pellets Medium" (used for solid biomass CHP) and "Gas turbine simple cycle (large)" (used for gas CHP) they are not extraction plants but back pressure plants. The back pressure coefficient in DEA c_b is simply c_b = name plate electricity efficiency / name plate heat efficiency both measured when both heat and electricity are produced at maximum. For the extraction plants, the efficiency was measured in condensation mode, i.e. no heat production. --- scripts/prepare_sector_network.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 25db733e..423421aa 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1236,10 +1236,10 @@ def add_heat(network): bus3="co2 atmosphere", carrier="urban central gas CHP", p_nom_extendable=True, - capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b']), + capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency'], marginal_cost=costs.at['central gas CHP','VOM'], - efficiency=costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b']), - efficiency2=costs.at['central gas CHP','efficiency']/(1+costs.at['central gas CHP','c_v']/costs.at['central gas CHP','c_b'])/costs.at['central gas CHP','c_b'], + efficiency=costs.at['central gas CHP','efficiency'], + efficiency2=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_b'], efficiency3=costs.at['gas','CO2 intensity'], lifetime=costs.at['central gas CHP','lifetime']) @@ -1252,10 +1252,10 @@ def add_heat(network): bus4="co2 stored", carrier="urban central gas CHP CCS", p_nom_extendable=True, - capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b']), + capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency'], marginal_cost=costs.at['central gas CHP CCS','VOM'], - efficiency=costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b']), - efficiency2=costs.at['central gas CHP CCS','efficiency']/(1+costs.at['central gas CHP CCS','c_v']/costs.at['central gas CHP CCS','c_b'])/costs.at['central gas CHP CCS','c_b'], + efficiency=costs.at['central gas CHP CCS','efficiency'], + efficiency2=costs.at['central gas CHP CCS','efficiency']/costs.at['central gas CHP CCS','c_b'], efficiency3=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), efficiency4=costs.at['gas','CO2 intensity']*options["ccs_fraction"], lifetime=costs.at['central gas CHP CCS','lifetime']) @@ -1456,10 +1456,10 @@ def add_biomass(network): bus2=urban_central + " urban central heat", carrier="urban central solid biomass CHP", p_nom_extendable=True, - capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b']), + capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency'], marginal_cost=costs.at['central solid biomass CHP','VOM'], - efficiency=costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b']), - efficiency2=costs.at['central solid biomass CHP','efficiency']/(1+costs.at['central solid biomass CHP','c_v']/costs.at['central solid biomass CHP','c_b'])/costs.at['central solid biomass CHP','c_b'], + efficiency=costs.at['central solid biomass CHP','efficiency'], + efficiency2=costs.at['central solid biomass CHP','efficiency-heat'], lifetime=costs.at['central solid biomass CHP','lifetime']) network.madd("Link", @@ -1471,10 +1471,10 @@ def add_biomass(network): bus4="co2 stored", carrier="urban central solid biomass CHP CCS", p_nom_extendable=True, - capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b']), + capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency'], marginal_cost=costs.at['central solid biomass CHP CCS','VOM'], - efficiency=costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b']), - efficiency2=costs.at['central solid biomass CHP CCS','efficiency']/(1+costs.at['central solid biomass CHP CCS','c_v']/costs.at['central solid biomass CHP CCS','c_b'])/costs.at['central solid biomass CHP CCS','c_b'], + efficiency=costs.at['central solid biomass CHP CCS','efficiency'], + efficiency2=costs.at['central solid biomass CHP CCS','efficiency-heat'], efficiency3=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], efficiency4=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], lifetime=costs.at['central solid biomass CHP CCS','lifetime']) From f34728c0c4306086a33abf162b54e0a46bfb46f9 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 7 Dec 2020 16:23:25 +0100 Subject: [PATCH 06/44] doc: Document supply and demand options in the different sectors --- doc/index.rst | 49 +++------- doc/spatial_resolution.rst | 54 +++++++++++ doc/supply_demand.rst | 180 +++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 35 deletions(-) create mode 100644 doc/spatial_resolution.rst create mode 100644 doc/supply_demand.rst diff --git a/doc/index.rst b/doc/index.rst index d9108eda..39c8ab04 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -66,41 +66,6 @@ PyPSA-Eur-Sec is designed to be imported into the open toolbox `PyPSA `_ at the `Institute for Automation and Applied Informatics `_ at the `Karlsruhe Institute of Technology `_. The group is funded by the `Helmholtz Association `_ until 2024. Previous versions were developed by the `Renewable Energy Group `_ at `FIAS `_ to carry out simulations for the `CoNDyNet project `_, financed by the `German Federal Ministry for Education and Research (BMBF) `_ as part of the `Stromnetze Research Initiative `_. -Spatial resolution of sectors -============================= - -Not all of the sectors are at the full nodal resolution, and some are -distributed to nodes using heuristics that need to be corrected. Some -networks are copper-plated to reduce computational times. - -For example: - -Electricity network: nodal. - -Electricity demand: nodal, distributed in each country based on -population, GDP and location of industrial facilities. - -Building heating demand: nodal, distributed in each country based on -population. - -Industry demand: nodal, distributed in each country based on -locations of industry from `HotMaps database `_. - -Hydrogen network: nodal. - -Methane network: single node for Europe, since future demand is so -low and no bottlenecks are expected. - -Solid biomass: single node for Europe, until transport costs can be -incorporated. - -CO2: single node for Europe, but a transport and storage cost is added for -sequestered CO2. - -Liquid hydrocarbons: single node for Europe, since transport costs are low. - - - Documentation ============= @@ -115,6 +80,20 @@ Documentation installation +**Implementation details** + +* :doc:`spatial_resolution` +* :doc:`supply_demand` + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Implementation details + + spatial_resolution + supply_demand + + **Foresight options** * :doc:`overnight` diff --git a/doc/spatial_resolution.rst b/doc/spatial_resolution.rst new file mode 100644 index 00000000..1be9f3ad --- /dev/null +++ b/doc/spatial_resolution.rst @@ -0,0 +1,54 @@ +.. _spatial_resolution: + +########################################## +Spatial resolution +########################################## + +The default nodal resolution of the model follows the electricity +generation and transmission model `PyPSA-Eur +`_, which clusters down the +electricity transmission substations in each European country based on +the k-means algorithm. This gives nodes which correspond to major load +and generation centres (typically cities). + +The total number of nodes for Europe is set in the ``config.yaml`` file +under ``clusters``. The number of nodes can vary between 37, the number +of independent countries / synchronous areas, and several +hundred. With 200-300 nodes the model needs 100-150 GB RAM to solve +with a commerical solver like Gurobi. + + +Not all of the sectors are at the full nodal resolution, and some +demand for some sectors is distributed to nodes using heuristics that +need to be corrected. Some networks are copper-plated to reduce +computational times. + +For example: + +Electricity network: nodal. + +Electricity residential and commercial demand: nodal, distributed in +each country based on population and GDP. + +Electricity demand in industry: based on the location of industrial +facilities from `HotMaps database `_. + +Building heating demand: nodal, distributed in each country based on +population. + +Industry demand: nodal, distributed in each country based on +locations of industry from `HotMaps database `_. + +Hydrogen network: nodal. + +Methane network: single node for Europe, since future demand is so +low and no bottlenecks are expected. + +Solid biomass: single node for Europe, until transport costs can be +incorporated. + +CO2: single node for Europe, but a transport and storage cost is added for +sequestered CO2. + +Liquid hydrocarbons: single node for Europe, since transport costs for +liquids are low. diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst new file mode 100644 index 00000000..660b9f24 --- /dev/null +++ b/doc/supply_demand.rst @@ -0,0 +1,180 @@ +.. _supply_demand: + +########################################## +Supply and demand +########################################## + +An initial orientation to the supply and demand options in the model +PyPSA-Eur-Sec can be found in the description of the model +PyPSA-Eur-Sec-30 in the paper `Synergies of sector coupling and +transmission reinforcement in a cost-optimised, highly renewable +European energy system `_ (2018). +The latest version of PyPSA-Eur-Sec differs by including biomass, +industry, industrial feedstocks, aviation, shipping, better carbon +management, carbon capture and usage/sequestration, and gas networks. + +The basic supply (left column) and demand (right column) options in the model are described in this figure: + +.. image:: ../graphics/multisector_figure.png + + + +Electricity supply and demand +============================= + +Electricity supply and demand follows the electricity generation and +transmission model `PyPSA-Eur `_, +except that hydrogen storage is integrated into the hydrogen supply, +demand and network, and PyPSA-Eur-Sec includes CHPs. + +Heat demand +============================= + +Heat demand is split into: + +* ``urban central``: large-scale district heating networks in urban areas with dense heat demand +* ``residential/services urban decentral``: heating for individual buildings in urban areas +* ``residential/services rural``: heating for individual buildings in rural areas + + +Heat supply +======================= + +Oil and gas boilers +-------------------- + +Heat pumps +------------- + +Either air-to-water or ground-to-water heat pumps are implemented. + +They have coefficient of performance (COP) based on either the +external air or the soil hourly temperature. + +Ground-source heat pumps are only allowed in rural areas because of +space constraints. + +Only air-source heat pumps are allowed in urban areas. This is a +conservative assumption, since there are many possible sources of +low-temperature heat that could be tapped in cities (waste water, +rivers, lakes, seas, etc.). + +Resistive heaters +-------------------- + + +Large Combined Heat and Power (CHP) plants +-------------------------------------------- + +A good summary of CHP options that can be implemented in PyPSA can be found in the paper `Cost sensitivity of optimal sector-coupled district heating production systems `_. + +PyPSA-Eur-Sec includes CHP plants fuelled by methane, hydrogen and solid biomass from waste and residues. + +Hydrogen CHPs are fuel cells. + +Methane and biomass CHPs are based on back pressure plants operating with a fixed ratio of electricity to heat output. The methane CHP is modelled on the Danish Energy Agency (DEA) "Gas turbine simple cycle (large)" while the solid biomass CHP is based on the DEA's "09b Wood Pellets Medium". + +The efficiencies of each are given on the back pressure line, where the back pressure coefficient ``c_b`` is the electricity output divided by the heat output. The plants are not allowed to deviate from the back pressure line and are implement as ``Link`` objects with a fixed ratio of heat to electricity output. + + +NB: The old PyPSA-Eur-Sec-30 model assumed an extraction plant (like the DEA coal CHP) for gas which has flexible production of heat and electricity within the feasibility diagram of Figure 4 in the `Synergies paper `_. We have switched to the DEA back pressure plants since these are more common for smaller plants for biomass, and because the extraction plants were on the back pressure line for 99.5% of the time anyway. The plants were all changed to back pressure in PyPSA-Eur-Sec v0.4.0. + + +Micro-CHP for individual buildings +----------------------------------- + +Optional. + +Waste heat from Fuel Cells, Methanation and Fischer-Tropsch plants +------------------------------------------------------------------- + + +Solar thermal collectors +------------------------- + +Thermal energy storage using hot water tanks +--------------------------------------------- + +Small for decentral applications. + +Big pit storage for district heating. + + +Hydrogen demand +================== + +Stationary fuel cell CHP. + +Transport applications. + +Industry (ammonia, precursor to hydrocarbons for chemicals and iron/steel). + + +Hydrogen supply +================= + +SMR, SMR+CCS, electrolysers. + + +Methane demand +================== + +Can be used in boilers, in CHPs, in industry for high temperature heat, in OCGT. + +Not used in transport because of engine slippage. + +Methane supply +================= + +Fossil, biogas, Sabatier (hydrogen to methane), HELMETH (directly power to methane with efficient heat integration). + + +Solid biomass demand +===================== + +Solid biomass provides process heat up to 500 Celsius in industry, as well as feeding CHP plants in district heating networks. + +Solid biomass supply +===================== + +Only wastes and residues from the JRC biomass dataset. + + +Oil product demand +===================== + +Transport fuels and naphtha as a feedstock for the chemicals industry. + +Oil product supply +====================== + +Fossil or Fischer-Tropsch. + + +Industry demand +================ + +Based on materials demand from JRC-IDEES and other sources. + + +Industry supply +================ + +Process switching (e.g. from blast furnaces to direct reduction and electric arc furnaces for steel) is defined exogenously. + +Fuel switching for process heat is mostly also done exogenously. + +Solid biomass is used for up to 500 Celsius, mostly in paper and pulp and food and beverages. + +Higher temperatures are met with methane. + + +Carbon dioxide capture, usage and sequestration (CCU/S) +========================================================= + +Carbon dioxide can be captured from industry process emissions, +emissions related to industry process heat, combined heat and power +plants, and directly from the air (DAC). + +Carbon dioxide can be used as an input for methanation and +Fischer-Tropsch fuels, or it can be sequestered underground. From b8f1f3018365a9b553ad9bad0148a7c6d1452cd0 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 15:18:01 +0100 Subject: [PATCH 07/44] Make code compatible with PyPSA-Eur v0.3.0 Only change was to remove the Store-Link-Bus combinations for batteries and H2 storage from PyPSA-Eur, since they are implemented with different names, costs and voltage level in PyPSA-Eur-Sec. Removals are now done in a more transparent way in the config.yaml. --- config.default.yaml | 9 +++++++++ scripts/prepare_sector_network.py | 17 ++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 016913c9..1885a8d0 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -56,6 +56,15 @@ electricity: battery: 6 H2: 168 +# regulate what components with which carriers are kept from PyPSA-Eur; +# some technologies are removed because they are implemented differently +# or have different year-dependent costs in PyPSA-Eur-Sec +pypsa_eur: + "Bus": ["AC"] + "Link": ["DC"] + "Generator": ["onwind", "offwind-ac", "offwind-dc", "solar", "ror"] + "StorageUnit": ["PHS","hydro"] + biomass: year: 2030 scenario: "Med" diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 423421aa..7105f806 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -155,16 +155,15 @@ def remove_elec_base_techs(n): """remove conventional generators (e.g. OCGT) and storage units (e.g. batteries and H2) from base electricity-only network, since they're added here differently using links """ - to_keep = {"generators" : snakemake.config["plotting"]["vre_techs"], - "storage_units" : snakemake.config["plotting"]["renewable_storage_techs"]} - n.carriers = n.carriers.loc[to_keep["generators"] + to_keep["storage_units"]] - - for components, techs in iteritems(to_keep): - df = getattr(n,components) - to_remove = df.carrier.value_counts().index^techs - print("removing {} with carrier {}".format(components,to_remove)) - df.drop(df.index[df.carrier.isin(to_remove)],inplace=True) + for c in n.iterate_components(snakemake.config["pypsa_eur"]): + to_keep = snakemake.config["pypsa_eur"][c.name] + to_remove = pd.Index(c.df.carrier.unique())^to_keep + print("Removing",c.list_name,"with carrier",to_remove) + names = c.df.index[c.df.carrier.isin(to_remove)] + print(names) + n.mremove(c.name, names) + n.carriers.drop(to_remove, inplace=True, errors="ignore") def add_co2_tracking(n): From 3ff669b07b40745610a80cb1002e85e087431d23 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 15:36:45 +0100 Subject: [PATCH 08/44] Move CO2 sequestration potential and costs to config.yaml --- config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 1885a8d0..3fd187c6 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -139,6 +139,8 @@ sector: 'dac' : True 'co2_vent' : True 'SMR' : True + 'co2_sequestration_potential' : 200 #MtCO2/a sequestration potential for Europe + 'co2_sequestration_cost' : 20 #EUR/tCO2 for transport and sequestration of CO2 'ccs_fraction' : 0.9 'hydrogen_underground_storage' : True 'use_fischer_tropsch_waste_heat' : True diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7105f806..20115c34 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -193,12 +193,10 @@ def add_co2_tracking(n): location="EU", carrier="co2 stored") - #TODO move cost to data/costs.csv - #TODO move maximum somewhere more transparent n.madd("Store",["co2 stored"], - e_nom_extendable = True, - e_nom_max=2e8, - capital_cost=20., + e_nom_extendable=True, + e_nom_max=options['co2_sequestration_potential']*1e6, + capital_cost=options['co2_sequestration_cost'], carrier="co2 stored", bus="co2 stored") From 608c9a5ac50389548ba46fb4a08a4231e4205fdb Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 16:38:49 +0100 Subject: [PATCH 09/44] Replace gas and solid biomass CHP CCS with CHP + DEA CC Use DEA assumptions for post-combustion carbon capture. Also rename CCS as CC whenever only carbon capture is involved, since sequestration (or CCU) is a separate step. --- config.default.yaml | 2 +- scripts/plot_summary.py | 2 +- scripts/prepare_sector_network.py | 90 +++++++++++++++---------------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 3fd187c6..52794c3d 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -141,7 +141,7 @@ sector: 'SMR' : True 'co2_sequestration_potential' : 200 #MtCO2/a sequestration potential for Europe 'co2_sequestration_cost' : 20 #EUR/tCO2 for transport and sequestration of CO2 - 'ccs_fraction' : 0.9 + 'cc_fraction' : 0.9 # default fraction of CO2 captured with post-combustion capture 'hydrogen_underground_storage' : True 'use_fischer_tropsch_waste_heat' : True 'use_fuel_cell_waste_heat' : True diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 9ecea108..7f37159c 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -22,7 +22,7 @@ def rename_techs(label): "retrofitting" : "building retrofitting", "H2" : "hydrogen storage", "battery" : "battery storage", - "CCS" : "CCS"} + "CC" : "CC"} rename = {"solar" : "solar PV", "Sabatier" : "methanation", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 20115c34..bff5cdef 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -218,7 +218,7 @@ def add_co2_tracking(n): marginal_cost=75., efficiency=1., p_nom_extendable=True, - lifetime=costs.at['DAC','lifetime']) + lifetime=costs.at['direct air capture','lifetime']) def add_co2limit(n, Nyears=1.,limit=0.): @@ -936,18 +936,18 @@ def add_storage(network): if options['SMR']: network.madd("Link", - nodes + " SMR CCS", + nodes + " SMR CC", bus0=["EU gas"]*len(nodes), bus1=nodes+" H2", bus2="co2 atmosphere", bus3="co2 stored", p_nom_extendable=True, - carrier="SMR CCS", - efficiency=costs.at["SMR CCS","efficiency"], - efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), - efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"], - capital_cost=costs.at["SMR CCS","fixed"], - lifetime=costs.at['SMR CCS','lifetime']) + carrier="SMR CC", + efficiency=costs.at["SMR CC","efficiency"], + efficiency2=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]), + efficiency3=costs.at['gas','CO2 intensity']*options["cc_fraction"], + capital_cost=costs.at["SMR CC","fixed"], + lifetime=costs.at['SMR CC','lifetime']) network.madd("Link", nodes + " SMR", @@ -1241,21 +1241,21 @@ def add_heat(network): lifetime=costs.at['central gas CHP','lifetime']) network.madd("Link", - nodes[name] + " urban central gas CHP CCS", + nodes[name] + " urban central gas CHP CC", bus0="EU gas", bus1=nodes[name], bus2=nodes[name] + " urban central heat", bus3="co2 atmosphere", bus4="co2 stored", - carrier="urban central gas CHP CCS", + carrier="urban central gas CHP CC", p_nom_extendable=True, - capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency'], - marginal_cost=costs.at['central gas CHP CCS','VOM'], - efficiency=costs.at['central gas CHP CCS','efficiency'], - efficiency2=costs.at['central gas CHP CCS','efficiency']/costs.at['central gas CHP CCS','c_b'], - efficiency3=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), - efficiency4=costs.at['gas','CO2 intensity']*options["ccs_fraction"], - lifetime=costs.at['central gas CHP CCS','lifetime']) + 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']), + efficiency3=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]), + efficiency4=costs.at['gas','CO2 intensity']*options["cc_fraction"], + lifetime=costs.at['central gas CHP','lifetime']) else: if options["micro_chp"]: @@ -1460,21 +1460,21 @@ def add_biomass(network): lifetime=costs.at['central solid biomass CHP','lifetime']) network.madd("Link", - urban_central + " urban central solid biomass CHP CCS", + urban_central + " urban central solid biomass CHP CC", bus0="EU solid biomass", bus1=urban_central, bus2=urban_central + " urban central heat", bus3="co2 atmosphere", bus4="co2 stored", - carrier="urban central solid biomass CHP CCS", + carrier="urban central solid biomass CHP CC", p_nom_extendable=True, - capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency'], - marginal_cost=costs.at['central solid biomass CHP CCS','VOM'], - efficiency=costs.at['central solid biomass CHP CCS','efficiency'], - efficiency2=costs.at['central solid biomass CHP CCS','efficiency-heat'], - efficiency3=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - efficiency4=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - lifetime=costs.at['central solid biomass CHP CCS','lifetime']) + 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']), + efficiency3=-costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], + efficiency4=costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], + lifetime=costs.at['central solid biomass CHP','lifetime']) @@ -1511,18 +1511,18 @@ def add_industry(network): efficiency=1.) network.madd("Link", - ["solid biomass for industry CCS"], + ["solid biomass for industry CC"], bus0="EU solid biomass", bus1="solid biomass for industry", bus2="co2 atmosphere", bus3="co2 stored", - carrier="solid biomass for industry CCS", + carrier="solid biomass for industry CC", p_nom_extendable=True, - capital_cost=costs.at["industry CCS","fixed"]*costs.at['solid biomass','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) + capital_cost=costs.at["industry CC","fixed"]*costs.at['solid biomass','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) efficiency=0.9, - efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"], - lifetime=costs.at['industry CCS','lifetime']) + efficiency2=-costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], + efficiency3=costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], + lifetime=costs.at['industry CC','lifetime']) network.madd("Bus", @@ -1547,18 +1547,18 @@ def add_industry(network): efficiency2=costs.at['gas','CO2 intensity']) network.madd("Link", - ["gas for industry CCS"], + ["gas for industry CC"], bus0="EU gas", bus1="gas for industry", bus2="co2 atmosphere", bus3="co2 stored", - carrier="gas for industry CCS", + carrier="gas for industry CC", p_nom_extendable=True, - capital_cost=costs.at["industry CCS","fixed"]*costs.at['gas','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) + capital_cost=costs.at["industry CC","fixed"]*costs.at['gas','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) efficiency=0.9, - efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]), - efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"], - lifetime=costs.at['industry CCS','lifetime']) + efficiency2=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]), + efficiency3=costs.at['gas','CO2 intensity']*options["cc_fraction"], + lifetime=costs.at['industry CC','lifetime']) network.madd("Load", @@ -1692,18 +1692,18 @@ def add_industry(network): p_nom_extendable=True, efficiency=1.) - #assume enough local waste heat for CCS + #assume enough local waste heat for CC network.madd("Link", - ["process emissions CCS"], + ["process emissions CC"], bus0="process emissions", bus1="co2 atmosphere", bus2="co2 stored", - carrier="process emissions CCS", + carrier="process emissions CC", p_nom_extendable=True, - capital_cost=costs.at["industry CCS","fixed"]*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) - efficiency=(1-options["ccs_fraction"]), - efficiency2=options["ccs_fraction"], - lifetime=costs.at['industry CCS','lifetime']) + capital_cost=costs.at["industry CC","fixed"]*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) + efficiency=(1-options["cc_fraction"]), + efficiency2=options["cc_fraction"], + lifetime=costs.at['industry CC','lifetime']) From 0d96ec1de4528b6c91f08a9c18bd8fa8f3964cc2 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 17:26:29 +0100 Subject: [PATCH 10/44] Use DEA capture rates for CHP CC rather than default rate --- scripts/prepare_sector_network.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index bff5cdef..03c45750 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1253,8 +1253,8 @@ def add_heat(network): 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']), - efficiency3=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]), - efficiency4=costs.at['gas','CO2 intensity']*options["cc_fraction"], + 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']) else: @@ -1472,8 +1472,8 @@ def add_biomass(network): 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']), - efficiency3=-costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], - efficiency4=costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], + 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']) From 19a7a1a6841faa4c377884babecf079b9f3b30ad Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 18:19:57 +0100 Subject: [PATCH 11/44] Implement DAC properly with electricity and heat demand Before it just had a fixed marginal cost. Now it uses DEA assumptions for heat, electricity and capital costs. This necessitates locating it somewhere concrete. Heat is taken from urban central or decentral buses. --- scripts/prepare_sector_network.py | 34 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 03c45750..6dcd3ad3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -208,17 +208,26 @@ def add_co2_tracking(n): efficiency=1., p_nom_extendable=True) - if options['dac']: - #direct air capture consumes electricity to take CO2 from the air to the underground store - #TODO do with cost from Breyer - later use elec and heat and capital cost - n.madd("Link",["DAC"], - bus0="co2 atmosphere", - bus1="co2 stored", - carrier="DAC", - marginal_cost=75., - efficiency=1., - p_nom_extendable=True, - lifetime=costs.at['direct air capture','lifetime']) +def add_dac(n): + + heat_buses = n.buses.index[n.buses.carrier.isin(["urban central heat", + "services urban decentral heat"])] + locations = n.buses.location[heat_buses] + + n.madd("Link", + locations, + suffix=" DAC", + bus0="co2 atmosphere", + bus1="co2 stored", + bus2=locations.values, + bus3=heat_buses, + carrier="DAC", + capital_cost=costs.at['direct air capture','fixed'], + efficiency=1., + efficiency2=-(costs.at['direct air capture','electricity-input'] + costs.at['direct air capture','compression-electricity-input']), + efficiency3=-(costs.at['direct air capture','heat-input'] - costs.at['direct air capture','compression-heat-output']), + p_nom_extendable=True, + lifetime=costs.at['direct air capture','lifetime']) def add_co2limit(n, Nyears=1.,limit=0.): @@ -1896,6 +1905,9 @@ if __name__ == "__main__": if "I" in opts and "H" in opts: add_waste_heat(n) + if options['dac']: + add_dac(n) + if "decentral" in opts: decentral(n) From b2c40dfe30c2681b2f6015f4e4f6ae7126a08cbb Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Wed, 9 Dec 2020 18:47:50 +0100 Subject: [PATCH 12/44] Replace old "industry CC" assumption with DEA "cement capture" --- config.default.yaml | 2 +- scripts/prepare_sector_network.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 52794c3d..325ba929 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -316,7 +316,7 @@ plotting: "DAC" : "#E74C3C" "co2 stored" : "#123456" "CO2 sequestration" : "#123456" - "CCS" : "k" + "CC" : "k" "co2" : "#123456" "co2 vent" : "#654321" "solid biomass for industry co2 from atmosphere" : "#654321" diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6dcd3ad3..34a02e0f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1527,11 +1527,11 @@ def add_industry(network): bus3="co2 stored", carrier="solid biomass for industry CC", p_nom_extendable=True, - capital_cost=costs.at["industry CC","fixed"]*costs.at['solid biomass','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) + capital_cost=costs.at["cement capture","fixed"]*costs.at['solid biomass','CO2 intensity'], efficiency=0.9, - efficiency2=-costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], - efficiency3=costs.at['solid biomass','CO2 intensity']*options["cc_fraction"], - lifetime=costs.at['industry CC','lifetime']) + efficiency2=-costs.at['solid biomass','CO2 intensity']*costs.at["cement capture","capture_rate"], + efficiency3=costs.at['solid biomass','CO2 intensity']*costs.at["cement capture","capture_rate"], + lifetime=costs.at['cement capture','lifetime']) network.madd("Bus", @@ -1563,11 +1563,11 @@ def add_industry(network): bus3="co2 stored", carrier="gas for industry CC", p_nom_extendable=True, - capital_cost=costs.at["industry CC","fixed"]*costs.at['gas','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) + capital_cost=costs.at["cement capture","fixed"]*costs.at['gas','CO2 intensity'], efficiency=0.9, - efficiency2=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]), - efficiency3=costs.at['gas','CO2 intensity']*options["cc_fraction"], - lifetime=costs.at['industry CC','lifetime']) + efficiency2=costs.at['gas','CO2 intensity']*(1-costs.at["cement capture","capture_rate"]), + efficiency3=costs.at['gas','CO2 intensity']**costs.at["cement capture","capture_rate"], + lifetime=costs.at['cement capture','lifetime']) network.madd("Load", @@ -1709,10 +1709,10 @@ def add_industry(network): bus2="co2 stored", carrier="process emissions CC", p_nom_extendable=True, - capital_cost=costs.at["industry CC","fixed"]*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h) - efficiency=(1-options["cc_fraction"]), - efficiency2=options["cc_fraction"], - lifetime=costs.at['industry CC','lifetime']) + capital_cost=costs.at["cement capture","fixed"], + efficiency=(1-costs.at["cement capture","capture_rate"]), + efficiency2=costs.at["cement capture","capture_rate"], + lifetime=costs.at['cement capture','lifetime']) From 27cc2935be3c3453779eb2917a002de3166826e1 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Fri, 11 Dec 2020 17:13:18 +0100 Subject: [PATCH 13/44] PyPSA-Eur-Sec Version 0.4.0 See the release notes: https://pypsa-eur-sec.readthedocs.io/en/latest/release_notes.html#pypsa-eur-sec-0-4-0-11th-december-2020 --- config.default.yaml | 2 +- doc/conf.py | 4 ++-- doc/release_notes.rst | 25 +++++++++++++++++++++++++ doc/supply_demand.rst | 15 ++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 325ba929..d43d0e29 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -1,4 +1,4 @@ -version: 0.3.0 +version: 0.4.0 logging_level: INFO diff --git a/doc/conf.py b/doc/conf.py index 4ba6b968..af5a2aa8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -70,9 +70,9 @@ author = u'2019-2020 Tom Brown (KIT), Marta Victoria (Aarhus University), Lisa Z # built documents. # # The short X.Y version. -version = u'0.3' +version = u'0.4' # The full version, including alpha/beta/rc tags. -release = u'0.3.0' +release = u'0.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 69b0221f..9d9acc4c 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -2,6 +2,29 @@ Release Notes ########################################## + +PyPSA-Eur-Sec 0.4.0 (11th December 2020) +========================================= + +This release includes a more accurate nodal disaggregation of industry demand within each country, fixes to CHP and CCS representations, as well as changes to some configuration settings. + +It has been released to coincide with `PyPSA-Eur `_ Version 0.3.0 and `Technology Data `_ Version 0.2.0, and is known to work with these releases. + +New features: + +* The `Hotmaps Industrial Database `_ is used to disaggregate the industrial demand spatially to the nodes inside each country (previously it was distributed by population density). +* Electricity demand from industry is now separated from the regular electricity demand and distributed according to the industry demand. Only the remaining regular electricity demand for households and services is distributed according to GDP and population. +* A cost database for the retrofitting of the thermal envelope of residential and services buildings has been integrated, as well as endogenous optimisation of the level of retrofitting. This is described in the paper `Mitigating heat demand peaks in buildings in a highly renewable European energy system `_. Retrofitting can be activated both exogenously and endogenously from the ``config.yaml``. +* The biomass and gas combined heat and power (CHP) parameters ``c_v`` and ``c_b`` were read in assuming they were extraction plants rather than back pressure plants. The data is now corrected in `Technology Data `_ Version 0.2.0 to the correct DEA back pressure assumptions and they are now implemented as single links with a fixed ratio of electricity to heat output (even as extraction plants, they were always sitting on the backpressure line in simulations, so there was no point in modelling the full heat-electricity feasibility polygon). The old assumptions underestimated the heat output. +* The Danish Energy Agency released `new assumptions for carbon capture `_ in October 2020, which have now been incorporated in PyPSA-Eur-Sec, including direct air capture (DAC) and post-combustion capture on CHPs, cement kilns and other industrial facilities. The electricity and heat demand for DAC is modelled for each node (with heat coming from district heating), but currently the electricity and heat demand for industrial capture is not modelled very cleanly (for process heat, 10% of the energy is assumed to go to carbon capture) - a new issue will be opened on this. +* Land transport is separated by energy carrier (fossil, hydrogen fuel cell electric vehicle, and electric vehicle), but still needs to be separated into heavy and light vehicles (the data is there, just not the code yet). +* For assumptions that change with the investment year, there is a new time-dependent format in the ``config.yaml`` using a dictionary with keys for each year. Implemented examples include the CO2 budget, exogenous retrofitting share and land transport energy carrier; more parameters will be dynamised like this in future. +* Some assumptions have been moved out of the code and into the ``config.yaml``, including the carbon sequestration potential and cost, the heat pump sink temperature, reductions in demand for high value chemicals, and some BEV DSM parameters and transport efficiencies. +* Documentation on :doc:`supply_demand` options has been added. + +Many thanks to Fraunhofer ISI for opening the hotmaps database and to Lisa Zeyen (KIT) for implementing the building retrofitting. + + PyPSA-Eur-Sec 0.3.0 (27th September 2020) ========================================= @@ -92,6 +115,8 @@ Release Process * Update version number in ``doc/conf.py`` and ``*config.*.yaml``. +* Make a ``git commit``. + * Tag a release by running ``git tag v0.x.x``, ``git push``, ``git push --tags``. Include release notes in the tag message. * Make a `GitHub release `_, which automatically triggers archiving by `zenodo `_. diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index 660b9f24..823bdd93 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -27,6 +27,15 @@ transmission model `PyPSA-Eur `_, except that hydrogen storage is integrated into the hydrogen supply, demand and network, and PyPSA-Eur-Sec includes CHPs. +Unlike PyPSA-Eur, PyPSA-Eur-Sec does not distribution electricity demand for industry according to population and GDP, but uses the +geographical data from the `Hotmaps Industrial Database +`_. + +Also unlike PyPSA-Eur, PyPSA-Eur-Sec subtracts existing electrified heating from the existing electricity demand, so that power-to-heat can be optimised separately. + +The remaining electricity demand for households and services is distributed inside each country proportional to GDP and population. + + Heat demand ============================= @@ -154,7 +163,11 @@ Fossil or Fischer-Tropsch. Industry demand ================ -Based on materials demand from JRC-IDEES and other sources. +Based on materials demand from JRC-IDEES and other sources such as the USGS for ammonia. + +Industry is split into many sectors, including iron and steel, ammonia, other basic chemicals, cement, non-metalic minerals, alumuninium, other non-ferrous metals, pulp, paper and printing, food, beverages and tobacco, and other more minor sectors. + +Inside each country the industrial demand is distributed using the `Hotmaps Industrial Database `_. Industry supply From 3ae8021cb69f18d7a80efc6d92b082ccf06f469a Mon Sep 17 00:00:00 2001 From: martavp Date: Sat, 26 Dec 2020 17:47:32 +0100 Subject: [PATCH 14/44] 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 ec5efcc93d02ef21df0bda976d9cd642804f20fc Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 28 Dec 2020 15:39:05 +0100 Subject: [PATCH 15/44] 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 5db196f92ad4c353d86cf349e87ee61fbdbc9990 Mon Sep 17 00:00:00 2001 From: martavp Date: Tue, 29 Dec 2020 11:31:00 +0100 Subject: [PATCH 16/44] 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 7479ba0424ad95d531094ee7313d76e83e05c281 Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 12:14:08 +0100 Subject: [PATCH 17/44] 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 3457d4ee386e5760eca5c1e73e5993ae3d385336 Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 15:55:08 +0100 Subject: [PATCH 18/44] 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 7907859a..08cda6c4 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 7c464f166b468c3ae64fb725036f83b7f2471a04 Mon Sep 17 00:00:00 2001 From: martavp Date: Wed, 30 Dec 2020 15:56:34 +0100 Subject: [PATCH 19/44] 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 cf71858bc0df2d52e57f4453abf22becde4d53b7 Mon Sep 17 00:00:00 2001 From: martavp Date: Fri, 1 Jan 2021 17:53:59 +0100 Subject: [PATCH 20/44] 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 93360119d941f3e9fe8c0484968b139f64dfdb30 Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 4 Jan 2021 10:07:29 +0100 Subject: [PATCH 21/44] 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 b98235c1f1ae4d502a688d3982a72ec711900422 Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 4 Jan 2021 10:13:30 +0100 Subject: [PATCH 22/44] Allow specifying an option to alter the capital cost of carriers by a factor indicated in the config file, eg: solar+c0.5 This is almost a direct copy PyPSA-Eur #167 https://github.com/PyPSA/pypsa-eur/pull/167 A factor altering the maximum capacity (p_nom_max) can also be specified by e.g. solar+p3 One should be careful when using this for solar because the factor is applied to all the generators whose carrier includes the string 'solar' (i.e., it is applied to both utility and rooftop solar) I would suggest implementing 'solar utility' and 'solar rooftop' as carriers, since this can be useful for other selecting processes. Is there is any reason for keeping 'solar' as a carrier for 'solar utility'? The previous way of increasing maximum capacity via the config file (e.g 'solar3') is still present in the code. --- config.default.yaml | 1 + doc/release_notes.rst | 1 + scripts/prepare_sector_network.py | 33 +++++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 08cda6c4..946977f9 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -24,6 +24,7 @@ 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 + # 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 # planning_horizons), be:beta decay; ex:exponential decay diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 66594614..1e43b92c 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 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. PyPSA-Eur-Sec 0.4.0 (11th December 2020) ========================================= diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 088b124f..a875a635 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1984,8 +1984,8 @@ if __name__ == "__main__": from vresutils.snakemake import MockSnakemake 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'), + opts='', planning_horizons='2050', + sector_opts='Co2L0-180H-T-H-B-I-solar3-dist1-solar+c0.5-Sabatier+c0.5'), 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 +2024,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-0/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'] ) import yaml with open('config.yaml', encoding='utf8') as f: @@ -2150,11 +2150,10 @@ 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: + 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) @@ -2170,6 +2169,28 @@ 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] + # 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:]) + if carrier == "AC": # lines do not have carrier + n.lines[attr] *= factor + else: + comps = {"Generator", "Link", "StorageUnit", "Store"} + for c in n.iterate_components(comps): + 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 a6a88e26daefeef1938683a37f3e6f70da9fecf0 Mon Sep 17 00:00:00 2001 From: martavp Date: Mon, 4 Jan 2021 16:22:39 +0100 Subject: [PATCH 23/44] 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 b7b7407756401917a851b2f5dac21843b82ea5f7 Mon Sep 17 00:00:00 2001 From: martavp Date: Sat, 26 Dec 2020 17:47:32 +0100 Subject: [PATCH 24/44] 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 25/44] 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 26/44] 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 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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 41/44] 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 42/44] 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 43/44] 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 44/44] 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'])