diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 35e56660..abafff4d 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -16,6 +16,8 @@ Upcoming Release * For industry distribution, use EPRTR as fallback if ETS data is not available. +* The minimum capacity for renewable generators when using the myopic option has been fixed. + PyPSA-Eur 0.8.1 (27th July 2023) ================================ diff --git a/rules/postprocess.smk b/rules/postprocess.smk index e09103cc..c37c688e 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -107,6 +107,8 @@ rule plot_summary: countries=config_provider("countries"), planning_horizons=config_provider("scenario", "planning_horizons"), sector_opts=config_provider("scenario", "sector_opts"), + emissions_scope=config_provider("energy", "emissions"), + eurostat_report_year=config_provider("energy", "eurostat_report_year"), plotting=config_provider("plotting"), RDIR=RDIR, input: @@ -114,6 +116,7 @@ rule plot_summary: energy=RESULTS + "csvs/energy.csv", balances=RESULTS + "csvs/supply_energy.csv", eurostat=input_eurostat, + co2="data/eea/UNFCCC_v23.csv", output: costs=RESULTS + "graphs/costs.pdf", energy=RESULTS + "graphs/energy.pdf", diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index ff5e950e..5ba473e8 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -165,7 +165,7 @@ def sanitize_carriers(n, config): nice_names = ( pd.Series(config["plotting"]["nice_names"]) .reindex(carrier_i) - .fillna(carrier_i.to_series().str.title()) + .fillna(carrier_i.to_series()) ) n.carriers["nice_name"] = n.carriers.nice_name.where( n.carriers.nice_name != "", nice_names diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index b2c534e6..269705a7 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -435,15 +435,20 @@ def add_heating_capacities_installed_before_baseyear( # split existing capacities between residential and services # proportional to energy demand + p_set_sum = n.loads_t.p_set.sum() ratio_residential = pd.Series( [ ( - n.loads_t.p_set.sum()[f"{node} residential rural heat"] + p_set_sum[f"{node} residential rural heat"] / ( - n.loads_t.p_set.sum()[f"{node} residential rural heat"] - + n.loads_t.p_set.sum()[f"{node} services rural heat"] + p_set_sum[f"{node} residential rural heat"] + + p_set_sum[f"{node} services rural heat"] ) ) + # if rural heating demand for one of the nodes doesn't exist, + # then columns were dropped before and heating demand share should be 0.0 + if all(f"{node} {service} rural heat" in p_set_sum.index for service in ["residential", "services"]) + else 0. for node in nodal_df.index ], index=nodal_df.index, diff --git a/scripts/build_industrial_distribution_key.py b/scripts/build_industrial_distribution_key.py index 24cb6fa0..c5cbd19c 100644 --- a/scripts/build_industrial_distribution_key.py +++ b/scripts/build_industrial_distribution_key.py @@ -17,6 +17,8 @@ import geopandas as gpd import pandas as pd from packaging.version import Version, parse +import country_converter as coco +cc = coco.CountryConverter() def locate_missing_industrial_sites(df): """ @@ -93,6 +95,17 @@ def prepare_hotmaps_database(regions): gdf.rename(columns={"index_right": "bus"}, inplace=True) gdf["country"] = gdf.bus.str[:2] + # the .sjoin can lead to duplicates if a geom is in two overlapping regions + if gdf.index.duplicated().any(): + # get all duplicated entries + duplicated_i = gdf.index[gdf.index.duplicated()] + # convert from raw data country name to iso-2-code + code = cc.convert(gdf.loc[duplicated_i, "Country"], to="iso2") + # screen out malformed country allocation + gdf_filtered = gdf.loc[duplicated_i].query("country == @code") + # concat not duplicated and filtered gdf + gdf = pd.concat([gdf.drop(duplicated_i), gdf_filtered]) + return gdf diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 56ee98c9..98a6a6d7 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -711,5 +711,5 @@ if __name__ == "__main__": if snakemake.params.foresight == "myopic": cumulative_cost = calculate_cumulative_cost() cumulative_cost.to_csv( - "results/" + snakemake.params.RDIR + "/csvs/cumulative_cost.csv" + "results/" + snakemake.params.RDIR + "csvs/cumulative_cost.csv" ) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index e7de5473..072c7128 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -387,6 +387,9 @@ def historical_emissions(countries): countries.remove("GB") countries.append("UK") + # remove countries which are not included in eea historical emission dataset + countries_to_remove = {"AL", "BA", "ME", "MK", "RS"} + countries = list(set(countries) - countries_to_remove) year = np.arange(1990, 2018).tolist() idx = pd.IndexSlice @@ -457,9 +460,20 @@ def plot_carbon_budget_distribution(input_eurostat): ax1.set_ylim([0, 5]) ax1.set_xlim([1990, snakemake.params.planning_horizons[-1] + 1]) - path_cb = "results/" + snakemake.params.RDIR + "/csvs/" + path_cb = "results/" + snakemake.params.RDIR + "csvs/" countries = snakemake.params.countries - e_1990 = co2_emissions_year(countries, input_eurostat, opts, year=1990) + emissions_scope = snakemake.params.emissions_scope + report_year = snakemake.params.eurostat_report_year + input_co2 = snakemake.input.co2 + e_1990 = co2_emissions_year( + countries, + input_eurostat, + opts, + emissions_scope, + report_year, + input_co2, + 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) @@ -535,7 +549,7 @@ def plot_carbon_budget_distribution(input_eurostat): fancybox=True, fontsize=18, loc=(0.01, 0.01), facecolor="white", frameon=True ) - path_cb_plot = "results/" + snakemake.params.RDIR + "/graphs/" + path_cb_plot = "results/" + snakemake.params.RDIR + "graphs/" plt.savefig(path_cb_plot + "carbon_budget_plot.pdf", dpi=300) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1ee42a72..e1cc0462 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -196,17 +196,15 @@ def get(item, investment_year=None): def co2_emissions_year( - countries, input_eurostat, opts, emissions_scope, report_year, year + countries, input_eurostat, opts, emissions_scope, report_year, input_co2, year ): """ Calculate CO2 emissions in one specific year (e.g. 1990 or 2018). """ - emissions_scope = snakemake.params.energy["emissions"] - eea_co2 = build_eea_co2(snakemake.input.co2, year, emissions_scope) + eea_co2 = build_eea_co2(input_co2, year, emissions_scope) # TODO: read Eurostat data from year > 2014 # this only affects the estimation of CO2 emissions for BA, RS, AL, ME, MK - report_year = snakemake.params.energy["eurostat_report_year"] if year > 2014: eurostat_co2 = build_eurostat_co2( input_eurostat, countries, report_year, year=2014 @@ -227,7 +225,7 @@ def co2_emissions_year( # TODO: move to own rule with sector-opts wildcard? -def build_carbon_budget(o, input_eurostat, fn, emissions_scope, report_year): +def build_carbon_budget(o, input_eurostat, fn, emissions_scope, report_year, input_co2): """ Distribute carbon budget following beta or exponential transition path. """ @@ -245,12 +243,24 @@ def build_carbon_budget(o, input_eurostat, fn, emissions_scope, report_year): countries = snakemake.params.countries e_1990 = co2_emissions_year( - countries, input_eurostat, opts, emissions_scope, report_year, year=1990 + countries, + input_eurostat, + opts, + emissions_scope, + report_year, + input_co2, + year=1990, ) # emissions at the beginning of the path (last year available 2018) e_0 = co2_emissions_year( - countries, input_eurostat, opts, emissions_scope, report_year, year=2018 + countries, + input_eurostat, + opts, + emissions_scope, + report_year, + input_co2, + year=2018, ) planning_horizons = snakemake.params.planning_horizons @@ -3398,12 +3408,18 @@ if __name__ == "__main__": if "cb" not in o: continue limit_type = "carbon budget" - fn = "results/" + snakemake.params.RDIR + "/csvs/carbon_budget_distribution.csv" + fn = "results/" + snakemake.params.RDIR + "csvs/carbon_budget_distribution.csv" if not os.path.exists(fn): emissions_scope = snakemake.params.emissions_scope report_year = snakemake.params.eurostat_report_year + input_co2 = snakemake.input.co2 build_carbon_budget( - o, snakemake.input.eurostat, fn, emissions_scope, report_year + o, + snakemake.input.eurostat, + fn, + emissions_scope, + report_year, + input_co2, ) co2_cap = pd.read_csv(fn, index_col=0).squeeze() limit = co2_cap.loc[investment_year] diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 37b05286..1e1e738b 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -55,6 +55,9 @@ def _add_land_use_constraint(n): # warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind' for carrier in ["solar", "onwind", "offwind-ac", "offwind-dc"]: + extendable_i = (n.generators.carrier == carrier) & n.generators.p_nom_extendable + n.generators.loc[extendable_i, "p_nom_min"] = 0 + ext_i = (n.generators.carrier == carrier) & ~n.generators.p_nom_extendable existing = ( n.generators.loc[ext_i, "p_nom"] @@ -71,7 +74,7 @@ def _add_land_use_constraint(n): if len(existing_large): logger.warning( f"Existing capacities larger than technical potential for {existing_large},\ - adjust technical potential to existing capacities" + adjust technical potential to existing capacities" ) n.generators.loc[existing_large, "p_nom_max"] = n.generators.loc[ existing_large, "p_nom_min"