Skip to content

Commit b75c16a

Browse files
authored
Merge branch 'master' into feat/775-upstream-retrieves
2 parents 9452bfe + bcd10e3 commit b75c16a

6 files changed

Lines changed: 171 additions & 31 deletions

File tree

doc/release_notes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
**Features**
1111

12+
* Merge low voltage and market nodes in the CBA ([#722](https://github.com/open-energy-transition/open-tyndp/pull/722)).
13+
1214
* Add Snakemake rules to launch the `PyPSA-Explorer` with pre-solved SB networks from previous releases ([#724](https://github.com/open-energy-transition/open-tyndp/pull/724)).
1315

1416
**Changes**
@@ -41,6 +43,8 @@
4143

4244
* Extend `tyndp-archive` support to integrate three new datasets (`desnz_electricity_consumption`, `jrc_energy_atlas`, `ons_lad`) and two new versions (`nitrogen_statistics`, `synthetic_electricity_demand`) ([#758](https://github.com/open-energy-transition/open-tyndp/pull/758)).
4345

46+
* Restore correct assigned countries for virtual TYNDP nodes ([#794](https://github.com/open-energy-transition/open-tyndp/pull/794)).
47+
4448

4549
**Documentation**
4650

@@ -72,6 +76,8 @@
7276

7377
* Disentangle the `data/versions.csv` file by introducing `data/tyndp_versions.csv` ([#788](https://github.com/open-energy-transition/open-tyndp/pull/788)). All the TYNDP-specific version entries, as well as the `tyndp-archive` entries, are now tracked in this dedicated file.
7478

79+
* Fix quoting patterns for clarity ([#791](https://github.com/open-energy-transition/open-tyndp/pull/791)).
80+
7581
* Use `combine_indicators` instead of `collect_indicators` in the function `summary_benchmark_indicators` ([#797](https://github.com/open-energy-transition/open-tyndp/pull/797)).
7682

7783
* Reduce dependency on upstream retrieves ([#798](https://github.com/open-energy-transition/open-tyndp/pull/798)).

rules/cba.smk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ if (CBA_NON_CO2_DATASET := dataset_version("tyndp_cba_non_co2_emissions"))[
5353
input:
5454
file=storage(CBA_NON_CO2_DATASET["url"]),
5555
output:
56-
file=f"{CBA_NON_CO2_DATASET["folder"]}/a.3_non-co2-emissions.csv",
56+
file=f"{CBA_NON_CO2_DATASET['folder']}/a.3_non-co2-emissions.csv",
5757
log:
5858
"logs/retrieve_tyndp_cba_non_co2_emissions.log",
5959
run:

rules/sb.smk

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ if (PECD_DATASET := dataset_version("tyndp_pecd"))["source"] in ARCHIVE_SOURCES:
1515
rule retrieve_tyndp_pecd:
1616
input:
1717
zip_file=storage(
18-
PECD_DATASET["url"] + f"PECD_{PECD_DATASET["version"]}.zip"
18+
PECD_DATASET["url"] + f"PECD_{PECD_DATASET['version']}.zip"
1919
),
2020
output:
2121
dir=directory(PECD_DATASET["folder"]),
@@ -34,9 +34,9 @@ if (VIS_PLFM_DATASET := dataset_version("tyndp_vis_plfm"))["source"] in ARCHIVE_
3434
zip_file=storage(VIS_PLFM_DATASET["url"]),
3535
output:
3636
dir=directory(VIS_PLFM_DATASET["folder"]),
37-
elec_demand=f"{VIS_PLFM_DATASET["folder"]}/250117_TYNDP2024Scenarios_Electricity_Demand.xlsx",
38-
elec_flex=f"{VIS_PLFM_DATASET["folder"]}/250117_TYNDP2024Scenarios_Electricity_Flexibility.xlsx",
39-
elec_supply=f"{VIS_PLFM_DATASET["folder"]}/250117_TYNDP2024Scenarios_Electricity_SupplyMix.xlsx",
37+
elec_demand=f"{VIS_PLFM_DATASET['folder']}/250117_TYNDP2024Scenarios_Electricity_Demand.xlsx",
38+
elec_flex=f"{VIS_PLFM_DATASET['folder']}/250117_TYNDP2024Scenarios_Electricity_Flexibility.xlsx",
39+
elec_supply=f"{VIS_PLFM_DATASET['folder']}/250117_TYNDP2024Scenarios_Electricity_SupplyMix.xlsx",
4040
log:
4141
"logs/retrieve_tyndp_vp_data.log",
4242
run:
@@ -55,8 +55,8 @@ if (NUC_PROFILES := dataset_version("tyndp_nuclear_profiles"))[
5555
zip_file=storage(NUC_PROFILES["url"]),
5656
output:
5757
dir=directory(NUC_PROFILES["folder"]),
58-
nuclear_p_max_pu_2030=f"{NUC_PROFILES["folder"]}/nuclear_p_max_pu_2030.csv",
59-
nuclear_p_max_pu_2040=f"{NUC_PROFILES["folder"]}/nuclear_p_max_pu_2040.csv",
58+
nuclear_p_max_pu_2030=f"{NUC_PROFILES['folder']}/nuclear_p_max_pu_2030.csv",
59+
nuclear_p_max_pu_2040=f"{NUC_PROFILES['folder']}/nuclear_p_max_pu_2040.csv",
6060
log:
6161
"logs/retrieve_tyndp_nuclear_profiles.log",
6262
run:
@@ -120,7 +120,7 @@ rule retrieve_countries_centroids:
120120
if not "pre-built" in PECD_DATASET["version"]:
121121

122122
def get_pecd_prebuilt_version(increment_minor=True):
123-
prebuilt_prefix = f"{PECD_DATASET["version"]}+pre-built."
123+
prebuilt_prefix = f"{PECD_DATASET['version']}+pre-built."
124124
versions = (
125125
dataset_version("tyndp_pecd", all_versions=True)
126126
.query("version.str.contains(@prebuilt_prefix, regex=False)")
@@ -142,7 +142,7 @@ if not "pre-built" in PECD_DATASET["version"]:
142142
pecd_raw=PECD_DATASET["folder"],
143143
output:
144144
pecd_prebuilt=directory(
145-
f"{PECD_DATASET["folder"]}+pre-built.{get_pecd_prebuilt_version(increment_minor= True)}"
145+
f"{PECD_DATASET['folder']}+pre-built.{get_pecd_prebuilt_version(increment_minor= True)}"
146146
),
147147
log:
148148
"logs/prepare_pecd_release.log",

scripts/base_network.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
get_snapshots,
4343
set_scenario_config,
4444
)
45+
from scripts.build_tyndp_network import AC_VIRTUAL_NODES_IT
4546

4647
PD_GE_2_2 = parse(pd.__version__) >= Version("2.2")
4748

@@ -560,6 +561,25 @@ def prefer_voltage(x, which):
560561
return buses
561562

562563

564+
def _restore_virtual_node_countries_tyndp(
565+
n: pypsa.Network, buses: pd.DataFrame
566+
) -> None:
567+
"""
568+
Restore the declared country of TYNDP virtual nodes instead of coordinate-based country assignment
569+
from `_set_countries_and_substations`.
570+
571+
Parameters
572+
----------
573+
n : pypsa.Network
574+
Network whose `buses` country assignment is corrected in place.
575+
buses : pd.DataFrame
576+
Buses as loaded in `_load_buses`, holding the countries declared in
577+
build_tyndp_network before the coordinate-based reassignment.
578+
"""
579+
nodes = n.buses.index.intersection(AC_VIRTUAL_NODES_IT)
580+
n.buses.loc[nodes, "country"] = buses.loc[nodes, "country"]
581+
582+
563583
def _replace_b2b_converter_at_country_border_by_link(n):
564584
# Affects only the B2B converter in Lithuania at the Polish border at the moment
565585
buscntry = n.buses.country
@@ -752,6 +772,8 @@ def base_network(
752772

753773
_set_countries_and_substations(n, config, country_shapes, offshore_shapes)
754774

775+
_restore_virtual_node_countries_tyndp(n, buses)
776+
755777
_set_links_underwater_fraction(n, offshore_shapes)
756778

757779
_replace_b2b_converter_at_country_border_by_link(n)

scripts/build_tyndp_network.py

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
"UK": "GB",
7979
}
8080

81+
AC_VIRTUAL_NODES_IT = {
82+
"ITCO": "FR15",
83+
"ITVI": "ITSI",
84+
}
85+
8186
IBFI_COORD = (63.0, 25.0)
8287

8388

@@ -223,6 +228,41 @@ def build_shapes(
223228
return bidding_shapes, country_shapes
224229

225230

231+
def _add_virtual_node(
232+
target_gdf: gpd.GeoDataFrame,
233+
new_bus: str,
234+
ref_bus: str,
235+
source_gdf: gpd.GeoDataFrame | None = None,
236+
**overrides,
237+
) -> None:
238+
"""
239+
Add a virtual node to target Dataframe as a copy of an existing reference bus.
240+
241+
The virtual node inherits every attribute of the reference bus and takes its
242+
own name as ``station_id`` and ``tags``. Any remaining attribute is set
243+
through ``overrides``.
244+
245+
Parameters
246+
----------
247+
target_gdf : gpd.GeoDataFrame
248+
Bus GeoDataFrame the virtual node is appended to, modified in place.
249+
new_bus : str
250+
Name of the virtual node.
251+
ref_bus : str
252+
Name of the reference bus whose attributes are copied.
253+
source_gdf : gpd.GeoDataFrame, optional
254+
Bus GeoDataFrame holding the reference bus. Defaults to None in which case ``target_gdf`` is used.
255+
**overrides, optional
256+
Attribute values overriding those inherited from the reference bus.
257+
"""
258+
source_gdf = target_gdf if source_gdf is None else source_gdf
259+
target_gdf.loc[new_bus] = (
260+
source_gdf.loc[[ref_bus]]
261+
.assign(station_id=new_bus, tags=new_bus, **overrides)
262+
.loc[ref_bus]
263+
)
264+
265+
226266
def build_buses(
227267
buses_fn: str,
228268
countries: list[str],
@@ -281,14 +321,10 @@ def build_buses(
281321

282322
# Manually add Italian virtual nodes # TODO Refine assumptions
283323
if "IT" in countries:
284-
buses.loc["ITCO"] = (
285-
buses.loc[["FR15"]]
286-
.assign(station_id="ITCO", country="IT", tags="ITCO")
287-
.loc["FR15"]
288-
)
289-
buses.loc["ITVI"] = (
290-
buses.loc[["ITSI"]].assign(station_id="ITVI", tags="ITVI").loc["ITSI"]
291-
)
324+
for node, location in AC_VIRTUAL_NODES_IT.items():
325+
_add_virtual_node(
326+
target_gdf=buses, new_bus=node, ref_bus=location, country="IT"
327+
)
292328

293329
buses_h2 = (
294330
country_shapes[["node", "x", "y"]]
@@ -309,23 +345,23 @@ def build_buses(
309345

310346
# Manually add IBIT and IBFI nodes # TODO Refine assumptions
311347
if "IT" in countries:
312-
buses_h2.loc["IBIT H2"] = (
313-
buses.loc[["ITN1"]]
314-
.assign(station_id="IBIT H2", voltage=None, dc="f", tags="IBIT H2")
315-
.loc["ITN1"]
348+
_add_virtual_node(
349+
target_gdf=buses_h2,
350+
new_bus="IBIT H2",
351+
ref_bus="ITN1",
352+
source_gdf=buses,
353+
voltage=None,
354+
dc="f",
316355
)
317356
if "FI" in countries:
318357
ibfi_lat, ibfi_long = IBFI_COORD
319-
buses_h2.loc["IBFI H2"] = (
320-
buses_h2.loc[["FI H2"]]
321-
.assign(
322-
station_id="IBFI H2",
323-
tags="IBFI H2",
324-
x=ibfi_long,
325-
y=ibfi_lat,
326-
geometry=Point(ibfi_long, ibfi_lat),
327-
)
328-
.loc["FI H2"]
358+
_add_virtual_node(
359+
target_gdf=buses_h2,
360+
new_bus="IBFI H2",
361+
ref_bus="FI H2",
362+
x=ibfi_long,
363+
y=ibfi_lat,
364+
geometry=Point(ibfi_long, ibfi_lat),
329365
)
330366

331367
return buses, buses_h2

scripts/cba/simplify_sb_network.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Fixed optimal capacities from scenario building
99
- Hurdle costs on DC links
1010
- Extended primary fuel source capacities
11+
- Merge low and high voltage buses
1112
1213
**Inputs**
1314
@@ -62,6 +63,78 @@ def extend_primary_fuel_sources(
6263
n.generators.loc[gen_i, "p_nom"] = inf
6364

6465

66+
def move_bus_carrier_and_cleanup(
67+
n: pypsa.Network,
68+
from_carrier: str = "low voltage",
69+
busmap: pd.Series | None = None,
70+
) -> None:
71+
"""
72+
Reassign all components attached to buses of `from_carrier` onto their
73+
corresponding buses (as given by `busmap`), remove the now-empty
74+
`from_carrier` buses, and drop any lines/links that end up with
75+
bus0 == bus1 as a result.
76+
77+
Also drops redundant load-shedding generators sitting on `from_carrier`
78+
buses, since after reassignment they would duplicate the load-shedding
79+
generator already present at the target bus.
80+
81+
Parameters
82+
----------
83+
n : pypsa.Network
84+
Network to modify in place.
85+
from_carrier : str, default "low voltage"
86+
Bus carrier to remove (e.g. "low voltage").
87+
busmap : pandas.Series, optional
88+
Mapping of `from_carrier` bus name -> target bus name onto which its
89+
components should be reassigned. Defaults to `None`, in which case
90+
each bus is mapped onto the bus matching its `location` field, i.e.
91+
`n.buses.loc[n.buses.carrier == from_carrier, "location"]` is used.
92+
93+
Returns
94+
-------
95+
None
96+
Network is modified in place.
97+
"""
98+
# create busmap
99+
if busmap is None:
100+
busmap = n.buses.loc[n.buses.carrier == from_carrier, "location"]
101+
# check if buses exist in network
102+
if not busmap.isin(n.buses.index).all():
103+
missing = busmap[~busmap.isin(n.buses.index)]
104+
raise ValueError(f"Locations not found in n.buses.index: {missing.tolist()}")
105+
106+
# remove load shedding generators on low voltage buses to avoid having two
107+
# load shedding generators at market node
108+
load_shedding_gens = n.generators.index[
109+
(n.generators.bus.map(n.buses.carrier) == from_carrier)
110+
& (n.generators.carrier == "load")
111+
]
112+
n.remove("Generator", load_shedding_gens)
113+
114+
# reassign every component attached to the from_carrier buses
115+
for c in n.components[sorted(n.branch_components | n.one_port_components)]:
116+
static = c.static
117+
if static.empty:
118+
continue
119+
for port in c.ports:
120+
col = f"bus{port}"
121+
mask = static[col].isin(busmap.index)
122+
if mask.any():
123+
static.loc[mask, col] = static.loc[mask, col].map(busmap)
124+
125+
# remove carrier buses
126+
n.remove("Bus", busmap.index)
127+
128+
# drop branch components (e.g. lines/links) where bus0 == bus1 after remapping
129+
for c in n.components[sorted(n.branch_components)]:
130+
static = c.static
131+
if static.empty:
132+
continue
133+
to_remove = static.index[static.bus0 == static.bus1]
134+
if len(to_remove):
135+
n.remove(c.name, to_remove)
136+
137+
65138
if __name__ == "__main__":
66139
if "snakemake" not in globals():
67140
from scripts._helpers import mock_snakemake
@@ -96,6 +169,9 @@ def extend_primary_fuel_sources(
96169
# TODO: for DE/GA add merging of the two H2 zones
97170
# TODO: for DE/GA add EV electricity consumption from SB as fixed demand
98171

172+
# merge low voltage to market node
173+
move_bus_carrier_and_cleanup(n, from_carrier="low voltage")
174+
99175
# Save base network
100176
n.export_to_netcdf(snakemake.output.network)
101177
logger.info(f"Saved CBA base network to {snakemake.output.network}")

0 commit comments

Comments
 (0)