From 52b81791397493f99726a805f35871429e596c8f Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:34:26 +0100 Subject: [PATCH 1/3] Replace bioenergy and MSW stores with generators (#1373) * replace bioenergy stores with generators * fix values to ensure feasibility * represent other bioenergy and msw sources as generators * fix spatial variable for biogas * fix virtual msw transport bug * make operational limit for msw work * fix bioliquids potential resolution * improve virtual msw transport * add release note --- doc/release_notes.rst | 2 + scripts/prepare_sector_network.py | 104 +++++++++++++----------------- 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index a4a2a4a3d..9e615a917 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -12,6 +12,8 @@ Upcoming Release ================ * Bugfix: Duplicates in build_transmission_projects were caught, but not removed from the network. This is now fixed. +* Replaced the Store representation of biogenic carriers (solid biomass, biogas, bioliquids, MSW) in ``prepare_sector_network`` with the extended Generator component that uses the ``e_sum_min`` and ``e_sum_max`` attributes to enforce minimum usage and limit maximum potential, respectively. + * Added option to reduce central heating forward temperatures by annual percentage (see rule :mod:`build_central_heating_temperature_profiles`). This makes COP profiles and heat pump efficiencies planning-horizon-dependent. Myopic and perfect foresight modes were adjusted accordingly to update COPs of existing heat pumps in preceding years to adjusted temperatures. * Rearranged workflow to cluster the electricity network before calculating diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a26cd0451..105018f92 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2528,6 +2528,9 @@ def add_biomass(n, costs): unsustainable_solid_biomass_potentials_spatial = biomass_potentials[ "unsustainable solid biomass" ].rename(index=lambda x: x + " unsustainable solid biomass") + unsustainable_liquid_biofuel_potentials_spatial = biomass_potentials[ + "unsustainable bioliquids" + ].rename(index=lambda x: x + " unsustainable bioliquids") else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() @@ -2537,12 +2540,6 @@ def add_biomass(n, costs): unsustainable_solid_biomass_potentials_spatial = biomass_potentials[ "unsustainable solid biomass" ].sum() - - if options["regional_oil_demand"]: - unsustainable_liquid_biofuel_potentials_spatial = biomass_potentials[ - "unsustainable bioliquids" - ].rename(index=lambda x: x + " unsustainable bioliquids") - else: unsustainable_liquid_biofuel_potentials_spatial = biomass_potentials[ "unsustainable bioliquids" ].sum() @@ -2572,18 +2569,15 @@ def add_biomass(n, costs): carrier="municipal solid waste", ) - e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.msw.nodes) - e_max_pu.iloc[-1] = 0 - n.add( - "Store", + "Generator", spatial.msw.nodes, bus=spatial.msw.nodes, carrier="municipal solid waste", - e_nom=msw_biomass_potentials_spatial, + p_nom=msw_biomass_potentials_spatial, marginal_cost=0, # costs.at["municipal solid waste", "fuel"], - e_max_pu=e_max_pu, - e_initial=msw_biomass_potentials_spatial, + e_sum_min=msw_biomass_potentials_spatial, + e_sum_max=msw_biomass_potentials_spatial, ) n.add( @@ -2603,23 +2597,25 @@ def add_biomass(n, costs): ) n.add( - "Store", + "Generator", spatial.gas.biogas, bus=spatial.gas.biogas, carrier="biogas", - e_nom=biogas_potentials_spatial, + p_nom=biogas_potentials_spatial, marginal_cost=costs.at["biogas", "fuel"], - e_initial=biogas_potentials_spatial, + e_sum_min=0, + e_sum_max=biogas_potentials_spatial, ) n.add( - "Store", + "Generator", spatial.biomass.nodes, bus=spatial.biomass.nodes, carrier="solid biomass", - e_nom=solid_biomass_potentials_spatial, + p_nom=solid_biomass_potentials_spatial, marginal_cost=costs.at["solid biomass", "fuel"], - e_initial=solid_biomass_potentials_spatial, + e_sum_min=0, + e_sum_max=solid_biomass_potentials_spatial, ) if options["solid_biomass_import"].get("enable", False): @@ -2671,38 +2667,29 @@ def add_biomass(n, costs): ) if biomass_potentials.filter(like="unsustainable").sum().sum() > 0: - # Create timeseries to force usage of unsustainable potentials - e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.gas.biogas) - e_max_pu.iloc[-1] = 0 - n.add( - "Store", + "Generator", spatial.gas.biogas, suffix=" unsustainable", bus=spatial.gas.biogas, carrier="unsustainable biogas", - e_nom=unsustainable_biogas_potentials_spatial, + p_nom=unsustainable_biogas_potentials_spatial, + p_nom_extendable=False, marginal_cost=costs.at["biogas", "fuel"], - e_initial=unsustainable_biogas_potentials_spatial, - e_nom_extendable=False, - e_max_pu=e_max_pu, - ) - - e_max_pu = pd.DataFrame( - 1, index=n.snapshots, columns=spatial.biomass.nodes_unsustainable + e_sum_min=unsustainable_biogas_potentials_spatial, + e_sum_max=unsustainable_biogas_potentials_spatial, ) - e_max_pu.iloc[-1] = 0 n.add( - "Store", + "Generator", spatial.biomass.nodes_unsustainable, bus=spatial.biomass.nodes, carrier="unsustainable solid biomass", - e_nom=unsustainable_solid_biomass_potentials_spatial, + p_nom=unsustainable_solid_biomass_potentials_spatial, + p_nom_extendable=False, marginal_cost=costs.at["fuelwood", "fuel"], - e_initial=unsustainable_solid_biomass_potentials_spatial, - e_nom_extendable=False, - e_max_pu=e_max_pu, + e_sum_min=unsustainable_solid_biomass_potentials_spatial, + e_sum_max=unsustainable_solid_biomass_potentials_spatial, ) n.add( @@ -2713,21 +2700,16 @@ def add_biomass(n, costs): unit="MWh_LHV", ) - e_max_pu = pd.DataFrame( - 1, index=n.snapshots, columns=spatial.biomass.bioliquids - ) - e_max_pu.iloc[-1] = 0 - n.add( - "Store", + "Generator", spatial.biomass.bioliquids, bus=spatial.biomass.bioliquids, carrier="unsustainable bioliquids", - e_nom=unsustainable_liquid_biofuel_potentials_spatial, + p_nom=unsustainable_liquid_biofuel_potentials_spatial, + p_nom_extendable=False, marginal_cost=costs.at["biodiesel crops", "fuel"], - e_initial=unsustainable_liquid_biofuel_potentials_spatial, - e_nom_extendable=False, - e_max_pu=e_max_pu, + e_sum_min=unsustainable_liquid_biofuel_potentials_spatial, + e_sum_max=unsustainable_liquid_biofuel_potentials_spatial, ) add_carrier_buses(n, "oil") @@ -2848,6 +2830,7 @@ def add_biomass(n, costs): n.add( "Generator", spatial.biomass.nodes, + suffix=" transported", bus=spatial.biomass.nodes, carrier="solid biomass", p_nom=10000, @@ -2866,6 +2849,7 @@ def add_biomass(n, costs): n.add( "Generator", spatial.biomass.nodes_unsustainable, + suffix=" transported", bus=spatial.biomass.nodes, carrier="unsustainable solid biomass", p_nom=10000, @@ -2877,14 +2861,11 @@ def add_biomass(n, costs): ) * average_distance, ) - # Set last snapshot of e_max_pu for unsustainable solid biomass to 1 to make operational limit work - unsus_stores_idx = n.stores.query( - "carrier == 'unsustainable solid biomass'" - ).index - unsus_stores_idx = unsus_stores_idx.intersection( - n.stores_t.e_max_pu.columns - ) - n.stores_t.e_max_pu.loc[n.snapshots[-1], unsus_stores_idx] = 1 + # Set e_sum_min to 0 to allow for the faux biomass transport + n.generators.loc[ + n.generators.carrier == "unsustainable solid biomass", "e_sum_min" + ] = 0 + n.add( "GlobalConstraint", "unsustainable biomass limit", @@ -2899,17 +2880,24 @@ def add_biomass(n, costs): n.add( "Generator", spatial.msw.nodes, + suffix=" transported", bus=spatial.msw.nodes, carrier="municipal solid waste", p_nom=10000, marginal_cost=0 # costs.at["municipal solid waste", "fuel"] - + bus_transport_costs * average_distance, + + bus_transport_costs.rename( + dict(zip(spatial.biomass.nodes, spatial.msw.nodes)) + ) + * average_distance, ) + n.generators.loc[ + n.generators.carrier == "municipal solid waste", "e_sum_min" + ] = 0 n.add( "GlobalConstraint", "msw limit", carrier_attribute="municipal solid waste", - sense="<=", + sense="==", constant=biomass_potentials["municipal solid waste"].sum(), type="operational_limit", ) From 92f0b2a898bc30783eff2458f93ebe210c8ba079 Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:53:45 +0100 Subject: [PATCH 2/3] Correct bioliquid emissions by BtL efficiency (#1410) * correct bioliquid emissions by BtL efficiency * use oil co2 intensity instead * add release note --- doc/release_notes.rst | 3 +++ scripts/prepare_sector_network.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9e615a917..dd6bab8d6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,9 @@ Release Notes Upcoming Release ================ + +* Bugfix: Adjusted efficiency2 (to atmosphere) for bioliquids-to-oil Link in `prepare_sector_network` to exactly offset the corresponding oil emissions. + * Bugfix: Duplicates in build_transmission_projects were caught, but not removed from the network. This is now fixed. * Replaced the Store representation of biogenic carriers (solid biomass, biogas, bioliquids, MSW) in ``prepare_sector_network`` with the extended Generator component that uses the ``e_sum_min`` and ``e_sum_max`` attributes to enforce minimum usage and limit maximum potential, respectively. diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 105018f92..85e83dcfa 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2722,8 +2722,7 @@ def add_biomass(n, costs): bus2="co2 atmosphere", carrier="unsustainable bioliquids", efficiency=1, - efficiency2=-costs.at["solid biomass", "CO2 intensity"] - + costs.at["BtL", "CO2 stored"], + efficiency2=-costs.at["oil", "CO2 intensity"], p_nom=unsustainable_liquid_biofuel_potentials_spatial, marginal_cost=costs.at["BtL", "VOM"], ) From 0c41cec6c407475edd6c617856f926ee3ab4c6c5 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:34:13 +0100 Subject: [PATCH 3/3] update sanitize carriers function (#1400) * add_electrictiy: update sanitize_carriers _helpers: move rename_techs to helpers plot_summary: import rename_techs form helpers plot_power_network: import rename_techs from helpers * revert unintended change in helpers * add_electricity: update sanitize carrier to only use rename_techs if tech_color was not defined * docs: add type hint and docstring to _helpers.rename_techs() * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add release note --------- Co-authored-by: Amos Schledorn Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 2 + scripts/_helpers.py | 87 +++++++++++++++++++++++++++++++++++ scripts/add_electricity.py | 8 +++- scripts/plot_power_network.py | 4 +- scripts/plot_summary.py | 73 +---------------------------- 5 files changed, 99 insertions(+), 75 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index dd6bab8d6..80af0514e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes Upcoming Release ================ +* Improve `sanitize_carrier`` function by filling in colors of missing carriers with colors mapped after using the function `rename_techs`. + * Bugfix: Adjusted efficiency2 (to atmosphere) for bioliquids-to-oil Link in `prepare_sector_network` to exactly offset the corresponding oil emissions. * Bugfix: Duplicates in build_transmission_projects were caught, but not removed from the network. This is now fixed. diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 6c7f1a675..5e86b51c3 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -829,3 +829,90 @@ def get_snapshots(snapshots, drop_leap_day=False, freq="h", **kwargs): time = time[~((time.month == 2) & (time.day == 29))] return time + + +def rename_techs(label: str) -> str: + """ + Rename technology labels for better readability. + + Removes some prefixes and renames if certain conditions defined in function body are met. + + Parameters: + ---------- + label: str + Technology label to be renamed + + Returns: + ------- + str + Renamed label + """ + prefix_to_remove = [ + "residential ", + "services ", + "urban ", + "rural ", + "central ", + "decentral ", + ] + + rename_if_contains = [ + "CHP", + "gas boiler", + "biogas", + "solar thermal", + "air heat pump", + "ground heat pump", + "resistive heater", + "Fischer-Tropsch", + ] + + rename_if_contains_dict = { + "water tanks": "hot water storage", + "retrofitting": "building retrofitting", + # "H2 Electrolysis": "hydrogen storage", + # "H2 Fuel Cell": "hydrogen storage", + # "H2 pipeline": "hydrogen storage", + "battery": "battery storage", + "H2 for industry": "H2 for industry", + "land transport fuel cell": "land transport fuel cell", + "land transport oil": "land transport oil", + "oil shipping": "shipping oil", + # "CC": "CC" + } + + rename = { + "solar": "solar PV", + "Sabatier": "methanation", + "offwind": "offshore wind", + "offwind-ac": "offshore wind (AC)", + "offwind-dc": "offshore wind (DC)", + "offwind-float": "offshore wind (Float)", + "onwind": "onshore wind", + "ror": "hydroelectricity", + "hydro": "hydroelectricity", + "PHS": "hydroelectricity", + "NH3": "ammonia", + "co2 Store": "DAC", + "co2 stored": "CO2 sequestration", + "AC": "transmission lines", + "DC": "transmission lines", + "B2B": "transmission lines", + } + + for ptr in prefix_to_remove: + if label[: len(ptr)] == ptr: + label = label[len(ptr) :] + + for rif in rename_if_contains: + if rif in label: + label = rif + + for old, new in rename_if_contains_dict.items(): + if old in label: + label = new + + for old, new in rename.items(): + if old == label: + label = new + return label diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 15bb987be..3dfffac23 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -122,6 +122,7 @@ from _helpers import ( configure_logging, get_snapshots, + rename_techs, set_scenario_config, update_p_nom_max, ) @@ -202,7 +203,12 @@ def sanitize_carriers(n, config): n.carriers["nice_name"] = n.carriers.nice_name.where( n.carriers.nice_name != "", nice_names ) - colors = pd.Series(config["plotting"]["tech_colors"]).reindex(carrier_i) + + tech_colors = config["plotting"]["tech_colors"] + colors = pd.Series(tech_colors).reindex(carrier_i) + # try to fill missing colors with tech_colors after renaming + missing_colors_i = colors[colors.isna()].index + colors[missing_colors_i] = missing_colors_i.map(rename_techs).map(tech_colors) if colors.isna().any(): missing_i = list(colors.index[colors.isna()]) logger.warning(f"tech_colors for carriers {missing_i} not defined in config.") diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index b8c22c863..69a4e4f9d 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -14,8 +14,8 @@ import matplotlib.pyplot as plt import pandas as pd import pypsa -from _helpers import configure_logging, set_scenario_config -from plot_summary import preferred_order, rename_techs +from _helpers import configure_logging, rename_techs, set_scenario_config +from plot_summary import preferred_order from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches logger = logging.getLogger(__name__) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index ca0e1823d..7c3b08dab 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -11,7 +11,7 @@ import matplotlib.gridspec as gridspec import matplotlib.pyplot as plt import pandas as pd -from _helpers import configure_logging, set_scenario_config +from _helpers import configure_logging, rename_techs, set_scenario_config from prepare_sector_network import co2_emissions_year logger = logging.getLogger(__name__) @@ -19,77 +19,6 @@ # consolidate and rename -def rename_techs(label): - prefix_to_remove = [ - "residential ", - "services ", - "urban ", - "rural ", - "central ", - "decentral ", - ] - - rename_if_contains = [ - "CHP", - "gas boiler", - "biogas", - "solar thermal", - "air heat pump", - "ground heat pump", - "resistive heater", - "Fischer-Tropsch", - ] - - rename_if_contains_dict = { - "water tanks": "hot water storage", - "retrofitting": "building retrofitting", - # "H2 Electrolysis": "hydrogen storage", - # "H2 Fuel Cell": "hydrogen storage", - # "H2 pipeline": "hydrogen storage", - "battery": "battery storage", - "H2 for industry": "H2 for industry", - "land transport fuel cell": "land transport fuel cell", - "land transport oil": "land transport oil", - "oil shipping": "shipping oil", - # "CC": "CC" - } - - rename = { - "solar": "solar PV", - "Sabatier": "methanation", - "offwind": "offshore wind", - "offwind-ac": "offshore wind (AC)", - "offwind-dc": "offshore wind (DC)", - "offwind-float": "offshore wind (Float)", - "onwind": "onshore wind", - "ror": "hydroelectricity", - "hydro": "hydroelectricity", - "PHS": "hydroelectricity", - "NH3": "ammonia", - "co2 Store": "DAC", - "co2 stored": "CO2 sequestration", - "AC": "transmission lines", - "DC": "transmission lines", - "B2B": "transmission lines", - } - - for ptr in prefix_to_remove: - if label[: len(ptr)] == ptr: - label = label[len(ptr) :] - - for rif in rename_if_contains: - if rif in label: - label = rif - - for old, new in rename_if_contains_dict.items(): - if old in label: - label = new - - for old, new in rename.items(): - if old == label: - label = new - return label - preferred_order = pd.Index( [