From c205083bc9735caa5abc97a9824f409dc531f80d Mon Sep 17 00:00:00 2001 From: Marius Maryniak Date: Wed, 19 Feb 2025 13:01:03 +0100 Subject: [PATCH] refactor!: remove `GridGenerator` (#474) --- .../_functional/geodata/coordinates_filter.py | 24 ++++- aviary/_functional/geodata/grid_generator.py | 96 ------------------ aviary/core/bounding_box.py | 8 ++ aviary/core/process_area.py | 39 ++++++-- aviary/geodata/__init__.py | 2 - aviary/geodata/grid_generator.py | 74 -------------- dev/mkdocs.yaml | 1 - docs/api_reference/geodata/grid_generator.md | 1 - .../geodata/data/data_test_grid_generator.py | 34 ------- .../geodata/test_grid_generator.py | 98 ------------------- tests/core/data/data_test_bounding_box.py | 16 ++- tests/core/data/data_test_process_area.py | 41 ++++++++ tests/core/test_bounding_box.py | 7 +- tests/geodata/conftest.py | 17 ---- tests/geodata/test_grid_generator.py | 66 ------------- 15 files changed, 123 insertions(+), 401 deletions(-) delete mode 100644 aviary/_functional/geodata/grid_generator.py delete mode 100644 aviary/geodata/grid_generator.py delete mode 100644 docs/api_reference/geodata/grid_generator.md delete mode 100644 tests/_functional/geodata/data/data_test_grid_generator.py delete mode 100644 tests/_functional/geodata/test_grid_generator.py delete mode 100644 tests/geodata/test_grid_generator.py diff --git a/aviary/_functional/geodata/coordinates_filter.py b/aviary/_functional/geodata/coordinates_filter.py index 06a72be6..e78c7886 100644 --- a/aviary/_functional/geodata/coordinates_filter.py +++ b/aviary/_functional/geodata/coordinates_filter.py @@ -5,8 +5,11 @@ import geopandas as gpd import numpy as np from numpy import typing as npt +from shapely.geometry import ( + Polygon, + box, +) -from aviary._functional.geodata.grid_generator import _generate_tiles from aviary.core.enums import ( GeospatialFilterMode, SetFilterMode, @@ -130,6 +133,25 @@ def _generate_grid( ) +def _generate_tiles( + coordinates: CoordinatesSet, + tile_size: TileSize, +) -> list[Polygon]: + """Generates a list of tiles from the coordinates. + + Parameters: + coordinates: coordinates (x_min, y_min) of each tile + tile_size: tile size in meters + + Returns: + list of tiles + """ + return [ + box(x_min, y_min, x_min + tile_size, y_min + tile_size) + for x_min, y_min in coordinates + ] + + def _geospatial_filter_difference( coordinates: CoordinatesSet, grid: gpd.GeoDataFrame, diff --git a/aviary/_functional/geodata/grid_generator.py b/aviary/_functional/geodata/grid_generator.py deleted file mode 100644 index 03b69d57..00000000 --- a/aviary/_functional/geodata/grid_generator.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import geopandas as gpd -import numpy as np -from shapely.geometry import Polygon, box - -if TYPE_CHECKING: - from aviary.core.bounding_box import BoundingBox - from aviary.core.type_aliases import ( - CoordinatesSet, - EPSGCode, - TileSize, - ) - - -def compute_coordinates( - bounding_box: BoundingBox, - tile_size: TileSize, - quantize: bool = True, -) -> CoordinatesSet: - """Computes the coordinates of the bottom left corner of each tile. - - Parameters: - bounding_box: bounding box - tile_size: tile size in meters - quantize: if True, the bounding box is quantized to `tile_size` - - Returns: - coordinates (x_min, y_min) of each tile - """ - if quantize: - bounding_box = bounding_box.quantize( - value=tile_size, - inplace=False, - ) - - coordinates_range_x = np.arange(bounding_box.x_min, bounding_box.x_max, tile_size) - coordinates_range_y = np.arange(bounding_box.y_min, bounding_box.y_max, tile_size) - coordinates_x, coordinates_y = np.meshgrid(coordinates_range_x, coordinates_range_y) - - coordinates_x = coordinates_x.reshape(-1)[..., np.newaxis] - coordinates_y = coordinates_y.reshape(-1)[..., np.newaxis] - return np.concatenate((coordinates_x, coordinates_y), axis=-1).astype(np.int32) - - -def generate_grid( - bounding_box: BoundingBox, - tile_size: TileSize, - epsg_code: EPSGCode, - quantize: bool = True, -) -> gpd.GeoDataFrame: - """Generates a geodataframe of the grid. - - Parameters: - bounding_box: bounding box - tile_size: tile size in meters - epsg_code: EPSG code - quantize: if True, the bounding box is quantized to `tile_size` - - Returns: - grid - """ - coordinates = compute_coordinates( - bounding_box=bounding_box, - tile_size=tile_size, - quantize=quantize, - ) - tiles = _generate_tiles( - coordinates=coordinates, - tile_size=tile_size, - ) - return gpd.GeoDataFrame( - geometry=tiles, - crs=f'EPSG:{epsg_code}', - ) - - -def _generate_tiles( - coordinates: CoordinatesSet, - tile_size: TileSize, -) -> list[Polygon]: - """Generates a list of tiles from the coordinates. - - Parameters: - coordinates: coordinates (x_min, y_min) of each tile - tile_size: tile size in meters - - Returns: - list of tiles - """ - return [ - box(x_min, y_min, x_min + tile_size, y_min + tile_size) - for x_min, y_min in coordinates - ] diff --git a/aviary/core/bounding_box.py b/aviary/core/bounding_box.py index edebc436..a606dee8 100644 --- a/aviary/core/bounding_box.py +++ b/aviary/core/bounding_box.py @@ -124,6 +124,7 @@ def from_gdf( Raises: AviaryUserError: Invalid `gdf` (the geodataframe contains no geometries) + AviaryUserError: Invalid `gdf` (the geodataframe contains geometries other than polygons) """ if gdf.empty: message = ( @@ -132,6 +133,13 @@ def from_gdf( ) raise AviaryUserError(message) + if not all(gdf.geometry.geom_type.isin(['Polygon', 'MultiPolygon'])): + message = ( + 'Invalid gdf! ' + 'The geodataframe must contain only polygons.' + ) + raise AviaryUserError(message) + x_min, y_min, x_max, y_max = gdf.total_bounds x_min = floor(x_min) y_min = floor(y_min) diff --git a/aviary/core/process_area.py b/aviary/core/process_area.py index e8ac7e97..3dfe39ca 100644 --- a/aviary/core/process_area.py +++ b/aviary/core/process_area.py @@ -24,9 +24,6 @@ geospatial_filter, set_filter, ) - -# noinspection PyProtectedMember -from aviary._functional.geodata.grid_generator import compute_coordinates from aviary.core.bounding_box import BoundingBox from aviary.core.enums import ( GeospatialFilterMode, @@ -192,7 +189,7 @@ def from_bounding_box( ) raise AviaryUserError(message) - coordinates = compute_coordinates( + coordinates = cls._compute_coordinates( bounding_box=bounding_box, tile_size=tile_size, quantize=quantize, @@ -231,7 +228,7 @@ def from_gdf( ) raise AviaryUserError(message) - if not all(gdf.geometry.geom_type == 'Polygon'): + if not all(gdf.geometry.geom_type.isin(['Polygon', 'MultiPolygon'])): message = ( 'Invalid gdf! ' 'The geodataframe must contain only polygons.' @@ -246,7 +243,7 @@ def from_gdf( raise AviaryUserError(message) bounding_box = BoundingBox.from_gdf(gdf=gdf) - coordinates = compute_coordinates( + coordinates = cls._compute_coordinates( bounding_box=bounding_box, tile_size=tile_size, quantize=quantize, @@ -262,6 +259,36 @@ def from_gdf( tile_size=tile_size, ) + @staticmethod + def _compute_coordinates( + bounding_box: BoundingBox, + tile_size: TileSize, + quantize: bool = True, + ) -> CoordinatesSet: + """Computes the coordinates of the bottom left corner of each tile. + + Parameters: + bounding_box: Bounding box + tile_size: Tile size in meters + quantize: If True, the bounding box is quantized to `tile_size` + + Returns: + Coordinates (x_min, y_min) of each tile in meters + """ + if quantize: + bounding_box = bounding_box.quantize( + value=tile_size, + inplace=False, + ) + + coordinates_range_x = np.arange(bounding_box.x_min, bounding_box.x_max, tile_size) + coordinates_range_y = np.arange(bounding_box.y_min, bounding_box.y_max, tile_size) + coordinates_x, coordinates_y = np.meshgrid(coordinates_range_x, coordinates_range_y) + + coordinates_x = coordinates_x.reshape(-1)[..., np.newaxis] + coordinates_y = coordinates_y.reshape(-1)[..., np.newaxis] + return np.concatenate((coordinates_x, coordinates_y), axis=-1).astype(np.int32) + @classmethod def from_json( cls, diff --git a/aviary/geodata/__init__.py b/aviary/geodata/__init__.py index 944536ec..dd9f8cd7 100644 --- a/aviary/geodata/__init__.py +++ b/aviary/geodata/__init__.py @@ -24,7 +24,6 @@ ValuePostprocessor, ValuePostprocessorConfig, ) -from .grid_generator import GridGenerator __all__ = [ 'ClipPostprocessor', @@ -41,7 +40,6 @@ 'GeodataPostprocessor', 'GeodataPostprocessorConfig', 'GeospatialFilter', - 'GridGenerator', 'MaskFilter', 'SetFilter', 'SievePostprocessor', diff --git a/aviary/geodata/grid_generator.py b/aviary/geodata/grid_generator.py deleted file mode 100644 index 3e7c657e..00000000 --- a/aviary/geodata/grid_generator.py +++ /dev/null @@ -1,74 +0,0 @@ -import geopandas as gpd - -# noinspection PyProtectedMember -from aviary._functional.geodata.grid_generator import ( - compute_coordinates, - generate_grid, -) -from aviary.core.bounding_box import BoundingBox -from aviary.core.type_aliases import ( - CoordinatesSet, - EPSGCode, - TileSize, -) - - -class GridGenerator: - """A grid generator generates a grid of tiles. - The grid generator can be used to compute the coordinates of the bottom left corner of each tile - or to generate a geodataframe of the grid for aggregation. - """ - - def __init__( - self, - bounding_box: BoundingBox, - epsg_code: EPSGCode, - ) -> None: - """ - Parameters: - bounding_box: bounding box - epsg_code: EPSG code - """ - self.bounding_box = bounding_box - self.epsg_code = epsg_code - - def compute_coordinates( - self, - tile_size: TileSize, - quantize: bool = True, - ) -> CoordinatesSet: - """Computes the coordinates of the bottom left corner of each tile. - - Parameters: - tile_size: tile size in meters - quantize: if True, the bounding box is quantized to `tile_size` - - Returns: - coordinates (x_min, y_min) of each tile - """ - return compute_coordinates( - bounding_box=self.bounding_box, - tile_size=tile_size, - quantize=quantize, - ) - - def generate_grid( - self, - tile_size: TileSize, - quantize: bool = True, - ) -> gpd.GeoDataFrame: - """Generates a geodataframe of the grid. - - Parameters: - tile_size: tile size in meters - quantize: if True, the bounding box is quantized to `tile_size` - - Returns: - grid - """ - return generate_grid( - bounding_box=self.bounding_box, - tile_size=tile_size, - epsg_code=self.epsg_code, - quantize=quantize, - ) diff --git a/dev/mkdocs.yaml b/dev/mkdocs.yaml index 6adeeb6c..228861d0 100644 --- a/dev/mkdocs.yaml +++ b/dev/mkdocs.yaml @@ -57,7 +57,6 @@ nav: - SievePostprocessor: api_reference/geodata/geodata_postprocessor/sieve_postprocessor.md - SimplifyPostprocessor: api_reference/geodata/geodata_postprocessor/simplify_postprocessor.md - ValuePostprocessor: api_reference/geodata/geodata_postprocessor/value_postprocessor.md - - GridGenerator: api_reference/geodata/grid_generator.md - aviary.inference: - Exporter: - Exporter: api_reference/inference/exporter/exporter.md diff --git a/docs/api_reference/geodata/grid_generator.md b/docs/api_reference/geodata/grid_generator.md deleted file mode 100644 index af33bdfe..00000000 --- a/docs/api_reference/geodata/grid_generator.md +++ /dev/null @@ -1 +0,0 @@ -::: aviary.geodata.GridGenerator diff --git a/tests/_functional/geodata/data/data_test_grid_generator.py b/tests/_functional/geodata/data/data_test_grid_generator.py deleted file mode 100644 index baebe24c..00000000 --- a/tests/_functional/geodata/data/data_test_grid_generator.py +++ /dev/null @@ -1,34 +0,0 @@ -import numpy as np -from shapely.geometry import box - -from aviary.core.bounding_box import BoundingBox - -data_test_compute_coordinates = [ - # test case 1: bounding_box is not quantized - ( - BoundingBox(-127, -127, 127, 127), - 128, - False, - np.array([[-127, -127], [1, -127], [-127, 1], [1, 1]], dtype=np.int32), - ), - # test case 2: bounding_box is quantized - ( - BoundingBox(-127, -127, 127, 127), - 128, - True, - np.array([[-128, -128], [0, -128], [-128, 0], [0, 0]], dtype=np.int32), - ), -] - -data_test__generate_tiles = [ - ( - np.array([[-128, -128], [0, -128], [-128, 0], [0, 0]], dtype=np.int32), - 128, - [ - box(-128, -128, 0, 0), - box(0, -128, 128, 0), - box(-128, 0, 0, 128), - box(0, 0, 128, 128), - ], - ), -] diff --git a/tests/_functional/geodata/test_grid_generator.py b/tests/_functional/geodata/test_grid_generator.py deleted file mode 100644 index 3d2188c2..00000000 --- a/tests/_functional/geodata/test_grid_generator.py +++ /dev/null @@ -1,98 +0,0 @@ -from unittest.mock import MagicMock, patch - -import geopandas as gpd -import geopandas.testing -import numpy as np -import pytest -from shapely.geometry import Polygon, box - -# noinspection PyProtectedMember -from aviary._functional.geodata.grid_generator import ( - _generate_tiles, - compute_coordinates, - generate_grid, -) -from aviary.core.bounding_box import BoundingBox -from aviary.core.type_aliases import ( - CoordinatesSet, - TileSize, -) -from tests._functional.geodata.data.data_test_grid_generator import ( - data_test__generate_tiles, - data_test_compute_coordinates, -) - - -@pytest.mark.parametrize(('bounding_box', 'tile_size', 'quantize', 'expected'), data_test_compute_coordinates) -def test_compute_coordinates( - bounding_box: BoundingBox, - tile_size: TileSize, - quantize: bool, - expected: CoordinatesSet, -) -> None: - coordinates = compute_coordinates( - bounding_box=bounding_box, - tile_size=tile_size, - quantize=quantize, - ) - - np.testing.assert_array_equal(coordinates, expected) - - -@patch('aviary._functional.geodata.grid_generator._generate_tiles') -@patch('aviary._functional.geodata.grid_generator.compute_coordinates') -def test_generate_grid( - mocked_compute_coordinates: MagicMock, - mocked__generate_tiles: MagicMock, -) -> None: - bounding_box = BoundingBox( - x_min=-128, - y_min=-128, - x_max=128, - y_max=128, - ) - tile_size = 128 - epsg_code = 25832 - quantize = True - expected_tiles = [ - box(-128, -128, 0, 0), - box(0, -128, 128, 0), - box(-128, 0, 0, 128), - box(0, 0, 128, 128), - ] - mocked__generate_tiles.return_value = expected_tiles - expected = gpd.GeoDataFrame( - geometry=mocked__generate_tiles.return_value, - crs=f'EPSG:{epsg_code}', - ) - grid = generate_grid( - bounding_box=bounding_box, - tile_size=tile_size, - epsg_code=epsg_code, - quantize=quantize, - ) - - mocked_compute_coordinates.assert_called_once_with( - bounding_box=bounding_box, - tile_size=tile_size, - quantize=quantize, - ) - mocked__generate_tiles.assert_called_once_with( - coordinates=mocked_compute_coordinates.return_value, - tile_size=tile_size, - ) - gpd.testing.assert_geodataframe_equal(grid, expected) - - -@pytest.mark.parametrize(('coordinates', 'tile_size', 'expected'), data_test__generate_tiles) -def test__generate_tiles( - coordinates: CoordinatesSet, - tile_size: TileSize, - expected: list[Polygon], -) -> None: - tiles = _generate_tiles( - coordinates=coordinates, - tile_size=tile_size, - ) - - assert all(polygon.equals(expected[i]) for i, polygon in enumerate(tiles)) diff --git a/tests/core/data/data_test_bounding_box.py b/tests/core/data/data_test_bounding_box.py index 39729008..080ea2eb 100644 --- a/tests/core/data/data_test_bounding_box.py +++ b/tests/core/data/data_test_bounding_box.py @@ -3,6 +3,7 @@ import geopandas as gpd from shapely.geometry import ( + MultiPolygon, Point, Polygon, box, @@ -400,12 +401,14 @@ ), get_bounding_box(), ), - # test case 6: gdf contains points + # test case 6: gdf contains a multi polygon ( gpd.GeoDataFrame( geometry=[ - Point(-128, -64), - Point(128, 192), + MultiPolygon([ + Polygon([[-128, -64], [-96, -64], [-96, -32], [-128, -32]]), + Polygon([[96, 160], [128, 160], [128, 192], [96, 192]]), + ]), ], ), get_bounding_box(), @@ -418,6 +421,13 @@ gpd.GeoDataFrame(), re.escape('Invalid gdf! The geodataframe must contain at least one geometry.'), ), + # test case 2: gdf contains geometries other than polygons + ( + gpd.GeoDataFrame( + geometry=[Point(0, 0)], + ), + re.escape('Invalid gdf! The geodataframe must contain only polygons.'), + ), ] data_test_bounding_box_getitem = [ diff --git a/tests/core/data/data_test_process_area.py b/tests/core/data/data_test_process_area.py index 8312b94d..757dbe83 100644 --- a/tests/core/data/data_test_process_area.py +++ b/tests/core/data/data_test_process_area.py @@ -4,6 +4,7 @@ import geopandas as gpd import numpy as np from shapely.geometry import ( + MultiPolygon, Point, Polygon, box, @@ -444,6 +445,46 @@ tile_size=128, ), ), + # test case 7: gdf contains a multi polygon and quantize is False + ( + gpd.GeoDataFrame( + geometry=[ + MultiPolygon([ + Polygon([[-128, -64], [-96, -64], [-96, -32], [-128, -32]]), + Polygon([[96, 160], [128, 160], [128, 192], [96, 192]]), + ]), + ], + ), + 128, + False, + ProcessArea( + coordinates=np.array( + [[-128, -64], [0, 64]], + dtype=np.int32, + ), + tile_size=128, + ), + ), + # test case 8: gdf contains a multi polygon and quantize is True + ( + gpd.GeoDataFrame( + geometry=[ + MultiPolygon([ + Polygon([[-128, -64], [-96, -64], [-96, -32], [-128, -32]]), + Polygon([[96, 160], [128, 160], [128, 192], [96, 192]]), + ]), + ], + ), + 128, + True, + ProcessArea( + coordinates=np.array( + [[-128, -128], [0, 128]], + dtype=np.int32, + ), + tile_size=128, + ), + ), ] data_test_process_area_from_gdf_exceptions = [ diff --git a/tests/core/test_bounding_box.py b/tests/core/test_bounding_box.py index 0d19360f..b2421a0a 100644 --- a/tests/core/test_bounding_box.py +++ b/tests/core/test_bounding_box.py @@ -36,6 +36,7 @@ def test_bounding_box_init() -> None: y_min = -64 x_max = 128 y_max = 192 + bounding_box = BoundingBox( x_min=x_min, y_min=y_min, @@ -231,6 +232,7 @@ def test_bounding_box_buffer_exceptions( def test_bounding_box_buffer_defaults() -> None: signature = inspect.signature(BoundingBox.buffer) inplace = signature.parameters['inplace'].default + expected_inplace = False assert inplace is expected_inplace @@ -300,6 +302,7 @@ def test_bounding_box_quantize_exceptions( def test_bounding_box_quantize_defaults() -> None: signature = inspect.signature(BoundingBox.quantize) inplace = signature.parameters['inplace'].default + expected_inplace = False assert inplace is expected_inplace @@ -313,10 +316,10 @@ def test_bounding_box_to_gdf( gdf = bounding_box.to_gdf(epsg_code=epsg_code) expected_geometry = [box(-128, -64, 128, 192)] - expected_epsg_code = 25832 + expected_epsg_code = 'EPSG:25832' expected = gpd.GeoDataFrame( geometry=expected_geometry, - crs=f'EPSG:{expected_epsg_code}', + crs=expected_epsg_code, ) gpd.testing.assert_geodataframe_equal(gdf, expected) diff --git a/tests/geodata/conftest.py b/tests/geodata/conftest.py index 743433b0..e75fca49 100644 --- a/tests/geodata/conftest.py +++ b/tests/geodata/conftest.py @@ -4,7 +4,6 @@ import numpy as np import pytest -from aviary.core.bounding_box import BoundingBox from aviary.core.enums import ( GeospatialFilterMode, SetFilterMode, @@ -27,7 +26,6 @@ SimplifyPostprocessor, ValuePostprocessor, ) -from aviary.geodata.grid_generator import GridGenerator @pytest.fixture(scope='session') @@ -105,21 +103,6 @@ def geospatial_filter() -> GeospatialFilter: ) -@pytest.fixture(scope='session') -def grid_generator() -> GridGenerator: - bounding_box = BoundingBox( - x_min=-128, - y_min=-128, - x_max=128, - y_max=128, - ) - epsg_code = 25832 - return GridGenerator( - bounding_box=bounding_box, - epsg_code=epsg_code, - ) - - @pytest.fixture(scope='session') def mask_filter() -> MaskFilter: mask = np.array([0, 1, 0, 1], dtype=np.bool_) diff --git a/tests/geodata/test_grid_generator.py b/tests/geodata/test_grid_generator.py deleted file mode 100644 index 29a613f2..00000000 --- a/tests/geodata/test_grid_generator.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest.mock import MagicMock, patch - -from aviary.core.bounding_box import BoundingBox -from aviary.geodata.grid_generator import GridGenerator - - -def test_init() -> None: - bounding_box = BoundingBox( - x_min=-128, - y_min=-128, - x_max=128, - y_max=128, - ) - epsg_code = 25832 - grid_generator = GridGenerator( - bounding_box=bounding_box, - epsg_code=epsg_code, - ) - - assert grid_generator.bounding_box == bounding_box - assert grid_generator.epsg_code == epsg_code - - -@patch('aviary.geodata.grid_generator.compute_coordinates') -def test_compute_coordinates( - mocked_compute_coordinates: MagicMock, - grid_generator: GridGenerator, -) -> None: - tile_size = 128 - quantize = True - expected = 'expected' - mocked_compute_coordinates.return_value = expected - coordinates = grid_generator.compute_coordinates( - tile_size=tile_size, - quantize=quantize, - ) - - mocked_compute_coordinates.assert_called_once_with( - bounding_box=grid_generator.bounding_box, - tile_size=tile_size, - quantize=quantize, - ) - assert coordinates == expected - - -@patch('aviary.geodata.grid_generator.generate_grid') -def test_generate_grid( - mocked_generate_grid: MagicMock, - grid_generator: GridGenerator, -) -> None: - tile_size = 128 - quantize = True - expected = 'expected' - mocked_generate_grid.return_value = expected - grid = grid_generator.generate_grid( - tile_size=tile_size, - quantize=quantize, - ) - - mocked_generate_grid.assert_called_once_with( - bounding_box=grid_generator.bounding_box, - tile_size=tile_size, - epsg_code=grid_generator.epsg_code, - quantize=quantize, - ) - assert grid == expected