3939from pathlib import Path
4040
4141import pandas as pd
42- import pypsa
4342
4443from scripts ._helpers import configure_logging , set_scenario_config
4544
@@ -99,6 +98,64 @@ def extract_investment_attributes(excel_path: Path) -> pd.DataFrame:
9998 return agg
10099
101100
101+ def read_tyndp_electricity_buses (
102+ buses_fn : str , col : str , virtual_buses : list [str ] | None = None
103+ ) -> pd .Index :
104+ """
105+ Read node list for electricity from tyndp data input.
106+
107+ Parameters
108+ ----------
109+ - buses_fn (str): Path to a TYNDP node list Excel file ("LIST OF NODES.xlsx"
110+ or offshore hubs "NODE.xlsx")
111+ - col (str): Column which is selected from the dataframe
112+ - virtual_buses (list): list of virtual buses to add, not present in
113+ the raw node list (e.g. added later in build_tyndp_network.py)
114+
115+ Returns
116+ -------
117+ - buses: Index of electricity buses as used in Open-TYNDP
118+
119+ See Also
120+ --------
121+ build_tyndp_network.py : build_buses
122+ build_tyndp_offshore_hubs.py : load_offshore_hubs
123+ """
124+ virtual_buses = virtual_buses if virtual_buses is not None else []
125+
126+ buses = pd .read_excel (buses_fn ).replace ("UK" , "GB" , regex = True ).set_index (col )
127+
128+ if "OFFSHORE_NODE_TYPE" in buses .columns :
129+ # drop radial offshore nodes, which are not built as hub buses
130+ buses = buses [buses .OFFSHORE_NODE_TYPE != "Radial" ]
131+
132+ return buses .index .union (virtual_buses )
133+
134+
135+ def get_existing_buses (buses_fn : str , offshore_buses_fn : str | list ) -> pd .Index :
136+ """
137+ Return the electricity bus universe used to validate CBA project borders.
138+
139+ Offshore hub buses (AC_OH carrier) only get created inside
140+ prepare_sector_network.py, so they can't be read off a built network here
141+ without depending on the (downstream) SB network and creating a cycle
142+ with fix_reference_sb_to_cba's use of clean_projects's own output. They
143+ are instead read directly from the raw offshore hub node list, mirroring
144+ build_tyndp_offshore_hubs.py's own node filtering.
145+ """
146+ existing_buses = read_tyndp_electricity_buses (
147+ buses_fn , col = "NODE" , virtual_buses = ["ITCO" , "ITVI" ]
148+ )
149+
150+ if offshore_buses_fn :
151+ existing_oh_buses = read_tyndp_electricity_buses (
152+ offshore_buses_fn , col = "OFFSHORE_NODE"
153+ )
154+ existing_buses = existing_buses .union (existing_oh_buses )
155+
156+ return existing_buses
157+
158+
102159def apply_offshore_hub_corrections (
103160 hub_corrections_path : Path , projects : pd .DataFrame
104161) -> pd .DataFrame :
@@ -198,10 +255,9 @@ def extract_transmission_projects(
198255
199256 # Several projects have capacities with "Up to ..."
200257 cols = ["p_nom 0->1" , "p_nom 1->0" ]
201- # Several projects have capacities with "Up to ..."
202258 up_to_projects = set ()
203259 for col in cols :
204- up_to = projects [col ].str .startswith ("Up to " )
260+ up_to = projects [col ].str .startswith ("Up to " , na = False )
205261 if up_to .any ():
206262 projects .loc [up_to , col ] = projects .loc [up_to , col ].str [len ("Up to " ) :]
207263 up_to_projects .update (projects .loc [up_to , "project_name" ])
@@ -296,6 +352,7 @@ def build_method_assignments(
296352 return projects .merge (assigned , on = "project_id" , how = "left" )
297353
298354
355+ # %%
299356if __name__ == "__main__" :
300357 if "snakemake" not in globals ():
301358 from scripts ._helpers import mock_snakemake
@@ -308,8 +365,9 @@ def build_method_assignments(
308365 set_scenario_config (snakemake )
309366
310367 # get existing buses
311- n = pypsa .Network (snakemake .input .network )
312- existing_buses = n .buses [n .buses .carrier .isin (["AC" , "AC_OH" ])].index
368+ existing_buses = get_existing_buses (
369+ snakemake .input .buses , snakemake .input .offshore_buses
370+ )
313371
314372 excel_path = Path (snakemake .input .dir ) / "20250312_export_transmission.xlsx"
315373 hub_corrections_path = snakemake .input .offshore_hub_corrections
0 commit comments