Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions config/config.de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run
run:
prefix: 20260422_small_changes
prefix: 20260710-elec-grid-limitations-test
name:
# - ExPol
- KN2045_Mix
Expand All @@ -15,6 +15,15 @@ run:
# - KN2045_H2
# - KN2045_NFniedrig
# - KN2045_NFhoch
# - KN2045_Mix_no_grid
- KN2045_Mix_transmission_limit_1.0
- KN2045_Mix_transmission_limit_1.1
# - KN2045_Mix_transmission_limit_1.2
# - KN2045_Mix_transmission_limit_1.3
- KN2045_Mix_transmission_limit_DE_1.0
# - KN2045_Mix_transmission_limit_DE_1.1
# - KN2045_Mix_transmission_limit_DE_1.2
- KN2045_Mix_transmission_limit_DE_1.3
scenarios:
enable: true
manual_file: config/scenarios.manual.yaml
Expand Down Expand Up @@ -70,12 +79,13 @@ scenario:
sector_opts:
- none
planning_horizons:
# - 2020
- 2025
- 2030
# - 2030
- 2035
- 2040
# - 2040
- 2045
- 2050
# - 2050

existing_capacities:
grouping_years_power: [1920, 1950, 1955, 1960, 1965, 1970, 1975, 1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2020, 2025]
Expand Down Expand Up @@ -341,7 +351,7 @@ sector:
regional_gas_demand: true
regional_co2_sequestration_potential:
enable: false
H2_retrofit: true
H2_retrofit: false
hydrogen_turbine: false
biogas_upgrading_cc: true
biomass_to_liquid: true
Expand Down Expand Up @@ -378,6 +388,7 @@ sector:
2040: 0.299
2045: 0.149


# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry
industry:
steam_biomass_fraction: 0.4
Expand Down Expand Up @@ -731,3 +742,5 @@ data:
costs:
source: primary
version: latest

remove_electricity_grid: false
5 changes: 5 additions & 0 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ electricity:
enable: false
by_country: false
transmission_limit: vopt
transmission_limit_countries:
enable: false
country: DE
internal: v1.0
interconnector: v1.0

# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#atlite
atlite:
Expand Down
64 changes: 64 additions & 0 deletions config/scenarios.manual.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,67 @@ KN2045_NFhoch:
2040: 0.24
2045: 0.29
2050: 0.33


KN2045_Mix_no_grid:

remove_electricity_grid: true

KN2045_Mix_transmission_limit_1.0:

electricity:
transmission_limit: v1.0

KN2045_Mix_transmission_limit_1.1:

electricity:
transmission_limit: v1.1

KN2045_Mix_transmission_limit_1.2:

electricity:
transmission_limit: v1.2

KN2045_Mix_transmission_limit_1.3:

electricity:
transmission_limit: v1.3

KN2045_Mix_transmission_limit_DE_1.0:
# Like KN2045_Mix_transmission_limit_1.0, but the expansion limit only
# applies to Germany's internal grid and its interconnectors; the rest of
# the European grid stays at the default (unconstrained, vopt).

electricity:
transmission_limit_countries:
enable: true
country: DE
internal: v1.0
interconnector: v1.0

KN2045_Mix_transmission_limit_DE_1.1:

electricity:
transmission_limit_countries:
enable: true
country: DE
internal: v1.1
interconnector: v1.1

KN2045_Mix_transmission_limit_DE_1.2:

electricity:
transmission_limit_countries:
enable: true
country: DE
internal: v1.2
interconnector: v1.2

KN2045_Mix_transmission_limit_DE_1.3:

electricity:
transmission_limit_countries:
enable: true
country: DE
internal: v1.3
interconnector: v1.3
12,160 changes: 12,120 additions & 40 deletions pixi.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ all-tests = {depends-on = ["integration-tests", "clean-tests", "unit-tests"]}
[feature.test.dependencies]
pytest = ">=8.4.2"

[feature.jupyter-kernel.dependencies]
ipykernel = ">=6,<7"

