diff --git a/momepy/functional/_elements.py b/momepy/functional/_elements.py index a35b7205..3a44e5e4 100644 --- a/momepy/functional/_elements.py +++ b/momepy/functional/_elements.py @@ -10,7 +10,7 @@ from libpysal.cg import voronoi_frames from libpysal.graph import Graph from packaging.version import Version -from pandas import Series +from pandas import MultiIndex, Series GPD_GE_013 = Version(gpd.__version__) >= Version("0.13.0") GPD_GE_10 = Version(gpd.__version__) >= Version("1.0dev") @@ -103,6 +103,10 @@ def morphological_tessellation( 4 POLYGON ((1603084.231 6464104.386, 1603083.773... """ + + if isinstance(geometry.index, MultiIndex): + raise ValueError("MultiIndex is not supported for tessellation.") + if isinstance(clip, GeoSeries | GeoDataFrame): clip = clip.union_all() if GPD_GE_10 else clip.unary_union @@ -218,6 +222,9 @@ def enclosed_tessellation( 126 POLYGON ((1603528.593 6464221.033, 1603527.796... 0 """ + if isinstance(geometry.index, MultiIndex): + raise ValueError("MultiIndex is not supported for tessellation.") + # convert to GeoDataFrame and add position (we will need it later) enclosures = enclosures.geometry.to_frame() enclosures["position"] = range(len(enclosures)) @@ -354,6 +361,12 @@ def verify_tessellation(tessellation, geometry): >>> excluded, multipolygons = momepy.verify_tessellation(tessellation, buildings) """ + + if isinstance(geometry.index, MultiIndex) or isinstance( + tessellation.index, MultiIndex + ): + raise ValueError("MultiIndex is not supported for tessellation.") + # check against input layer ids_original = geometry.index ids_generated = tessellation.index @@ -516,6 +529,17 @@ def get_nearest_node( 143 22.0 Length: 144, dtype: float64 """ + + if ( + isinstance(buildings.index, MultiIndex) + or isinstance(nearest_edge.index, MultiIndex) + or isinstance(nodes.index, MultiIndex) + or isinstance(edges.index, MultiIndex) + ): + raise ValueError( + "MultiIndex is not supported for calculating the nearest node." + ) + # treat possibly missing edge index a = np.empty(len(buildings)) na_mask = np.isnan(nearest_edge) @@ -600,6 +624,12 @@ def generate_blocks( >>> tessellation["block_id"] = tessellation_id """ + if ( + isinstance(buildings.index, MultiIndex) + or isinstance(tessellation.index, MultiIndex) + or isinstance(edges.index, MultiIndex) + ): + raise ValueError("MultiIndex is not supported for generating blocks.") id_name: str = "bID" # slice the tessellations by the street network diff --git a/momepy/functional/_shape.py b/momepy/functional/_shape.py index a4f00d58..ea1eb5cf 100644 --- a/momepy/functional/_shape.py +++ b/momepy/functional/_shape.py @@ -4,7 +4,7 @@ from geopandas import GeoDataFrame, GeoSeries from numpy.typing import NDArray from packaging.version import Version -from pandas import DataFrame, Series +from pandas import DataFrame, MultiIndex, Series from momepy.functional import _dimension @@ -724,6 +724,10 @@ def centroid_corner_distance( "momepy.centroid_corner_distance requires geopandas 0.13 or later. " ) + result_index = geometry.index + if isinstance(geometry.index, MultiIndex): + geometry = geometry.reset_index(drop=True) + def _ccd(points: DataFrame, eps: float) -> Series: centroid = points.values[0, 2:] pts = points.values[:-1, :2] @@ -738,7 +742,7 @@ def _ccd(points: DataFrame, eps: float) -> Series: coords = geometry.exterior.get_coordinates(index_parts=False) coords[["cent_x", "cent_y"]] = geometry.centroid.get_coordinates(index_parts=False) ccd = coords.groupby(level=0).apply(_ccd, eps=eps) - ccd.index = geometry.index + ccd.index = result_index return ccd diff --git a/momepy/functional/tests/test_elements.py b/momepy/functional/tests/test_elements.py index 8305d7da..2ae62bdc 100644 --- a/momepy/functional/tests/test_elements.py +++ b/momepy/functional/tests/test_elements.py @@ -332,6 +332,34 @@ def test_blocks_inner(self): else: assert len(blocks.sindex.query_bulk(blocks.geometry, "overlaps")[0]) == 0 + def test_multi_index(self): + buildings = self.df_buildings.set_index(["uID", "uID"]) + with pytest.raises( + ValueError, match="MultiIndex is not supported for tessellation." + ): + mm.morphological_tessellation(buildings) + with pytest.raises( + ValueError, match="MultiIndex is not supported for tessellation." + ): + mm.enclosed_tessellation(buildings, self.enclosures) + with pytest.raises( + ValueError, match="MultiIndex is not supported for tessellation." + ): + mm.verify_tessellation(buildings, self.enclosures) + + with pytest.raises( + ValueError, + match="MultiIndex is not supported for calculating the nearest node.", + ): + mm.get_nearest_node( + buildings, self.enclosures, self.enclosures, self.enclosures + ) + + with pytest.raises( + ValueError, match="MultiIndex is not supported for generating blocks." + ): + mm.generate_blocks(buildings, self.enclosures, self.enclosures) + class TestElementsEquivalence: def setup_method(self): diff --git a/momepy/functional/tests/test_multi_index.py b/momepy/functional/tests/test_multi_index.py deleted file mode 100644 index f49383ab..00000000 --- a/momepy/functional/tests/test_multi_index.py +++ /dev/null @@ -1,60 +0,0 @@ -import geopandas as gpd -import numpy as np -from packaging.version import Version - -import momepy as mm - -GPD_013 = Version(gpd.__version__) >= Version("0.13") - - -class TestShape: - def setup_method(self): - test_file_path = mm.datasets.get_path("bubenec") - self.df_buildings = gpd.read_file(test_file_path, layer="buildings") - self.df_streets = gpd.read_file(test_file_path, layer="streets") - self.df_tessellation = gpd.read_file(test_file_path, layer="tessellation") - self.df_buildings["height"] = np.linspace(10.0, 30.0, 144) - self.df_buildings["area"] = self.df_buildings.geometry.area - blocks = mm.Blocks( - self.df_tessellation, self.df_streets, self.df_buildings, "bID", "uID" - ) - self.df_buildings["bID"] = blocks.buildings_id - self.df_tessellation["bID"] = blocks.tessellation_id - self.df_buildings_multi = self.df_buildings.set_index(["bID", "uID"]) - - def test_dimension(self): - dimension_functions = [ - "volume", - "floor_area", - "courtyard_area", - "longest_axis_length", - "street_profile", - ] - - dimension_functions_args = [ - (self.df_buildings["area"], self.df_buildings["height"]), - (self.df_buildings["area"], self.df_buildings["height"]), - (self.df_buildings,), - (self.df_buildings,), - (self.df_streets, self.df_buildings), - ] - - dimension_functions_args_multi = [ - (self.df_buildings_multi["area"], self.df_buildings_multi["height"]), - (self.df_buildings_multi["area"], self.df_buildings_multi["height"]), - (self.df_buildings_multi,), - (self.df_buildings_multi,), - (self.df_streets, self.df_buildings), - ] - - for fname, fargs, fargs_multi in zip( - dimension_functions, - dimension_functions_args, - dimension_functions_args_multi, - strict=True, - ): - func = getattr(mm, fname) - res = func(*fargs) - res_multi = func(*fargs_multi) - - assert np.allclose(res.values, res_multi.values, equal_nan=True)