Skip to content

Commit 29daaf8

Browse files
committed
unit commitment and scaling of cross border elec capa
1 parent 345695a commit 29daaf8

File tree

3 files changed

+168
-14
lines changed

3 files changed

+168
-14
lines changed

Snakefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,9 @@ rule prepare_regret_network:
10001000
unit_commitment=config_provider(
10011001
"iiasa_database", "regret_run", "unit_commitment"
10021002
),
1003+
scale_cross_border_elec_capa=config_provider(
1004+
"iiasa_database", "regret_run", "scale_cross_border_elec_capa"
1005+
),
10031006
input:
10041007
decision=RESULTS.replace("{run}", "{decision}")
10051008
+ "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}.nc",

config/config.de.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ iiasa_database:
5555
unit_commitment:
5656
enable: false
5757
params: custom # options: conservative, average, optimistic, custom
58-
carriers: ["OCGT", "coal", "lignite", "urban central solid biomass CHP"] # subset of ["OCGT", "CCGT", "coal", "lignite", "nuclear", "oil","urban central solid biomass CHP"]
59-
regions: ["DE"]
58+
carriers: ["OCGT", "CCGT", "coal", "lignite", "nuclear", "oil", "urban central solid biomass CHP"] # subset of ["OCGT", "CCGT", "coal", "lignite", "nuclear", "oil","urban central solid biomass CHP"]
59+
regions: ['DE'] # subset of ['AT', 'BE', 'CH', 'CZ', 'DE', 'DK', 'FR', 'GB', 'LU', 'NL', 'NO', 'PL', 'SE', 'ES', 'IT']
60+
scale_cross_border_elec_capa: false # If true, scales cross-border electricity capacities to target values given in prepare_regret_network.py
6061
ageb_for_mobility: true # In 2020 use AGEB data for final energy demand and KBA for vehicles
6162
uba_for_mobility: # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025
6263
- 2025

scripts/pypsa-de/prepare_regret_network.py

Lines changed: 162 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import numpy as np
77
import pypsa
8+
import pandas as pd
89

910
from scripts._helpers import (
1011
configure_logging,
@@ -28,49 +29,56 @@
2829
"OCGT": {
2930
"p_min_pu": 0.2,
3031
"start_up_cost": 20,
32+
"shut_down_cost": 20,
3133
"min_up_time": 1,
3234
"min_down_time": 1,
3335
"ramp_limit_up": 1,
3436
},
3537
"CCGT": {
3638
"p_min_pu": 0.45,
3739
"start_up_cost": 80,
40+
"shut_down_cost": 80,
3841
"min_up_time": 3,
3942
"min_down_time": 2,
4043
"ramp_limit_up": 1,
4144
},
4245
"coal": {
43-
"p_min_pu": 0.325,
44-
"start_up_cost": 60,
45-
"min_up_time": 6,
46-
"min_down_time": 6,
46+
"p_min_pu": 0.5,
47+
"start_up_cost": 200,
48+
"shut_down_cost": 200,
49+
"min_up_time": 24,
50+
"min_down_time": 24,
4751
"ramp_limit_up": 1,
4852
},
4953
"lignite": {
50-
"p_min_pu": 0.325,
51-
"start_up_cost": 80,
52-
"min_up_time": 10,
53-
"min_down_time": 10,
54+
"p_min_pu": 0.5,
55+
"start_up_cost": 200,
56+
"shut_down_cost": 200,
57+
"min_up_time": 24,
58+
"min_down_time": 24,
5459
"ramp_limit_up": 1,
5560
},
5661
"nuclear": {
5762
"p_min_pu": 0.5,
5863
"start_up_cost": 100,
64+
"shut_down_cost": 100,
5965
"min_up_time": 8,
6066
"min_down_time": 10,
6167
},
6268
"oil": {
6369
"p_min_pu": 0.2,
6470
"start_up_cost": 30,
71+
"shut_down_cost": 30,
6572
"min_up_time": 1,
6673
"min_down_time": 1,
6774
"ramp_limit_up": 1,
6875
},
6976
"urban central solid biomass CHP": {
70-
"p_min_pu": 0.38,
71-
"start_up_cost": 50,
72-
"min_up_time": 2,
73-
"min_down_time": 2,
77+
"p_min_pu": 0.5,
78+
"start_up_cost": 150,
79+
"shut_down_cost": 150,
80+
"min_up_time": 5,
81+
"min_down_time": 5,
7482
},
7583
}
7684

@@ -336,6 +344,140 @@ def get_filtered_links(carrier_list):
336344
n.links.loc[links_i, "committable"] = True
337345

338346