[environments]
doc = { features = ["doc"], no-default-feature = true }
test = ["test"]
jupyter = ["jupyter-kernel"]
3 changes: 3 additions & 0 deletions rules/build_electricity.smk
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,9 @@ rule prepare_network:
autarky=config_provider("electricity", "autarky", default={}),
drop_leap_day=config_provider("enable", "drop_leap_day"),
transmission_limit=config_provider("electricity", "transmission_limit"),
transmission_limit_countries=config_provider(
"electricity", "transmission_limit_countries", default={}
),
message:
"Preparing network for model with {wildcards.clusters} clusters and options {wildcards.opts}"
script:
Expand Down
1 change: 1 addition & 0 deletions rules/pypsa-de/modifications.smk
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ rule modify_prenetwork:
limit_cross_border_flows_ac=config_provider(
"pypsa-de", "limit_cross_border_flows_ac"
),
remove_electricity_grid=config_provider("pypsa-de", "remove_electricity_grid"),
script:
scripts("pypsa-de/modify_prenetwork.py")

Expand Down
54 changes: 54 additions & 0 deletions scripts/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,60 @@ def handle_exception(exc_type, exc_value, exc_traceback):
sys.excepthook = handle_exception


def get_transmission_country_subsets(n: pypsa.Network, country: str) -> dict:
"""
Split AC lines and DC links into three disjoint groups relative to `country`.

Used to apply separate transmission expansion limits to a country's
internal grid and its cross-border interconnectors (see
``electricity.transmission_limit_countries``).

Parameters
----------
n : pypsa.Network
country : str
ISO2 country code (must match values in ``n.buses.country``).

Returns
-------
dict
Keys "internal" (both buses in `country`), "interconnector" (exactly
one bus in `country`) and "rest" (neither bus in `country`). Each maps
to a dict with keys "lines" and "links" (pandas Index).
"""
dc_i = (
n.links.index[n.links.carrier == "DC"] if not n.links.empty else n.links.index
)

lines_c0 = n.lines.bus0.map(n.buses.country)
lines_c1 = n.lines.bus1.map(n.buses.country)
links_c0 = n.links.bus0.map(n.buses.country)
links_c1 = n.links.bus1.map(n.buses.country)

lines_internal = lines_c0.eq(country) & lines_c1.eq(country)
links_internal = links_c0.eq(country) & links_c1.eq(country)

lines_cross_border = lines_c0.eq(country) ^ lines_c1.eq(country)
links_cross_border = links_c0.eq(country) ^ links_c1.eq(country)

return {
"internal": {
"lines": n.lines.index[lines_internal],
"links": dc_i.intersection(n.links.index[links_internal]),
},
"interconnector": {
"lines": n.lines.index[lines_cross_border],
"links": dc_i.intersection(n.links.index[links_cross_border]),
},
"rest": {
"lines": n.lines.index[~(lines_internal | lines_cross_border)],
"links": dc_i.intersection(
n.links.index[~(links_internal | links_cross_border)]
),
},
}


