diff --git a/config/config.yaml b/config/config.yaml index 18a70e00..5bd589ad 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,7 +4,7 @@ # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run run: - prefix: 20241011PlotReport + prefix: 20241023-UpstreamEmissions-100H name: # - CurrentPolicies @@ -233,7 +233,7 @@ clustering: # 'ES': 0.0408 # 'IT': 0.0612 temporal: - resolution_sector: 365H + resolution_sector: 100H # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#co2-budget co2_budget: @@ -297,11 +297,9 @@ sector: regional_oil_demand: true #set to true if regional CO2 constraints needed regional_coal_demand: true #set to true if regional CO2 constraints needed gas_network: false - regional_gas_demand: true - H2_retrofit: true biogas_upgrading_cc: true - biomass_to_liquid: true - biomass_to_liquid_cc: true + biomass_to_liquid: false + biomass_to_liquid_cc: false cluster_heat_buses: true # calculated based on ariadne "Stock|Space Heating" # and then 2% of buildings renovated per year to reduce their demand by 80% @@ -632,6 +630,9 @@ must_run_biogas: p_min_pu: 0.6 regions: ['DE'] +emissions_upstream: + enable: true + transmission_projects: new_link_capacity: keep #keep or zero diff --git a/workflow/Snakefile b/workflow/Snakefile index de9ea28d..0bbb828f 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -230,6 +230,7 @@ rule modify_prenetwork: transmission_costs=config_provider("costs", "transmission"), biogas_must_run=config_provider("must_run_biogas"), clustering=config_provider("clustering", "temporal", "resolution_sector"), + emissions_upstream=config_provider("emissions_upstream"), H2_plants=config_provider("electricity", "H2_plants_DE"), land_transport_electric_share=config_provider( "sector", "land_transport_electric_share" diff --git a/workflow/scripts/additional_functionality.py b/workflow/scripts/additional_functionality.py index 2def2e92..24969ce9 100644 --- a/workflow/scripts/additional_functionality.py +++ b/workflow/scripts/additional_functionality.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging +import os import pandas as pd from prepare_sector_network import determine_emission_sectors @@ -8,6 +9,14 @@ logger = logging.getLogger(__name__) +# specific emissions in tons CO2/MWh according to n.links[n.links.carrier =="your_carrier].efficiency2.unique().item() +specific_emissions = { + "oil primary" : 0.2571, + "oil" : 0.2571, + "gas" : 0.198, # OCGT + "coal" : 0.3361, + "lignite" : 0.4069, +} def add_capacity_limits(n, investment_year, limits_capacity, sense="maximum"): @@ -254,22 +263,70 @@ def electricity_import_limits(n, investment_year, limits_volume_max): cname = f"Electricity_import_limit-{ct}" - n.model.add_constraints(lhs <= limit, name=f"GlobalConstraint-{cname}") + n.model.add_constraints( + lhs <= limit, name=f"GlobalConstraint-{cname}" + ) - if cname in n.global_constraints.index: - logger.warning( - f"Global constraint {cname} already exists. Dropping and adding it again." + if cname not in n.global_constraints.index: + n.add( + "GlobalConstraint", + cname, + constant=limit, + sense="<=", + type="", + carrier_attribute="", ) - n.global_constraints.drop(cname, inplace=True) - n.add( - "GlobalConstraint", - cname, - constant=limit, - sense="<=", - type="", - carrier_attribute="", +def emissions_upstream(n): + + logger.info(f"Adding global upstream co2 constraint.") + limit = n.meta["_global_co2_limit"] + + lhs = [] + + for c in specific_emissions.keys(): + + i_fossil = n.generators.index[(n.generators.carrier == c)] + lhs.append((n.model["Generator-p"].loc[:, i_fossil]*specific_emissions[c]*n.snapshot_weightings.generators).sum()) + + # sequestration + i_sequestered = n.links.index[(n.links.carrier == "co2 sequestered")] + lhs.append((-1*n.model["Link-p"].loc[:, i_sequestered]*n.snapshot_weightings.generators).sum()) + + # # process emissions + # i_pe = n.links.index[n.links.carrier == "process emissions"] + # lhs.append((n.model["Link-p"].loc[:, i_pe]*n.snapshot_weightings.generators).sum()) + + # i_pecc = n.links.index[n.links.carrier == "process emissions CC"] + # lhs.append((n.model["Link-p"].loc[:, i_pecc]*n.snapshot_weightings.generators).sum()) + + # # lost oil emissions: this is the hvc sequestered emissions that are not accounted in the downstream constraint + # i_nfi = n.links.index[(n.links.carrier == "naphtha for industry")] + # lhs.append(-1*((n.model["Link-p"].loc[:, i_nfi]*(1-n.links.loc[i_nfi, "efficiency2"])*specific_emissions["oil"]*n.snapshot_weightings.generators).sum())) + + lhs = sum(lhs) + + cname = "CO2LimitUpstream" + + n.model.add_constraints( + lhs <= limit, + name=f"GlobalConstraint-{cname}", + ) + + if cname in n.global_constraints.index: + logger.warning( + f"Global constraint {cname} already exists. Dropping and adding it again." ) + n.global_constraints.drop(cname, inplace=True) + + n.add( + "GlobalConstraint", + cname, + constant=limit, + sense="<=", + type="", + carrier_attribute="", + ) def add_co2limit_country(n, limit_countries, snakemake, debug=False): @@ -284,8 +341,7 @@ def add_co2limit_country(n, limit_countries, snakemake, debug=False): limit_countries : dict snakemake: snakemake object """ - logger.info(f"Adding CO2 budget limit for each country as per unit of 1990 levels") - + logger.info(f"Adding CO2 budget limit for each country as per unit of 1990 levels (downstream)") nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 @@ -296,111 +352,114 @@ def add_co2limit_country(n, limit_countries, snakemake, debug=False): co2_total_totals = co2_totals[sectors].sum(axis=1) * nyears - for ct in limit_countries: - limit = co2_total_totals[ct] * limit_countries[ct] - logger.info( - f"Limiting emissions in country {ct} to {limit_countries[ct]:.1%} of " - f"1990 levels, i.e. {limit:,.2f} tCO2/a", - ) + # functionality if emissions upstream are not enabled + if not snakemake.config["emissions_upstream"]["enable"]: - lhs = [] + for ct in limit_countries: + limit = co2_total_totals[ct] * limit_countries[ct] + logger.info( + f"Limiting emissions in country {ct} to {limit_countries[ct]:.1%} of " + f"1990 levels, i.e. {limit:,.2f} tCO2/a", + ) - for port in [col[3:] for col in n.links if col.startswith("bus")]: + lhs = [] - links = n.links.index[ - (n.links.index.str[:2] == ct) - & (n.links[f"bus{port}"] == "co2 atmosphere") - ] + for port in [col[3:] for col in n.links if col.startswith("bus")]: - logger.info( - f"For {ct} adding following link carriers to port {port} CO2 constraint: {n.links.loc[links,'carrier'].unique()}" - ) + links = n.links.index[ + (n.links.index.str[:2] == ct) + & (n.links[f"bus{port}"] == "co2 atmosphere") + ] - if port == "0": - efficiency = -1.0 - elif port == "1": - efficiency = n.links.loc[links, f"efficiency"] - else: - efficiency = n.links.loc[links, f"efficiency{port}"] + logger.info( + f"For {ct} adding following link carriers to port {port} CO2 constraint: {n.links.loc[links,'carrier'].unique()}" + ) + + if port == "0": + efficiency = -1.0 + elif port == "1": + efficiency = n.links.loc[links, f"efficiency"] + else: + efficiency = n.links.loc[links, f"efficiency{port}"] + + lhs.append( + ( + n.model["Link-p"].loc[:, links] + * efficiency + * n.snapshot_weightings.generators + ).sum() + ) + + # Adding Efuel imports and exports to constraint + incoming_oil = n.links.index[n.links.index == "EU renewable oil -> DE oil"] + outgoing_oil = n.links.index[n.links.index == "DE renewable oil -> EU oil"] + + if not debug: + lhs.append( + ( + -1 + * n.model["Link-p"].loc[:, incoming_oil] + * 0.2571 + * n.snapshot_weightings.generators + ).sum() + ) + lhs.append( + ( + n.model["Link-p"].loc[:, outgoing_oil] + * 0.2571 + * n.snapshot_weightings.generators + ).sum() + ) + + incoming_methanol = n.links.index[n.links.index == "EU methanol -> DE methanol"] + outgoing_methanol = n.links.index[n.links.index == "DE methanol -> EU methanol"] lhs.append( ( - n.model["Link-p"].loc[:, links] - * efficiency + -1 + * n.model["Link-p"].loc[:, incoming_methanol] + / snakemake.config["sector"]["MWh_MeOH_per_tCO2"] * n.snapshot_weightings.generators ).sum() ) - # Adding Efuel imports and exports to constraint - incoming_oil = n.links.index[n.links.index == "EU renewable oil -> DE oil"] - outgoing_oil = n.links.index[n.links.index == "DE renewable oil -> EU oil"] + lhs.append( + ( + n.model["Link-p"].loc[:, outgoing_methanol] + / snakemake.config["sector"]["MWh_MeOH_per_tCO2"] + * n.snapshot_weightings.generators + ).sum() + ) + + # Methane + incoming_CH4 = n.links.index[n.links.index == "EU renewable gas -> DE gas"] + outgoing_CH4 = n.links.index[n.links.index == "DE renewable gas -> EU gas"] - if not debug: lhs.append( ( -1 - * n.model["Link-p"].loc[:, incoming_oil] - * 0.2571 + * n.model["Link-p"].loc[:, incoming_CH4] + * 0.198 * n.snapshot_weightings.generators ).sum() ) + lhs.append( ( - n.model["Link-p"].loc[:, outgoing_oil] - * 0.2571 + n.model["Link-p"].loc[:, outgoing_CH4] + * 0.198 * n.snapshot_weightings.generators ).sum() ) - incoming_methanol = n.links.index[n.links.index == "EU methanol -> DE methanol"] - outgoing_methanol = n.links.index[n.links.index == "DE methanol -> EU methanol"] - - lhs.append( - ( - -1 - * n.model["Link-p"].loc[:, incoming_methanol] - / snakemake.config["sector"]["MWh_MeOH_per_tCO2"] - * n.snapshot_weightings.generators - ).sum() - ) + lhs = sum(lhs) - lhs.append( - ( - n.model["Link-p"].loc[:, outgoing_methanol] - / snakemake.config["sector"]["MWh_MeOH_per_tCO2"] - * n.snapshot_weightings.generators - ).sum() - ) - - # Methane - incoming_CH4 = n.links.index[n.links.index == "EU renewable gas -> DE gas"] - outgoing_CH4 = n.links.index[n.links.index == "DE renewable gas -> EU gas"] - - lhs.append( - ( - -1 - * n.model["Link-p"].loc[:, incoming_CH4] - * 0.198 - * n.snapshot_weightings.generators - ).sum() - ) + cname = f"co2_limit-{ct}" - lhs.append( - ( - n.model["Link-p"].loc[:, outgoing_CH4] - * 0.198 - * n.snapshot_weightings.generators - ).sum() - ) - - lhs = sum(lhs) - - cname = f"co2_limit-{ct}" - - n.model.add_constraints( - lhs <= limit, - name=f"GlobalConstraint-{cname}", - ) + n.model.add_constraints( + lhs <= limit, + name=f"GlobalConstraint-{cname}", + ) if cname in n.global_constraints.index: logger.warning( @@ -417,6 +476,74 @@ def add_co2limit_country(n, limit_countries, snakemake, debug=False): carrier_attribute="", ) + # functionality if emissions upstream are enabled + else: + logger.info(f"Adding CO2 budget limit for each country as per unit of 1990 levels (upstream)") + + for ct in limit_countries: + limit = co2_total_totals[ct]*limit_countries[ct] + logger.info( + f"Limiting emissions in country {ct} to {limit_countries[ct]:.1%} of " + f"1990 levels, i.e. {limit:,.2f} tCO2/a (upstream)", + ) + + lhs = [] + + # generation + for c in specific_emissions.keys(): + i_fossil = n.generators.index[(n.generators.carrier == c) & (n.generators.index.str[:2] == ct)] + lhs.append((n.model["Generator-p"].loc[:, i_fossil]*specific_emissions[c]*n.snapshot_weightings.generators).sum()) + + # sequestration + i_sequestered = n.links.index[(n.links.carrier == "co2 sequestered") & (n.links.index.str[:2] == ct)] + lhs.append((-1*n.model["Link-p"].loc[:, i_sequestered]*n.snapshot_weightings.generators).sum()) + + # process emissions + i_pe = n.links.index[(n.links.carrier == "process emissions") & (n.links.index.str[:2] == ct)] + lhs.append((n.model["Link-p"].loc[:, i_pe]*n.snapshot_weightings.generators).sum()) + + i_pecc = n.links.index[(n.links.carrier == "process emissions CC") & (n.links.index.str[:2] == ct)] + lhs.append((n.model["Link-p"].loc[:, i_pecc]*n.snapshot_weightings.generators).sum()) + + # lost oil emissions: this is the hvc sequestered emissions that are not accounted in the downstream constraint + i_nfi = n.links.index[(n.links.carrier == "naphtha for industry") & (n.links.index.str[:2] == ct)] + lhs.append(-1*((n.model["Link-p"].loc[:, i_nfi]*(1-n.links.loc[i_nfi, "efficiency2"])*specific_emissions["oil"]*n.snapshot_weightings.generators).sum())) + + # trade: import of fossils must be restricted; trade of gas as well + + coal_in = n.links.index[(n.links.bus0 == "EU coal") & (n.links.bus1.str[:2] == ct)] + lhs.append((n.model["Link-p"].loc[:, coal_in]*specific_emissions["coal"]*n.snapshot_weightings.generators).sum()) + + lignite_in = n.links.index[(n.links.bus0 == "EU lignite") & (n.links.bus1.str[:2] == ct)] + lhs.append((n.model["Link-p"].loc[:, lignite_in]*specific_emissions["lignite"]*n.snapshot_weightings.generators).sum()) + + gas_pipe_c = ['gas pipeline', 'gas pipeline new'] + gas_out = n.links.index[(n.links.carrier.isin(gas_pipe_c)) & (n.links.bus0.str[:2] == ct) & (n.links.bus1.str[:2] != ct)] + gas_in = n.links.index[(n.links.carrier.isin(gas_pipe_c)) & (n.links.bus0.str[:2] != ct) & (n.links.bus1.str[:2] == ct)] + + lhs.append((n.model["Link-p"].loc[:, gas_in]*specific_emissions["gas"]*n.snapshot_weightings.generators).sum()) + lhs.append(-1*(n.model["Link-p"].loc[:, gas_out]*specific_emissions["gas"]*n.snapshot_weightings.generators).sum()) + + lhs = sum(lhs) + + cname = f"co2_limit_upstream-{ct}" + + n.model.add_constraints( + lhs <= limit, + name=f"GlobalConstraint-{cname}", + ) + + if cname not in n.global_constraints.index: + n.add( + "GlobalConstraint", + cname, + constant=limit, + sense="<=", + type="", + carrier_attribute="", + ) + + def force_boiler_profiles_existing_per_load(n): """ @@ -634,16 +761,19 @@ def additional_functionality(n, snapshots, snakemake): # force_boiler_profiles_existing_per_load(n) force_boiler_profiles_existing_per_boiler(n) - if isinstance(constraints["co2_budget_national"], dict): - limit_countries = constraints["co2_budget_national"][investment_year] - add_co2limit_country( - n, - limit_countries, - snakemake, - debug=snakemake.config["run"]["debug_co2_limit"], - ) - else: - logger.warning("No national CO2 budget specified!") + # if isinstance(constraints["co2_budget_national"], dict): + # limit_countries = constraints["co2_budget_national"][investment_year] + # add_co2limit_country( + # n, + # limit_countries, + # snakemake, + # debug=snakemake.config["run"]["debug_co2_limit"], + # ) + # else: + # logger.warning("No national CO2 budget specified!") + + if snakemake.config["emissions_upstream"]["enable"]: + emissions_upstream(n) if investment_year == 2020: adapt_nuclear_output(n) diff --git a/workflow/scripts/export_ariadne_variables.py b/workflow/scripts/export_ariadne_variables.py index e1d8d1b6..7d9c342e 100644 --- a/workflow/scripts/export_ariadne_variables.py +++ b/workflow/scripts/export_ariadne_variables.py @@ -3784,6 +3784,14 @@ def get_grid_investments(n, costs, region, length_factor=1.0): def get_policy(n, investment_year): var = pd.Series() + n_glob_co2 = ( + "CO2Limit" if "CO2Limit" in n.global_constraints.index else "CO2LimitUpstream" + ) + n_loc_co2 = ( + "co2_limit-DE" + if "co2_limit-DE" in n.global_constraints.index + else "co2_limit_upstream-DE" + ) # add carbon component to fossil fuels if specified if investment_year in snakemake.params.co2_price_add_on_fossils.keys(): diff --git a/workflow/scripts/modify_prenetwork.py b/workflow/scripts/modify_prenetwork.py index d3d38af6..7d491e74 100644 --- a/workflow/scripts/modify_prenetwork.py +++ b/workflow/scripts/modify_prenetwork.py @@ -837,6 +837,21 @@ def aladin_mobility_demand(n): n.stores.loc[dsm_i].e_nom *= pd.Series(factor.values, index=dsm_i) +def remove_downstream_constraint(n): + """ + Delete current downstream constraint and save global co2 limit in n.meta. + + Parameters: + n (pypsa.Network): The PyPSA network object. + + Returns: + None + """ + logger.info(f"Remove global downstream co2 constraint.") + n.meta["_global_co2_limit"] = n.global_constraints.loc["CO2Limit", "constant"] + n.remove("GlobalConstraint", "CO2Limit") + + def add_hydrogen_turbines(n): """ This adds links that instead of a gas turbine use a hydrogen turbine. @@ -1157,9 +1172,9 @@ def force_connection_nep_offshore(n, current_year): new_boiler_ban(n) - fix_new_boiler_profiles(n) + # fix_new_boiler_profiles(n) - remove_old_boiler_profiles(n) + # remove_old_boiler_profiles(n) coal_generation_ban(n) @@ -1211,6 +1226,9 @@ def force_connection_nep_offshore(n, current_year): ): force_retrofit(n, snakemake.params.H2_plants) + if snakemake.params.emissions_upstream["enable"]: + remove_downstream_constraint(n) + current_year = int(snakemake.wildcards.planning_horizons) enforce_transmission_project_build_years(n, current_year)