diff --git a/data/transmission_projects/manual/new_links.csv b/data/transmission_projects/manual/new_links.csv index 80d2768c9a..b8270470c0 100644 --- a/data/transmission_projects/manual/new_links.csv +++ b/data/transmission_projects/manual/new_links.csv @@ -16,5 +16,6 @@ TYNDP2024_1197,Hornsea (UK),Moerdijk (NL),460,1200,under_consideration,2031,"{na TYNDP2024_1210,La Farga (ES),La Spezia (IT),725,2091,under_consideration,2032,"{name:APOLLO-LINK,url:{name:TYNDP2024_1210,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",2.883,42.062,9.884,44.107 TYNDP2024_1211,Lubmin (DE),Sindi (EE),960,2000,under_consideration,2037,"{name:Baltic WindConnector (BWC),url:{name:TYNDP2024_1211,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",13.686,54.139,24.671903,58.429161 TYNDP2024_1216,Constanta (RO),Albertirsa (HU),930,2500,under_consideration,2030,"{name:High-Voltage Direct Current Interconnector Project Romania-Hungary,url:{name:TYNDP2024_1216,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",28.588,44.201,19.584,47.224 -TYNDP2024_1229,Ovelgönne (DE),Marxheim (DE),433,4000,in_permitting,2035,"{name:Rhine-Main-Link,url:{name:TYNDP2024_1229,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",8.379,53.315,8.435,50.078 +TYNDP2024_1229_NEP_DC34,Ovelgönne (DE),Bürstadt (DE),523,2000,in_permitting,2033,"{name:Rhine-Main-Link,url:{name:TYNDP2024_1229,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",8.4209977,53.3427799,8.4747076,49.6531823 +TYNDP2024_1229_NEP_DC35,Ovelgönne (DE),Marxheim (DE),461,2000,in_permitting,2035,"{name:Rhine-Main-Link,url:{name:TYNDP2024_1229,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",8.4209977,53.3427799,8.4320722,50.0724011 TYNDP2024_1231,Arachthos (GR),Ottenhofen (DE),600,3000,under_consideration,2035,"{name:Green Aegean Interconnector,url:{name:TYNDP2024_1231,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",20.967,39.185,11.868,48.207 diff --git a/doc/release_notes.md b/doc/release_notes.md index a14921cf77..582c06ddef 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -6,6 +6,8 @@ +* Fix: Removed a duplicate tranmission project between NEP and TYNDP sources; improved the matching algorithm for duplicate projects + * Feature: New switches `industry: solid_biomass_for_industry_cc`, `industry: gas_for_industry_cc` and `industry: process_emissions_cc` to disable the carbon capture options for solid biomass for industry, gas for industry and industrial process emissions. All default to `true`, which matches the previous behaviour ([#2357](https://github.com/PyPSA/pypsa-eur/pull/2357)). * Feature: New option `sector: electrolysis` to allow hydrogen electrolysis to be configurable ([#2358](https://github.com/PyPSA/pypsa-eur/pull/2358)). diff --git a/scripts/build_transmission_projects.py b/scripts/build_transmission_projects.py index 46b9d7e855..f211d5e225 100644 --- a/scripts/build_transmission_projects.py +++ b/scripts/build_transmission_projects.py @@ -134,14 +134,16 @@ def connect_new_lines( return lines, new_buses_df -def get_branch_coords_from_geometry(linestring, reversed=False): +def get_branch_coords_from_geometry( + linestring: LineString | str, reversed: bool = False +) -> np.ndarray: """ Reduces a linestring to its start and end points. Used to simplify the linestring which can have more than two points. Parameters ---------- - linestring : Shapely linestring + linestring : Shapely linestring or WKT string The linestring to reduce. reversed : bool, optional If True, returns the end and start points instead of the start and end points. @@ -152,30 +154,14 @@ def get_branch_coords_from_geometry(linestring, reversed=False): numpy.ndarray Flattened array of start and end coordinates. """ + if isinstance(linestring, str): + linestring = shapely.from_wkt(linestring) coords = np.asarray(linestring.coords) ind = [0, -1] if not reversed else [-1, 0] start_end_coords = coords[ind] return start_end_coords.flatten() -def get_branch_coords_from_buses(line): - """ - Gets line string for branch component in an pypsa network. - - Parameters - ---------- - line : pandas.Series - A row from a branch component DataFrame with bus0 and bus1 attributes. - - Returns - ------- - numpy.ndarray: Flattened array of start and end coordinates. - """ - start_coords = n.buses.loc[line.bus0, ["x", "y"]].values - end_coords = n.buses.loc[line.bus1, ["x", "y"]].values - return np.array([start_coords, end_coords]).flatten() - - def get_bus_coords_from_port(linestring, port=0): """ Extracts the coordinates of a specified port from a given linestring. @@ -196,15 +182,45 @@ def get_bus_coords_from_port(linestring, port=0): return coords -def find_closest_lines(lines, new_lines, distance_upper_bound=0.1, type="new"): +def get_existing_branch_coords( + lines: pd.DataFrame, buses: pd.DataFrame | None = None +) -> np.ndarray: + """Extract geometry endpoints, falling back to buses for missing geometry.""" + coords = [] + for name, branch in lines.iterrows(): + geometry = branch.get("geometry") + if isinstance(geometry, str): + geometry = shapely.from_wkt(geometry) if geometry.strip() else None + if pd.isna(geometry) or geometry.is_empty: + if buses is None: + raise ValueError("Missing geometry; bus coordinates are required.") + logger.warning( + "Missing or empty geometry for %s; using bus coordinates.", name + ) + coords.append( + buses.loc[[branch.bus0, branch.bus1], ["x", "y"]].to_numpy().flatten() + ) + else: + coords.append(get_branch_coords_from_geometry(geometry)) + return np.vstack(coords) + + +def find_closest_lines( + lines: pd.DataFrame, + new_lines: pd.DataFrame, + distance_upper_bound: float = 0.1, + type: str = "new", + buses: pd.DataFrame | None = None, +) -> pd.Series: """ Find the closest lines in the existing set of lines to a set of new lines. Parameters ---------- - lines (pandas.DataFrame): DataFrame of the existing lines. + lines (pandas.DataFrame): DataFrame of the existing lines with column geometry. new_lines (pandas.DataFrame): DataFrame with column geometry containing the new lines. - distance_upper_bound (float, optional): Maximum distance to consider a line as a match. Defaults to 0.1 which corresponds to approximately 15 km. + distance_upper_bound (float, optional): Maximum Euclidean distance between geometry endpoints (x0, y0, x1, y1), in degrees. Defaults to 0.1. + buses (pandas.DataFrame, optional): Bus coordinates used when existing geometry is missing or empty. Returns ------- @@ -212,14 +228,13 @@ def find_closest_lines(lines, new_lines, distance_upper_bound=0.1, type="new"): """ # get coordinates of start and end points of all lines, for new lines we need to check both directions - treelines = lines.apply(get_branch_coords_from_buses, axis=1) + treelines = get_existing_branch_coords(lines, buses) querylines = pd.concat( [ new_lines["geometry"].apply(get_branch_coords_from_geometry), new_lines["geometry"].apply(get_branch_coords_from_geometry, reversed=True), ] ) - treelines = np.vstack(treelines) querylines = np.vstack(querylines) tree = spatial.KDTree(treelines) dist, ind = tree.query(querylines, distance_upper_bound=distance_upper_bound) @@ -399,7 +414,7 @@ def add_projects( lines, n, new_buses_df, bus_carrier="AC" ) duplicate_lines = find_closest_lines( - n.lines, new_lines, distance_upper_bound=0.10, type="new" + n.lines, new_lines, distance_upper_bound=0.10, type="new", buses=n.buses ) new_lines = new_lines.drop(duplicate_lines.index, errors="ignore") new_lines_df = pd.concat([new_lines_df, new_lines]) @@ -415,7 +430,7 @@ def add_projects( bus_carrier=["AC", "DC"], ) duplicate_links = find_closest_lines( - n.links, new_links, distance_upper_bound=0.10, type="new" + n.links, new_links, distance_upper_bound=0.10, type="new", buses=n.buses ) new_links = new_links.drop(duplicate_links.index, errors="ignore") set_underwater_fraction(new_links, offshore_shapes) @@ -424,7 +439,11 @@ def add_projects( n.add("Link", new_links.index, **new_links) elif key == "upgraded_lines": line_map = find_closest_lines( - n.lines, lines, distance_upper_bound=0.30, type="upgraded" + n.lines, + lines, + distance_upper_bound=0.30, + type="upgraded", + buses=n.buses, ) upgraded_lines = lines.loc[line_map.index] lines_to_adjust = adjust_decommissioning(upgraded_lines, line_map) @@ -437,6 +456,7 @@ def add_projects( lines, distance_upper_bound=0.30, type="upgraded", + buses=n.buses, ) upgraded_links = lines.loc[line_map.index] links_to_adjust = adjust_decommissioning(upgraded_links, line_map)