def update_p_nom_max(n):
# if extendable carriers (solar/onwind/...) have capacity >= 0,
# e.g. existing assets from GEM are included to the network,
Expand Down
100 changes: 65 additions & 35 deletions scripts/add_brownfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from scripts._helpers import (
configure_logging,
get_snapshots,
get_transmission_country_subsets,
sanitize_custom_columns,
set_scenario_config,
update_config_from_wildcards,
Expand Down Expand Up @@ -173,7 +174,7 @@ def add_brownfield(
n.links.loc[gas_pipes_i, "p_nom_max"] = remaining_capacity


def disable_grid_expansion_if_limit_hit(n):
def disable_grid_expansion_if_limit_hit(n, transmission_limit_countries=None):
"""
Check if transmission expansion limit is already reached; then turn off.

Expand All @@ -183,45 +184,72 @@ def disable_grid_expansion_if_limit_hit(n):
n.global_constraints. If so, the nominal capacities are set to the
minimum and extendable is turned off; the corresponding global
constraint is then dropped.

This acts as a ratchet across myopic planning horizons: since
``prepare_network.py`` re-adds the same (absolute) limit at every
horizon and brownfield carries forward built capacity as ``s_nom_min``/
``p_nom_min``, expansion is automatically frozen for all later horizons
once a cap is reached, without needing a per-horizon budget.

Parameters
----------
transmission_limit_countries : dict, optional
The ``electricity.transmission_limit_countries`` config. When
enabled, the DE-internal and DE-interconnector limits (added as
custom ``GlobalConstraint`` types by ``prepare_network.py``, see
``scripts.solve_network.add_country_transmission_limit_constraints``)
are each checked against their own line/link subset, while the plain
(unsuffixed) limit is checked only against the remaining lines/links.
"""
types = {"expansion_cost": "capital_cost", "volume_expansion": "length"}
for limit_type in types:
glcs = n.global_constraints.query(f"type == 'transmission_{limit_type}_limit'")

for name, glc in glcs.iterrows():
total_expansion = (
(
n.lines.query("s_nom_extendable")
.eval(f"s_nom_min * {types[limit_type]}")
.sum()
)
+ (
n.links.query("carrier == 'DC' and p_nom_extendable")
.eval(f"p_nom_min * {types[limit_type]}")
.sum()
)
).sum()

# Allow small numerical differences
if np.abs(glc.constant - total_expansion) / glc.constant < 1e-6:
logger.info(
f"Transmission expansion {limit_type} is already reached, disabling expansion and limit"
dc_i = (
n.links.index[n.links.carrier == "DC"] if not n.links.empty else n.links.index
)
country_cfg = transmission_limit_countries or {}
if country_cfg.get("enable", False):
subsets = get_transmission_country_subsets(n, country_cfg["country"])
country = country_cfg["country"].lower()
suffix_scopes = {
"": subsets["rest"],
f"_{country}_internal": subsets["internal"],
f"_{country}_interconnector": subsets["interconnector"],
}
else:
suffix_scopes = {"": {"lines": n.lines.index, "links": dc_i}}

for limit_type, attr in types.items():
for suffix, scope in suffix_scopes.items():
glc_type = f"transmission_{limit_type}_limit{suffix}"
glcs = n.global_constraints.query("type == @glc_type")
if glcs.empty:
continue

ext_lines = n.lines.loc[scope["lines"]].query("s_nom_extendable")
ext_links = n.links.loc[scope["links"]].query("p_nom_extendable")

for name, glc in glcs.iterrows():
total_expansion = (
ext_lines.eval(f"s_nom_min * {attr}").sum()
+ ext_links.eval(f"p_nom_min * {attr}").sum()
)
extendable_acs = n.lines.query("s_nom_extendable").index
n.lines.loc[extendable_acs, "s_nom_extendable"] = False
n.lines.loc[extendable_acs, "s_nom"] = n.lines.loc[
extendable_acs, "s_nom_min"
]

extendable_dcs = n.links.query(
"carrier == 'DC' and p_nom_extendable"
).index
n.links.loc[extendable_dcs, "p_nom_extendable"] = False
n.links.loc[extendable_dcs, "p_nom"] = n.links.loc[
extendable_dcs, "p_nom_min"
]
# Allow small numerical differences
if np.abs(glc.constant - total_expansion) / glc.constant < 1e-6:
logger.info(
f"Transmission expansion {limit_type}{suffix} is already reached, disabling expansion and limit"
)
n.lines.loc[ext_lines.index, "s_nom_extendable"] = False
n.lines.loc[ext_lines.index, "s_nom"] = n.lines.loc[
ext_lines.index, "s_nom_min"
]

n.links.loc[ext_links.index, "p_nom_extendable"] = False
n.links.loc[ext_links.index, "p_nom"] = n.links.loc[
ext_links.index, "p_nom_min"
]

n.global_constraints.drop(name, inplace=True)
n.global_constraints.drop(name, inplace=True)


def adjust_renewable_profiles(n, input_profiles, params, year):
Expand Down Expand Up @@ -386,7 +414,9 @@ def update_dynamic_ptes_capacity(
capacity_threshold=snakemake.params.threshold_capacity,
)

disable_grid_expansion_if_limit_hit(n)
disable_grid_expansion_if_limit_hit(
n, snakemake.config["electricity"].get("transmission_limit_countries")
)

n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards)))

Expand Down
Loading