diff --git a/edisgo/network/grids.py b/edisgo/network/grids.py index 7f466b3d..20d50ed7 100644 --- a/edisgo/network/grids.py +++ b/edisgo/network/grids.py @@ -90,20 +90,19 @@ def graph(self): @property def geopandas(self): """ - Returns components as :geopandas:`GeoDataFrame`\\ s + Returns components as :geopandas:`GeoDataFrame`\\ s. Returns container with :geopandas:`GeoDataFrame`\\ s containing all georeferenced components within the grid. Returns ------- - :class:`~.tools.geopandas_helper.GeoPandasGridContainer` or \ - list(:class:`~.tools.geopandas_helper.GeoPandasGridContainer`) + :class:`~.tools.geopandas_helper.GeoPandasGridContainer` Data container with GeoDataFrames containing all georeferenced components - within the grid(s). + within the grid. """ - return to_geopandas(self) + return to_geopandas(self, srid=self.edisgo_obj.topology.grid_district["srid"]) @property def station(self): @@ -650,10 +649,3 @@ def draw( else: plt.savefig(filename, dpi=150, bbox_inches="tight", pad_inches=0.1) plt.close() - - @property - def geopandas(self): - """ - TODO: Remove this as soon as LVGrids are georeferenced - """ - raise NotImplementedError("LV Grids are not georeferenced yet.") diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 2462314f..997a3b9f 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -5,6 +5,7 @@ import random import warnings +from typing import TYPE_CHECKING from zipfile import ZipFile import networkx as nx @@ -15,7 +16,7 @@ from edisgo.network.components import Switch from edisgo.network.grids import LVGrid, MVGrid -from edisgo.tools import geo, networkx_helper +from edisgo.tools import geo, geopandas_helper, networkx_helper from edisgo.tools.tools import ( calculate_apparent_power, calculate_line_reactance, @@ -30,6 +31,9 @@ from shapely.ops import transform from shapely.wkt import loads as wkt_loads +if TYPE_CHECKING: + from edisgo.tools.geopandas_helper import GeoPandasGridContainer + logger = logging.getLogger(__name__) COLUMNS = { @@ -2756,7 +2760,9 @@ def to_graph(self): self.transformers_df, ) - def to_geopandas(self, mode: str = "mv"): + def to_geopandas( + self, mode: str | None = None, lv_grid_id: int | None = None + ) -> GeoPandasGridContainer: """ Returns components as :geopandas:`GeoDataFrame`\\ s. @@ -2766,23 +2772,29 @@ def to_geopandas(self, mode: str = "mv"): Parameters ---------- mode : str - Return mode. If mode is "mv" the mv components are returned. If mode is "lv" - a generator with a container per lv grid is returned. Default: "mv" + If `mode` is None, GeoDataFrames for the MV grid and underlying LV grids is + returned. If `mode` is "mv", GeoDataFrames for only the MV grid are + returned. If `mode` is "lv", GeoDataFrames for the LV grid specified through + `lv_grid_id` are returned. + Default: None. + lv_grid_id : int + Only needs to be provided in case `mode` is "lv". In that case `lv_grid_id` + gives the LV grid ID as integer of the LV grid for which to return the + geodataframes. Returns ------- - :class:`~.tools.geopandas_helper.GeoPandasGridContainer` or \ - list(:class:`~.tools.geopandas_helper.GeoPandasGridContainer`) + :class:`~.tools.geopandas_helper.GeoPandasGridContainer` Data container with GeoDataFrames containing all georeferenced components - within the grid(s). + within the grid. """ - if mode == "mv": + if mode is None: + return geopandas_helper.to_geopandas(self, srid=self.grid_district["srid"]) + elif mode == "mv": return self.mv_grid.geopandas elif mode == "lv": - raise NotImplementedError("LV Grids are not georeferenced yet.") - # for lv_grid in self.mv_grid.lv_grids: - # yield lv_grid.geopandas + return self.get_lv_grid(name=lv_grid_id).geopandas else: raise ValueError(f"{mode} is not valid. See docstring for more info.") diff --git a/edisgo/tools/geopandas_helper.py b/edisgo/tools/geopandas_helper.py index 6c48a62e..14066d56 100644 --- a/edisgo/tools/geopandas_helper.py +++ b/edisgo/tools/geopandas_helper.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from edisgo.network.grids import Grid + from edisgo.network.topology import Topology COMPONENTS: list[str] = [ "generators_df", @@ -162,14 +163,17 @@ def plot(self): raise NotImplementedError -def to_geopandas(grid_obj: Grid): +def to_geopandas(grid_obj: Grid | Topology, srid: int) -> GeoPandasGridContainer: """ - Translates all DataFrames with geolocations within a Grid class to GeoDataFrames. + Translates all DataFrames with geolocations within a grid topology to GeoDataFrames. Parameters ---------- - grid_obj : :class:`~.network.grids.Grid` - Grid object to transform. + grid_obj : :class:`~.network.grids.Grid` or :class:`~.network.topology.Topology` + Grid or Topology object to transform. + srid : int + SRID (spatial reference ID) of x and y coordinates of buses. Usually given in + Topology.grid_district["srid"]. Returns ------- @@ -178,9 +182,6 @@ def to_geopandas(grid_obj: Grid): their geolocation. """ - # get srid id - srid = grid_obj._edisgo_obj.topology.grid_district["srid"] - # convert buses_df buses_df = grid_obj.buses_df buses_df = buses_df.assign( @@ -204,25 +205,27 @@ def to_geopandas(grid_obj: Grid): crs=f"EPSG:{srid}", ) if components_dict[component.replace("_df", "_gdf")].empty: - components_dict[component.replace("_df", "_gdf")].index = components_dict[ - component.replace("_df", "_gdf") - ].index.astype(object) + components_dict[component.replace("_df", "_gdf")].index = attr.index # convert lines_df lines_df = grid_obj.lines_df - geom_0 = lines_df.merge( - buses_gdf[["geometry"]], left_on="bus0", right_index=True - ).geometry - geom_1 = lines_df.merge( - buses_gdf[["geometry"]], left_on="bus1", right_index=True - ).geometry - - geometry = [ - LineString([point_0, point_1]) for point_0, point_1 in list(zip(geom_0, geom_1)) - ] - - lines_gdf = gpd.GeoDataFrame(lines_df.assign(geometry=geometry), crs=f"EPSG:{srid}") + lines_gdf = lines_df.merge( + buses_gdf[["geometry", "v_nom"]].rename(columns={"geometry": "geom_0"}), + left_on="bus0", + right_index=True, + ) + lines_gdf = lines_gdf.merge( + buses_gdf[["geometry"]].rename(columns={"geometry": "geom_1"}), + left_on="bus1", + right_index=True, + ) + lines_gdf["geometry"] = lines_gdf.apply( + lambda _: LineString([_["geom_0"], _["geom_1"]]), axis=1 + ) + lines_gdf = gpd.GeoDataFrame( + lines_gdf.drop(columns=["geom_0", "geom_1"]), crs=f"EPSG:{srid}" + ) return GeoPandasGridContainer( crs=f"EPSG:{srid}", diff --git a/tests/network/test_topology.py b/tests/network/test_topology.py index 0baf02f3..a3422db4 100644 --- a/tests/network/test_topology.py +++ b/tests/network/test_topology.py @@ -951,9 +951,17 @@ def setup_class(self): self.edisgo.set_time_series_worst_case_analysis() def test_to_geopandas(self): - geopandas_container = self.edisgo.topology.to_geopandas() + # further tests of to_geopandas are conducted in test_geopandas_helper.py - assert isinstance(geopandas_container, GeoPandasGridContainer) + # set up edisgo object with georeferenced LV + edisgo_geo = EDisGo( + ding0_grid=pytest.ding0_test_network_3_path, legacy_ding0_grids=False + ) + test_suits = { + "mv": {"edisgo_obj": self.edisgo, "mode": "mv", "lv_grid_id": None}, + "lv": {"edisgo_obj": edisgo_geo, "mode": "lv", "lv_grid_id": 1164120002}, + "mv+lv": {"edisgo_obj": edisgo_geo, "mode": None, "lv_grid_id": None}, + } attrs = [ "buses_gdf", @@ -964,19 +972,30 @@ def test_to_geopandas(self): "transformers_gdf", ] - for attr_str in attrs: - attr = getattr(geopandas_container, attr_str) - grid_attr = getattr( - self.edisgo.topology.mv_grid, attr_str.replace("_gdf", "_df") + for test_suit, params in test_suits.items(): + # call to_geopandas() function with different settings + geopandas_container = params["edisgo_obj"].topology.to_geopandas( + mode=params["mode"], lv_grid_id=params["lv_grid_id"] ) - assert isinstance(attr, GeoDataFrame) + assert isinstance(geopandas_container, GeoPandasGridContainer) - common_cols = list(set(attr.columns).intersection(grid_attr.columns)) + # check that content of geodataframes is the same as content of original + # dataframes + for attr_str in attrs: + grid = getattr(geopandas_container, "grid") + attr = getattr(geopandas_container, attr_str) + grid_attr = getattr(grid, attr_str.replace("_gdf", "_df")) - assert_frame_equal( - attr[common_cols], grid_attr[common_cols], check_names=False - ) + assert isinstance(attr, GeoDataFrame) + + common_cols = list(set(attr.columns).intersection(grid_attr.columns)) + + assert_frame_equal( + attr[common_cols].sort_index(), + grid_attr[common_cols].sort_index(), + check_names=False, + ) def test_from_csv(self): """ diff --git a/tests/tools/test_geopandas_helper.py b/tests/tools/test_geopandas_helper.py new file mode 100644 index 00000000..a9aca942 --- /dev/null +++ b/tests/tools/test_geopandas_helper.py @@ -0,0 +1,65 @@ +import pytest + +from edisgo import EDisGo +from edisgo.tools import geopandas_helper + + +class TestGeopandasHelper: + @classmethod + def setup_class(self): + self.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_path) + + def test_to_geopandas(self): + # further tests of this function are conducted in test_topology.py + # test MV grid + data = geopandas_helper.to_geopandas(self.edisgo.topology.mv_grid, 4326) + assert data.buses_gdf.shape[0] == self.edisgo.topology.mv_grid.buses_df.shape[0] + assert ( + data.buses_gdf.shape[1] + == self.edisgo.topology.mv_grid.buses_df.shape[1] + 1 - 2 + ) + assert "geometry" in data.buses_gdf.columns + + assert data.lines_gdf.shape[0] == self.edisgo.topology.mv_grid.lines_df.shape[0] + assert ( + data.lines_gdf.shape[1] + == self.edisgo.topology.mv_grid.lines_df.shape[1] + 2 + ) + assert "geometry" in data.lines_gdf.columns + + assert data.loads_gdf.shape[0] == self.edisgo.topology.mv_grid.loads_df.shape[0] + assert ( + data.loads_gdf.shape[1] + == self.edisgo.topology.mv_grid.loads_df.shape[1] + 2 + ) + assert "geometry" in data.loads_gdf.columns + + assert ( + data.generators_gdf.shape[0] + == self.edisgo.topology.mv_grid.generators_df.shape[0] + ) + assert ( + data.generators_gdf.shape[1] + == self.edisgo.topology.mv_grid.generators_df.shape[1] + 2 + ) + assert "geometry" in data.generators_gdf.columns + + assert ( + data.storage_units_gdf.shape[0] + == self.edisgo.topology.mv_grid.storage_units_df.shape[0] + ) + assert ( + data.storage_units_gdf.shape[1] + == self.edisgo.topology.mv_grid.storage_units_df.shape[1] + 2 + ) + assert "geometry" in data.storage_units_gdf.columns + + assert ( + data.transformers_gdf.shape[0] + == self.edisgo.topology.mv_grid.transformers_df.shape[0] + ) + assert ( + data.transformers_gdf.shape[1] + == self.edisgo.topology.mv_grid.transformers_df.shape[1] + 2 + ) + assert "geometry" in data.transformers_gdf.columns