add option for additional national carbon budget constraints

This commit is contained in:
chrstphtrs 2023-10-18 16:59:49 +02:00
parent 7884392326
commit 9b9090c76c
3 changed files with 118 additions and 0 deletions

View File

@ -84,6 +84,22 @@ co2_budget:
2045: 0.032 2045: 0.032
2050: 0.000 2050: 0.000
co2_budget_national:
2030:
'DE': 0.350
'AT': 0.450
'BE': 0.450
'CH': 0.450
'CZ': 0.450
'DK': 0.450
'FR': 0.450
'GB': 0.450
'LU': 0.450
'NL': 0.450
'NO': 0.450
'PL': 0.450
'SE': 0.450
# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#electricity # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#electricity
electricity: electricity:
voltages: [220., 300., 380.] voltages: [220., 300., 380.]
@ -454,6 +470,7 @@ sector:
hydrogen_turbine: false hydrogen_turbine: false
SMR: true SMR: true
SMR_cc: true SMR_cc: true
co2_budget_national: false
regional_co2_sequestration_potential: regional_co2_sequestration_potential:
enable: false enable: false
attribute: 'conservative estimate Mt' attribute: 'conservative estimate Mt'

View File

@ -88,11 +88,13 @@ rule solve_sector_network_myopic:
co2_sequestration_potential=config["sector"].get( co2_sequestration_potential=config["sector"].get(
"co2_sequestration_potential", 200 "co2_sequestration_potential", 200
), ),
countries=config["countries"],
input: input:
network=RESULTS network=RESULTS
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
costs="data/costs_{planning_horizons}.csv", costs="data/costs_{planning_horizons}.csv",
config=RESULTS + "config.yaml", config=RESULTS + "config.yaml",
co2_totals_name=RESOURCES + "co2_totals.csv",
output: output:
RESULTS RESULTS
+ "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",

View File

@ -41,6 +41,8 @@ logger = logging.getLogger(__name__)
pypsa.pf.logger.setLevel(logging.WARNING) pypsa.pf.logger.setLevel(logging.WARNING)
from pypsa.descriptors import get_switchable_as_dense as get_as_dense from pypsa.descriptors import get_switchable_as_dense as get_as_dense
from prepare_sector_network import emission_sectors_from_opts
def add_land_use_constraint(n, planning_horizons, config): def add_land_use_constraint(n, planning_horizons, config):
if "m" in snakemake.wildcards.clusters: if "m" in snakemake.wildcards.clusters:
@ -762,6 +764,92 @@ def add_pipe_retrofit_constraint(n):
n.model.add_constraints(lhs == rhs, name="Link-pipe_retrofit") n.model.add_constraints(lhs == rhs, name="Link-pipe_retrofit")
def add_co2limit_country(n, config, limit_countries, nyears=1.0):
"""
Add a set of emissions limit constraints for specified countries.
The countries and emissions limits are specified in the config file entry 'co2_budget_country_{investment_year}'.
Parameters
----------
n : pypsa.Network
config : dict
limit_countries : dict
nyears: float, optional
Used to scale the emissions constraint to the number of snapshots of the base network.
"""
logger.info(f"Adding CO2 budget limit for each country as per unit of 1990 levels")
# TODO: n.config (submodule) vs snakemake.config (main module, overwrite/overwritten config)?
# countries = config.countries
# print(config)
countries = ['AT', 'BE', 'CH', 'CZ', 'DE', 'DK', 'FR', 'GB', 'LU', 'NL', 'NO', 'PL', 'SE']
# TODO: import function from prepare_sector_network? Move to common place?
sectors = emission_sectors_from_opts(opts)
# convert Mt to tCO2
co2_totals = 1e6 * pd.read_csv(snakemake.input.co2_totals_name, index_col=0)
co2_limit_countries = co2_totals.loc[countries, sectors].sum(axis=1)
co2_limit_countries = co2_limit_countries.loc[co2_limit_countries.index.isin(limit_countries.keys())]
co2_limit_countries *= co2_limit_countries.index.map(limit_countries) * nyears
p = n.model["Link-p"] # dimension: (time, component)
# NB: Most country-specific links retain their locational information in bus1 (except for DAC, where it is in bus2)
country = n.links.bus1.map(n.buses.location).map(n.buses.country)
country_DAC = (
n.links[n.links.carrier == "DAC"]
.bus2.map(n.buses.location)
.map(n.buses.country)
)
country[country_DAC.index] = country_DAC
lhs = []
for port in [col[3:] for col in n.links if col.startswith("bus")]:
if port == str(0):
efficiency = (
n.links["efficiency"].apply(lambda x: 1.0).rename("efficiency0")
)
elif port == str(1):
efficiency = n.links["efficiency"].rename("efficiency1")
else:
efficiency = n.links[f"efficiency{port}"]
mask = n.links[f"bus{port}"].map(n.buses.carrier).eq("co2")
idx = n.links[mask].index
grouping = country.loc[idx]
if not grouping.isnull().all():
expr = (
(p.loc[:, idx] * efficiency[idx])
.groupby(grouping, axis=1)
.sum()
.sum(dims="snapshot")
)
lhs.append(expr)
lhs = sum(lhs) # dimension: (country)
lhs = lhs.rename({list(lhs.dims.keys())[0]: "country"})
rhs = pd.Series(co2_limit_countries) # dimension: (country)
for ct in lhs.indexes["country"]:
n.model.add_constraints(
lhs.loc[ct] <= rhs[ct],
name=f"GlobalConstraint-co2_limit_per_country{ct}",
)
n.add(
"GlobalConstraint",
f"co2_limit_per_country{ct}",
constant=rhs[ct],
sense="<=",
type="",
)
def extra_functionality(n, snapshots): def extra_functionality(n, snapshots):
""" """
Collects supplementary constraints which will be passed to Collects supplementary constraints which will be passed to
@ -792,6 +880,17 @@ def extra_functionality(n, snapshots):
add_carbon_budget_constraint(n, snapshots) add_carbon_budget_constraint(n, snapshots)
add_retrofit_gas_boiler_constraint(n, snapshots) add_retrofit_gas_boiler_constraint(n, snapshots)
if n.config["sector"]["co2_budget_national"]:
# prepare co2 constraint
nhours = n.snapshot_weightings.generators.sum()
nyears = nhours / 8760
investment_year = int(snakemake.wildcards.planning_horizons[-4:])
limit_countries = snakemake.config["co2_budget_national"][investment_year]
# add co2 constraint for each country
logger.info(f"Add CO2 limit for each country")
add_co2limit_country(n, config, limit_countries, nyears)
def solve_network(n, config, solving, opts="", **kwargs): def solve_network(n, config, solving, opts="", **kwargs):
set_of_options = solving["solver"]["options"] set_of_options = solving["solver"]["options"]