|
5 | 5 |
|
6 | 6 | import numpy as np |
7 | 7 | import pypsa |
| 8 | +import pandas as pd |
8 | 9 |
|
9 | 10 | from scripts._helpers import ( |
10 | 11 | configure_logging, |
|
28 | 29 | "OCGT": { |
29 | 30 | "p_min_pu": 0.2, |
30 | 31 | "start_up_cost": 20, |
| 32 | + "shut_down_cost": 20, |
31 | 33 | "min_up_time": 1, |
32 | 34 | "min_down_time": 1, |
33 | 35 | "ramp_limit_up": 1, |
34 | 36 | }, |
35 | 37 | "CCGT": { |
36 | 38 | "p_min_pu": 0.45, |
37 | 39 | "start_up_cost": 80, |
| 40 | + "shut_down_cost": 80, |
38 | 41 | "min_up_time": 3, |
39 | 42 | "min_down_time": 2, |
40 | 43 | "ramp_limit_up": 1, |
41 | 44 | }, |
42 | 45 | "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, |
47 | 51 | "ramp_limit_up": 1, |
48 | 52 | }, |
49 | 53 | "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, |
54 | 59 | "ramp_limit_up": 1, |
55 | 60 | }, |
56 | 61 | "nuclear": { |
57 | 62 | "p_min_pu": 0.5, |
58 | 63 | "start_up_cost": 100, |
| 64 | + "shut_down_cost": 100, |
59 | 65 | "min_up_time": 8, |
60 | 66 | "min_down_time": 10, |
61 | 67 | }, |
62 | 68 | "oil": { |
63 | 69 | "p_min_pu": 0.2, |
64 | 70 | "start_up_cost": 30, |
| 71 | + "shut_down_cost": 30, |
65 | 72 | "min_up_time": 1, |
66 | 73 | "min_down_time": 1, |
67 | 74 | "ramp_limit_up": 1, |
68 | 75 | }, |
69 | 76 | "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, |
74 | 82 | }, |
75 | 83 | } |
76 | 84 |
|
@@ -336,6 +344,140 @@ def get_filtered_links(carrier_list): |
336 | 344 | n.links.loc[links_i, "committable"] = True |
337 | 345 |
|
338 | 346 |
|
| 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 | + |
339 | 481 | def _unfix_bottlenecks(new, deci, name, extendable_i): |
340 | 482 | if name == "links": |
341 | 483 | # Links that have 0-cost and are extendable |
@@ -538,6 +680,14 @@ def fix_capacities(realization, decision, scope="DE", strict=False, no_flex=Fals |
538 | 680 | regions=unit_commitment["regions"], |
539 | 681 | ) |
540 | 682 |
|
| 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 | + |
541 | 691 | if strict: |
542 | 692 | logger.info( |
543 | 693 | "Strict regret run chosen. No capacities are extendable. Activating load shedding to prevent infeasibilites." |
|
0 commit comments