From 0470f119cf34cc0b3d1cc17559a0f719f6b0e1ee Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Jul 2024 15:31:46 +0200 Subject: [PATCH] base_network: use GeoSeries.voronoi_polygons instead of custom solution (#1172) * base_network: use GeoSeries.voronoi_polygons instead of custom solution * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * base_network: move voronoi calculations into separate function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * base_network: fix typo in function voronoi() * base_network: refine voronoi function * add release note [no ci] --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 3 ++ scripts/base_network.py | 80 ++++++++++++----------------------------- 2 files changed, 26 insertions(+), 57 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 0aa97e5a..fa0473fc 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -55,6 +55,9 @@ Upcoming Release excess power) at every AC bus. This can speed up the solving process as the curtailment decision is aggregated into a single generator per region. +* In :mod:`base_network`, replace own voronoi polygon calculation function with + Geopandas `gdf.voronoi_polygons` method. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/scripts/base_network.py b/scripts/base_network.py index f87d666b..118e7dba 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -72,6 +72,7 @@ Creates the network topology from an ENTSO-E map extract, and create Voronoi sha """ import logging +import warnings from itertools import product import geopandas as gpd @@ -85,9 +86,9 @@ import shapely.wkt import yaml from _helpers import REGION_COLS, configure_logging, get_snapshots, set_scenario_config from packaging.version import Version, parse -from scipy import spatial from scipy.sparse import csgraph -from shapely.geometry import LineString, Point, Polygon +from scipy.spatial import KDTree +from shapely.geometry import LineString, Point PD_GE_2_2 = parse(pd.__version__) >= Version("2.2") @@ -118,7 +119,7 @@ def _find_closest_links(links, new_links, distance_upper_bound=1.5): querycoords = np.vstack( [new_links[["x1", "y1", "x2", "y2"]], new_links[["x2", "y2", "x1", "y1"]]] ) - tree = spatial.KDTree(treecoords) + tree = KDTree(treecoords) dist, ind = tree.query(querycoords, distance_upper_bound=distance_upper_bound) found_b = ind < len(links) found_i = np.arange(len(new_links) * 2)[found_b] % len(new_links) @@ -273,7 +274,7 @@ def _add_links_from_tyndp(buses, links, links_tyndp, europe_shape): return buses, links tree_buses = buses.query("carrier=='AC'") - tree = spatial.KDTree(tree_buses[["x", "y"]]) + tree = KDTree(tree_buses[["x", "y"]]) _, ind0 = tree.query(links_tyndp[["x1", "y1"]]) ind0_b = ind0 < len(tree_buses) links_tyndp.loc[ind0_b, "bus0"] = tree_buses.index[ind0[ind0_b]] @@ -788,59 +789,26 @@ def base_network( return n -def voronoi_partition_pts(points, outline): +def voronoi(points, outline, crs=4326): """ - Compute the polygons of a voronoi partition of `points` within the polygon - `outline`. Taken from - https://github.com/FRESNA/vresutils/blob/master/vresutils/graph.py. - - Attributes - ---------- - points : Nx2 - ndarray[dtype=float] - outline : Polygon - Returns - ------- - polygons : N - ndarray[dtype=Polygon|MultiPolygon] + Create Voronoi polygons from a set of points within an outline. """ - points = np.asarray(points) + pts = gpd.GeoSeries( + gpd.points_from_xy(points.x, points.y), + index=points.index, + crs=crs, + ) + voronoi = pts.voronoi_polygons(extend_to=outline).clip(outline) - if len(points) == 1: - polygons = [outline] - else: - xmin, ymin = np.amin(points, axis=0) - xmax, ymax = np.amax(points, axis=0) - xspan = xmax - xmin - yspan = ymax - ymin + # can be removed with shapely 2.1 where order is preserved + # https://github.com/shapely/shapely/issues/2020 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + pts = gpd.GeoDataFrame(geometry=pts) + voronoi = gpd.GeoDataFrame(geometry=voronoi) + joined = gpd.sjoin_nearest(pts, voronoi, how="right") - # to avoid any network positions outside all Voronoi cells, append - # the corners of a rectangle framing these points - vor = spatial.Voronoi( - np.vstack( - ( - points, - [ - [xmin - 3.0 * xspan, ymin - 3.0 * yspan], - [xmin - 3.0 * xspan, ymax + 3.0 * yspan], - [xmax + 3.0 * xspan, ymin - 3.0 * yspan], - [xmax + 3.0 * xspan, ymax + 3.0 * yspan], - ], - ) - ) - ) - - polygons = [] - for i in range(len(points)): - poly = Polygon(vor.vertices[vor.regions[vor.point_region[i]]]) - - if not poly.is_valid: - poly = poly.buffer(0) - - with np.errstate(invalid="ignore"): - poly = poly.intersection(outline) - - polygons.append(poly) - - return polygons + return joined.dissolve(by="Bus").squeeze() def build_bus_shapes(n, country_shapes, offshore_shapes, countries): @@ -870,9 +838,7 @@ def build_bus_shapes(n, country_shapes, offshore_shapes, countries): "name": onshore_locs.index, "x": onshore_locs["x"], "y": onshore_locs["y"], - "geometry": voronoi_partition_pts( - onshore_locs.values, onshore_shape - ), + "geometry": voronoi(onshore_locs, onshore_shape), "country": country, }, crs=n.crs, @@ -888,7 +854,7 @@ def build_bus_shapes(n, country_shapes, offshore_shapes, countries): "name": offshore_locs.index, "x": offshore_locs["x"], "y": offshore_locs["y"], - "geometry": voronoi_partition_pts(offshore_locs.values, offshore_shape), + "geometry": voronoi(offshore_locs, offshore_shape), "country": country, }, crs=n.crs,