diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4b419f1e3..bb37ef1d9 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,10 +10,17 @@ 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: Waste CHPs were added to all electricity buses even if they were not connected to heating network. This is now fixed. * 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/_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( [ diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 56eefd482..2c77e0e97 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_sum_min=unsustainable_biogas_potentials_spatial, + e_sum_max=unsustainable_biogas_potentials_spatial, ) - e_max_pu = pd.DataFrame( - 1, index=n.snapshots, columns=spatial.biomass.nodes_unsustainable - ) - 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") @@ -2740,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"], ) @@ -2848,6 +2829,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 +2848,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 +2860,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 +2879,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", )