347+
target_caps = {
348+
"GB": 0,
349+
"CH": 8000,
350+
"CZ": 5500,
351+
"NL": 8000,
352+
"FR": 9500,
353+
"PL": 4500,
354+
"NO": 2500,
355+
"BE": 0,
356+
"DK": 6000,
357+
"AT": 6000,
358+
"LU": 1500,
359+
"SE": 1000,
360+
}
361+
362+
363+
def scale_transmission_capacity(n, target_capacities):
364+
"""
365+
Scale transmission capacities in PyPSA network to match target values.
366+
367+
Parameters
368+
----------
369+
n : pypsa.Network
370+
PyPSA network object
371+
target_capacities : pd.Series or dict
372+
Target transmission capacities by country in MW (already rounded)
373+
"""
374+
375+
if isinstance(target_capacities, dict):
376+
target_capacities = pd.Series(target_capacities)
377+
378+
# Calculate current capacities (without reversed links)
379+
countries_ac = ["AT", "CH", "CZ", "DK", "FR", "LU", "NL", "PL"]
380+
countries_dc = ["BE", "FR", "GB", "DK", "SE", "NO", "CH"]
381+
382+
current_capa = pd.DataFrame(
383+
data=0,
384+
index=list(set(countries_ac + countries_dc)),
385+
columns=["AC_MW", "DC_MW", "Total_MW"],
386+
)
387+
388+
# AC lines
389+
for ct in countries_ac:
390+
ac_lines = n.lines # No need to filter out reversed for AC lines
391+
ind = ac_lines[
392+
(ac_lines.bus0.str.startswith("DE") & ac_lines.bus1.str.startswith(ct))
393+
| (ac_lines.bus0.str.startswith(ct) & ac_lines.bus1.str.startswith("DE"))
394+
].index
395+
current_capa.loc[ct, "AC_MW"] = n.lines.loc[ind, "s_nom_opt"].sum()
396+
397+
# DC links
398+
for ct in countries_dc:
399+
dc_links = n.links[
400+
n.links.carrier.isin(["DC"]) & ~n.links.index.str.contains("reversed")
401+
]
402+
ind = dc_links[
403+
(dc_links.bus0.str.startswith("DE") & dc_links.bus1.str.startswith(ct))
404+
| (dc_links.bus0.str.startswith(ct) & dc_links.bus1.str.startswith("DE"))
405+
].index
406+
current_capa.loc[ct, "DC_MW"] = n.links.loc[ind, "p_nom_opt"].sum()
407+
408+
current_capa["Total_MW"] = current_capa["AC_MW"] + current_capa["DC_MW"]
409+
410+
# Calculate scaling factors
411+
for country in target_capacities.index:
412+
if country in current_capa.index:
413+
current_total = current_capa.loc[country, "Total_MW"]
414+
target_total = target_capacities[country]
415+
416+
if current_total > 0:
417+
scaling_factor = target_total / current_total
418+
419+
print(
420+
f"{country}: {current_total:.0f} MW -> {target_total:.0f} MW (factor: {scaling_factor:.3f})"
421+
)
422+
423+
# Scale AC lines (no reversed links for AC)
424+
if current_capa.loc[country, "AC_MW"] > 0:
425+
ac_lines = n.lines # No need to filter out reversed for AC lines
426+
ac_ind = ac_lines[
427+
(
428+
ac_lines.bus0.str.startswith("DE")
429+
& ac_lines.bus1.str.startswith(country)
430+
)
431+
| (
432+
ac_lines.bus0.str.startswith(country)
433+
& ac_lines.bus1.str.startswith("DE")
434+
)
435+
].index
436+
437+
# Scale AC lines
438+
n.lines.loc[ac_ind, "s_nom_opt"] *= scaling_factor
439+
n.lines.loc[ac_ind, "s_nom"] *= scaling_factor
440+
441+
# Scale DC links
442+
if current_capa.loc[country, "DC_MW"] > 0:
443+
dc_links = n.links[
444+
n.links.carrier.isin(["DC"])
445+
& ~n.links.index.str.contains("reversed")
446+
]
447+
dc_ind = dc_links[
448+
(
449+
dc_links.bus0.str.startswith("DE")
450+
& dc_links.bus1.str.startswith(country)
451+
)
452+
| (
453+
dc_links.bus0.str.startswith(country)
454+
& dc_links.bus1.str.startswith("DE")
455+
)
456+
].index
457+
458+
# Scale main DC links
459+
n.links.loc[dc_ind, "p_nom_opt"] *= scaling_factor
460+
n.links.loc[dc_ind, "p_nom"] *= scaling_factor
461+
462+
# Scale reversed DC links to match
463+
for link_id in dc_ind:
464+
reversed_id = link_id + "-reversed"
465+
if reversed_id in n.links.index:
466+
n.links.loc[reversed_id, "p_nom_opt"] = n.links.loc[
467+
link_id, "p_nom_opt"
468+
]
469+
n.links.loc[reversed_id, "p_nom"] = n.links.loc[
470+
link_id, "p_nom"
471+
]
472+
473+
elif target_total > 0:
474+
logger.info(
475+
f"WARNING: {country} has target capacity {target_total:.0f} MW but no current capacity to scale"
476+
)
477+
else:
478+
logger.info(f"{country}: Target is 0 MW - no scaling needed")
479+
480+
339481
def _unfix_bottlenecks(new, deci, name, extendable_i):
340482
if name == "links":
341483
# Links that have 0-cost and are extendable
@@ -538,6 +680,14 @@ def fix_capacities(realization, decision, scope="DE", strict=False, no_flex=Fals
538680
regions=unit_commitment["regions"],
539681
)
540682

683+
scale_cross_border_elec_capa = snakemake.params.get(
684+
"scale_cross_border_elec_capa", False
685+
)
686+
687+
if scale_cross_border_elec_capa:
688+
logger.info("Scaling cross-border electricity capacities to target values.")
689+
scale_transmission_capacity(n, target_caps)
690+
541691
if strict:
542692
logger.info(
543693
"Strict regret run chosen. No capacities are extendable. Activating load shedding to prevent infeasibilites."

0 commit comments

Comments
 (0)