diff --git a/.github/workflows/_tests.yml b/.github/workflows/_tests.yml index 709b350ce..d1b63db68 100644 --- a/.github/workflows/_tests.yml +++ b/.github/workflows/_tests.yml @@ -8,10 +8,10 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] include: - os: macos-latest - python-version: "3.10" + python-version: "3.11" env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1b505cb..9b426c21e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `include_center` parameter to Neighbourhoods [#288](https://github.com/srai-lab/srai/issues/288) -- Added `__version__` entry to library API. [#305](https://github.com/srai-lab/srai/issues/305) +- `__version__` entry to library API. [#305](https://github.com/srai-lab/srai/issues/305) +- `srai.h3` module with functions for translating list of h3 cells into shapely polygons and calculating local ij coordinates. ### Changed @@ -18,6 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - BREAKING! Renamed NetworkType to OSMNetworkType and made it importable directly from `srai.loaders` [#227](https://github.com/srai-lab/srai/issues/227) - BREAKING! Renamed osm_filter_type and grouped_osm_filter_type into OsmTagsFilter and GroupedOsmTagsFilter [#261](https://github.com/srai-lab/srai/issues/261) - Removed osmnx dependency version cap [#303](https://github.com/srai-lab/srai/issues/303) +- BREAKING! Removed `utils` module [#128](https://github.com/srai-lab/srai/issues/128) + - `srai.utils._optional` moved to `srai._optional` + - `srai.utils._pytorch_stubs` moved to `srai.embedders._pytorch_stubs` + - `srai.utils.download` moved to `srai.loaders.download` (and can be imported with `from srai.loaders import download_file`) + - `srai.utils.geocode` moved to `srai.regionalizers.geocode` (and can be imported with `from srai.regionalizers import geocode_to_region_gdf`) + - `srai.utils.geometry` and `srai.utils.merge` moved to `srai.geometry` + - `srai.utils.typing` moved to `srai._typing` ### Deprecated diff --git a/README.md b/README.md index 3203ea3dd..0eb2e40a6 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ Example with `OSMOnlineLoader`: ```python from srai.loaders import OSMOnlineLoader -from srai.utils import geocode_to_region_gdf from srai.plotting import plot_regions +from srai.regionalizers import geocode_to_region_gdf query = {"leisure": "park"} area = geocode_to_region_gdf("Wrocław, Poland") @@ -119,8 +119,8 @@ Road network downloading is a special case of OSM data downloading. To download ```python from srai.loaders import OSMNetworkType, OSMWayLoader -from srai.utils import geocode_to_region_gdf from srai.plotting import plot_regions +from srai.regionalizers import geocode_to_region_gdf area = geocode_to_region_gdf("Utrecht, Netherlands") loader = OSMWayLoader(OSMNetworkType.BIKE) @@ -142,9 +142,9 @@ To extract features from GTFS use `GTFSLoader`. It will extract trip count and a ```python from pathlib import Path -from srai.loaders import GTFSLoader -from srai.utils import geocode_to_region_gdf, download_file +from srai.loaders import GTFSLoader, download_file from srai.plotting import plot_regions +from srai.regionalizers import geocode_to_region_gdf area = geocode_to_region_gdf("Vienna, Austria") gtfs_file = Path("vienna_gtfs.zip") @@ -173,8 +173,7 @@ Regionalization is a process of dividing a given area into smaller regions. This Example: ```python -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf area = geocode_to_region_gdf("Berlin, Germany") regionalizer = H3Regionalizer(resolution=7) @@ -206,8 +205,7 @@ from srai.embedders import CountEmbedder from srai.joiners import IntersectionJoiner from srai.loaders import OSMOnlineLoader from srai.plotting import plot_regions, plot_numeric_data -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf loader = OSMOnlineLoader() regionalizer = H3Regionalizer(resolution=9) @@ -238,8 +236,7 @@ from srai.joiners import IntersectionJoiner from srai.loaders import OSMPbfLoader from srai.loaders.osm_loaders.filters import HEX2VEC_FILTER from srai.neighbourhoods.h3_neighbourhood import H3Neighbourhood -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf from srai.plotting import plot_regions, plot_numeric_data loader = OSMPbfLoader() diff --git a/docs/README.md b/docs/README.md index 2e975f597..777a6436d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,7 +97,7 @@ Example with `OSMOnlineLoader`: ```python from srai.loaders import OSMOnlineLoader -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import geocode_to_region_gdf from srai.plotting import plot_regions query = {"leisure": "park"} @@ -119,7 +119,7 @@ Road network downloading is a special case of OSM data downloading. To download ```python from srai.loaders import OSMNetworkType, OSMWayLoader -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import geocode_to_region_gdf from srai.plotting import plot_regions area = geocode_to_region_gdf("Utrecht, Netherlands") @@ -142,8 +142,8 @@ To extract features from GTFS use `GTFSLoader`. It will extract trip count and a ```python from pathlib import Path -from srai.loaders import GTFSLoader -from srai.utils import geocode_to_region_gdf, download_file +from srai.loaders import GTFSLoader, download_file +from srai.regionalizers import geocode_to_region_gdf from srai.plotting import plot_regions area = geocode_to_region_gdf("Vienna, Austria") @@ -173,8 +173,7 @@ Regionalization is a process of dividing a given area into smaller regions. This Example: ```python -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf area = geocode_to_region_gdf("Berlin, Germany") regionalizer = H3Regionalizer(resolution=7) @@ -206,8 +205,7 @@ from srai.embedders import CountEmbedder from srai.joiners import IntersectionJoiner from srai.loaders import OSMOnlineLoader from srai.plotting import plot_regions, plot_numeric_data -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf loader = OSMOnlineLoader() regionalizer = H3Regionalizer(resolution=9) @@ -238,8 +236,7 @@ from srai.joiners import IntersectionJoiner from srai.loaders import OSMPbfLoader from srai.loaders.osm_loaders.filters import HEX2VEC_FILTER from srai.neighbourhoods.h3_neighbourhood import H3Neighbourhood -from srai.regionalizers import H3Regionalizer -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf from srai.plotting import plot_regions, plot_numeric_data loader = OSMPbfLoader() diff --git a/examples/embedders/contextual_count_embedder.ipynb b/examples/embedders/contextual_count_embedder.ipynb index 286a08609..ae8f9b7d4 100644 --- a/examples/embedders/contextual_count_embedder.ipynb +++ b/examples/embedders/contextual_count_embedder.ipynb @@ -32,7 +32,7 @@ "metadata": {}, "outputs": [], "source": [ - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import geocode_to_region_gdf\n", "\n", "area_gdf = geocode_to_region_gdf(\"Lisboa, PT\")\n", "plot_regions(area_gdf)" diff --git a/examples/embedders/hex2vec_embedder.ipynb b/examples/embedders/hex2vec_embedder.ipynb index 311e6fc24..5e8bfac50 100644 --- a/examples/embedders/hex2vec_embedder.ipynb +++ b/examples/embedders/hex2vec_embedder.ipynb @@ -10,8 +10,7 @@ "from srai.joiners import IntersectionJoiner\n", "from srai.loaders import OSMOnlineLoader\n", "from srai.neighbourhoods import H3Neighbourhood\n", - "from srai.regionalizers import H3Regionalizer\n", - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf\n", "from srai.plotting import plot_regions, plot_numeric_data\n", "from pytorch_lightning import seed_everything" ] diff --git a/examples/embedders/highway2vec_embedder.ipynb b/examples/embedders/highway2vec_embedder.ipynb index 8b6dfbafb..1b70309f7 100644 --- a/examples/embedders/highway2vec_embedder.ipynb +++ b/examples/embedders/highway2vec_embedder.ipynb @@ -32,7 +32,7 @@ "metadata": {}, "outputs": [], "source": [ - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import geocode_to_region_gdf\n", "\n", "area_gdf = geocode_to_region_gdf(\"Wrocław, PL\")\n", "plot_regions(area_gdf, tiles_style=\"CartoDB positron\")" diff --git a/examples/embedders/load_and_save.ipynb b/examples/embedders/load_and_save.ipynb index 0784bdb04..c04047c83 100644 --- a/examples/embedders/load_and_save.ipynb +++ b/examples/embedders/load_and_save.ipynb @@ -10,8 +10,7 @@ "from srai.joiners import IntersectionJoiner\n", "from srai.loaders import OSMOnlineLoader\n", "from srai.neighbourhoods import H3Neighbourhood\n", - "from srai.regionalizers import H3Regionalizer\n", - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf\n", "from srai.plotting import plot_regions, plot_numeric_data\n", "from pytorch_lightning import seed_everything" ] diff --git a/examples/loaders/gtfs_loader.ipynb b/examples/loaders/gtfs_loader.ipynb index 1d4cac089..b22dceb22 100644 --- a/examples/loaders/gtfs_loader.ipynb +++ b/examples/loaders/gtfs_loader.ipynb @@ -15,12 +15,11 @@ "outputs": [], "source": [ "from pathlib import Path\n", - "from srai.loaders import GTFSLoader\n", + "from srai.loaders import GTFSLoader, download_file\n", "import gtfs_kit as gk\n", "import geopandas as gpd\n", "from shapely.geometry import Point\n", - "from srai.constants import WGS84_CRS\n", - "from srai.utils import download_file" + "from srai.constants import WGS84_CRS" ] }, { diff --git a/examples/loaders/osm_online_loader.ipynb b/examples/loaders/osm_online_loader.ipynb index b96f02a80..30e1982da 100644 --- a/examples/loaders/osm_online_loader.ipynb +++ b/examples/loaders/osm_online_loader.ipynb @@ -17,7 +17,7 @@ "from srai.loaders.osm_loaders.filters.popular import get_popular_tags\n", "from srai.loaders.osm_loaders.filters import GEOFABRIK_LAYERS, HEX2VEC_FILTER\n", "from srai.loaders.osm_loaders import OSMOnlineLoader\n", - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import geocode_to_region_gdf\n", "from srai.plotting.folium_wrapper import plot_regions\n", "from functional import seq" ] diff --git a/examples/loaders/osm_pbf_loader.ipynb b/examples/loaders/osm_pbf_loader.ipynb index ad9239903..cc3c6175f 100644 --- a/examples/loaders/osm_pbf_loader.ipynb +++ b/examples/loaders/osm_pbf_loader.ipynb @@ -24,7 +24,8 @@ "from srai.loaders.osm_loaders.filters.popular import get_popular_tags\n", "from srai.loaders.osm_loaders import OSMPbfLoader\n", "from srai.constants import REGIONS_INDEX, WGS84_CRS\n", - "from srai.utils import buffer_geometry, geocode_to_region_gdf\n", + "from srai.regionalizers import geocode_to_region_gdf\n", + "from srai.geometry import buffer_geometry\n", "\n", "from shapely.geometry import Point, box\n", "import geopandas as gpd" diff --git a/examples/loaders/osm_tile_loader.ipynb b/examples/loaders/osm_tile_loader.ipynb index 2161559aa..b497086b6 100644 --- a/examples/loaders/osm_tile_loader.ipynb +++ b/examples/loaders/osm_tile_loader.ipynb @@ -15,7 +15,7 @@ "outputs": [], "source": [ "from srai.loaders.osm_loaders import OSMTileLoader\n", - "from srai.utils import geocode_to_region_gdf\n", + "from srai.regionalizers import geocode_to_region_gdf\n", "\n", "ZOOM = 9" ] diff --git a/examples/loaders/osm_way_loader.ipynb b/examples/loaders/osm_way_loader.ipynb index ae2140f36..87f320aa4 100644 --- a/examples/loaders/osm_way_loader.ipynb +++ b/examples/loaders/osm_way_loader.ipynb @@ -20,7 +20,7 @@ "from srai.loaders import OSMNetworkType, OSMWayLoader\n", "from srai.constants import WGS84_CRS, REGIONS_INDEX\n", "from srai.plotting.folium_wrapper import plot_regions\n", - "from srai.utils import geocode_to_region_gdf" + "from srai.regionalizers import geocode_to_region_gdf" ] }, { diff --git a/examples/neighbourhoods/adjacency_neighbourhood.ipynb b/examples/neighbourhoods/adjacency_neighbourhood.ipynb index 59632e10b..88cc76dad 100644 --- a/examples/neighbourhoods/adjacency_neighbourhood.ipynb +++ b/examples/neighbourhoods/adjacency_neighbourhood.ipynb @@ -12,8 +12,11 @@ "\n", "from srai.constants import WGS84_CRS\n", "from srai.neighbourhoods import AdjacencyNeighbourhood\n", - "from srai.regionalizers import AdministrativeBoundaryRegionalizer, VoronoiRegionalizer\n", - "from srai.utils.geocode import geocode_to_region_gdf\n", + "from srai.regionalizers import (\n", + " AdministrativeBoundaryRegionalizer,\n", + " VoronoiRegionalizer,\n", + " geocode_to_region_gdf,\n", + ")\n", "from srai.plotting.folium_wrapper import plot_regions, plot_neighbours, plot_all_neighbourhood" ] }, diff --git a/examples/neighbourhoods/h3_neighbourhood.ipynb b/examples/neighbourhoods/h3_neighbourhood.ipynb index eb1874e5a..a8157c34e 100644 --- a/examples/neighbourhoods/h3_neighbourhood.ipynb +++ b/examples/neighbourhoods/h3_neighbourhood.ipynb @@ -7,8 +7,7 @@ "outputs": [], "source": [ "from srai.neighbourhoods import H3Neighbourhood\n", - "from srai.regionalizers import H3Regionalizer\n", - "from srai.utils.geocode import geocode_to_region_gdf\n", + "from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf\n", "from srai.plotting.folium_wrapper import plot_neighbours, plot_all_neighbourhood" ] }, diff --git a/examples/regionalizers/administrative_boundary_regionalizer.ipynb b/examples/regionalizers/administrative_boundary_regionalizer.ipynb index e5c43a7a8..c9629f01e 100644 --- a/examples/regionalizers/administrative_boundary_regionalizer.ipynb +++ b/examples/regionalizers/administrative_boundary_regionalizer.ipynb @@ -10,9 +10,8 @@ "import plotly.express as px\n", "from shapely.geometry import Point, box\n", "\n", - "from srai.regionalizers import AdministrativeBoundaryRegionalizer\n", - "from srai.plotting.folium_wrapper import plot_regions\n", - "from srai.utils import geocode_to_region_gdf" + "from srai.regionalizers import AdministrativeBoundaryRegionalizer, geocode_to_region_gdf\n", + "from srai.plotting.folium_wrapper import plot_regions" ] }, { diff --git a/examples/regionalizers/voronoi_regionalizer.ipynb b/examples/regionalizers/voronoi_regionalizer.ipynb index f866ad7da..385ea7968 100644 --- a/examples/regionalizers/voronoi_regionalizer.ipynb +++ b/examples/regionalizers/voronoi_regionalizer.ipynb @@ -11,10 +11,9 @@ "import plotly.express as px\n", "from shapely.geometry import Point\n", "\n", - "from srai.regionalizers import VoronoiRegionalizer\n", + "from srai.regionalizers import VoronoiRegionalizer, geocode_to_region_gdf\n", "from srai.constants import WGS84_CRS\n", - "from srai.plotting.folium_wrapper import plot_regions\n", - "from srai.utils import geocode_to_region_gdf" + "from srai.plotting.folium_wrapper import plot_regions" ] }, { diff --git a/pdm.lock b/pdm.lock index 0f1d6dd43..f33d1a8f0 100644 --- a/pdm.lock +++ b/pdm.lock @@ -529,7 +529,7 @@ summary = "Hierarchical hexagonal geospatial indexing system" [[package]] name = "h3ronpy" -version = "0.17.2" +version = "0.17.4" summary = "Data science toolkit for the H3 geospatial grid" dependencies = [ "Shapely>=1.7", @@ -2360,7 +2360,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.2" groups = ["default", "all", "dev", "docs", "gtfs", "license", "lint", "osm", "performance", "plotting", "test", "torch", "visualization", "voronoi"] -content_hash = "sha256:fc6f33de0a32db546b016e11ef714b82da4b979d79ab0c31f90599fdd4f6a78f" +content_hash = "sha256:bf7006156f874b71f59d10d18a20457833beb6e8dacd6ac8c8afb13123453a19" [metadata.files] "aiohttp 3.8.5" = [ @@ -3111,11 +3111,11 @@ content_hash = "sha256:fc6f33de0a32db546b016e11ef714b82da4b979d79ab0c31f90599fdd {url = "https://files.pythonhosted.org/packages/fd/30/ce35791eac7efce2ff6458a94f1a936c8631418e0a0d2a8bf30fdd3ee51b/h3-4.0.0b2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e952df0562581c2a564fed48d5910822963307756de93157ba5a5f8535b42044"}, {url = "https://files.pythonhosted.org/packages/fe/aa/9a97f3f866f79232a07f30bc838bb48e46b12ccf9e40b052720db7fd70ee/h3-4.0.0b2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0489e8892472b9514825d480544bac854862ce5cf94c7f497936053565787954"}, ] -"h3ronpy 0.17.2" = [ - {url = "https://files.pythonhosted.org/packages/0f/87/bd9b72f3e127732e9e84f6ab14971c64d537b08344f80163a7ed3e76c410/h3ronpy-0.17.2.tar.gz", hash = "sha256:b0b1adb5c5709a1a63c6d6bcc20706a29fa7263e4d0d9d255c0bc9a0f0b26b76"}, - {url = "https://files.pythonhosted.org/packages/24/2d/8eeee30f3894940c0d47d8a3d85005fb9a8fc8e93fdbdbc0a702f23085e4/h3ronpy-0.17.2-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:43bc39f43f4d5342ace553296d23bee9d3ea3b10c0850ab9d01d2373a28bcdea"}, - {url = "https://files.pythonhosted.org/packages/90/23/991ba93c9f8333bd2cf3d3333bc85336cfc189a20ff9c2c8e7464e492ae3/h3ronpy-0.17.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:14a5b5e14a7d54e06534fc94a11aa22792114dfab9a09e4a4c4ee19fe94e5c56"}, - {url = "https://files.pythonhosted.org/packages/f2/f3/d107af14a392b0bae1fecab3c8105caa7ca817320bd9be0c528901b25106/h3ronpy-0.17.2-cp38-abi3-win_amd64.whl", hash = "sha256:622e325abe3b276ed264d855a3c9ff76ab5cac037a92961f4c4853e45973a9fa"}, +"h3ronpy 0.17.4" = [ + {url = "https://files.pythonhosted.org/packages/84/f1/7f41961ad818f9b49fb16e68bc704218f03ad65beef48b96aefc4c0aea54/h3ronpy-0.17.4-cp38-abi3-win_amd64.whl", hash = "sha256:bcdd460dcf69cdfb03aca5e0d8599e1fe670d9b40e7a69133996d474cde5a742"}, + {url = "https://files.pythonhosted.org/packages/ad/bf/6feca53dd0797fd83e117b8547c1cba41e46782f824d2b1b2b57dac84bd4/h3ronpy-0.17.4.tar.gz", hash = "sha256:1a341d82122366fbbaef29550449d7c67854ad351f15be02e8d600b8cb810fed"}, + {url = "https://files.pythonhosted.org/packages/c0/e3/3362e56e1b6aeb6e21dd8d2a2c0f8ecb48a8132f39772c6f3478f95e5019/h3ronpy-0.17.4-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:82b761c2febd840779866d1370a8860fcf0457c3c6c94d296f8abe31e2028cf0"}, + {url = "https://files.pythonhosted.org/packages/cb/bc/1ebeee02871998301835da8ad1d26e15bf514e89e70179118ee5ac5036ed/h3ronpy-0.17.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dbc6f272b48d0824f742be89eae59a13e3e6f3932db40f39f8c268cea6d2787a"}, ] "haversine 2.8.0" = [ {url = "https://files.pythonhosted.org/packages/b9/6b/0a774af6a2eea772aa99e5fbc7af7711eba02ff0dee3e71838c1b5926ef5/haversine-2.8.0.tar.gz", hash = "sha256:cca39afd2ae5f1e6ed9231b332395bb8afb2e0a64edf70c238c176492e60c150"}, diff --git a/pyproject.toml b/pyproject.toml index c2261caa6..302763a69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "s2", "typeguard", "requests", - "h3ronpy", + "h3ronpy>=0.17.4", ] requires-python = ">=3.8" readme = "README.md" @@ -59,12 +59,7 @@ Changelog = "https://github.com/srai-lab/srai/blob/main/CHANGELOG.md" # add tests # pdm add -G osm -osm = [ - "osmium", - "osmnx", - "overpass", - "pillow", -] +osm = ["osmium", "osmnx", "overpass", "pillow"] # pdm add -G voronoi voronoi = ["pymap3d", "haversine", "scipy", "spherical-geometry"] # pdm add -G gtfs diff --git a/srai/utils/_optional.py b/srai/_optional.py similarity index 100% rename from srai/utils/_optional.py rename to srai/_optional.py diff --git a/srai/utils/typing.py b/srai/_typing.py similarity index 100% rename from srai/utils/typing.py rename to srai/_typing.py diff --git a/srai/embedders/_base.py b/srai/embedders/_base.py index e99c077c2..0ac30abc6 100644 --- a/srai/embedders/_base.py +++ b/srai/embedders/_base.py @@ -13,7 +13,7 @@ from pytorch_lightning import LightningModule except ImportError: - from srai.utils._pytorch_stubs import LightningModule + from srai.embedders._pytorch_stubs import LightningModule class Model(LightningModule): # type: ignore diff --git a/srai/utils/_pytorch_stubs.py b/srai/embedders/_pytorch_stubs.py similarity index 100% rename from srai/utils/_pytorch_stubs.py rename to srai/embedders/_pytorch_stubs.py diff --git a/srai/embedders/gtfs2vec/embedder.py b/srai/embedders/gtfs2vec/embedder.py index 5629e8b2f..29a8a6350 100644 --- a/srai/embedders/gtfs2vec/embedder.py +++ b/srai/embedders/gtfs2vec/embedder.py @@ -17,11 +17,11 @@ import numpy as np import pandas as pd +from srai._optional import import_optional_dependencies from srai.embedders import Embedder, ModelT from srai.embedders.gtfs2vec.model import GTFS2VecModel from srai.exceptions import ModelNotFitException from srai.loaders.gtfs_loader import GTFS2VEC_DIRECTIONS_PREFIX, GTFS2VEC_TRIPS_PREFIX -from srai.utils._optional import import_optional_dependencies class GTFS2VecEmbedder(Embedder): diff --git a/srai/embedders/gtfs2vec/model.py b/srai/embedders/gtfs2vec/model.py index dc793c663..08bb97440 100644 --- a/srai/embedders/gtfs2vec/model.py +++ b/srai/embedders/gtfs2vec/model.py @@ -8,8 +8,8 @@ """ from typing import TYPE_CHECKING, Any +from srai._optional import import_optional_dependencies from srai.embedders import Model -from srai.utils._optional import import_optional_dependencies if TYPE_CHECKING: # pragma: no cover import torch diff --git a/srai/embedders/hex2vec/embedder.py b/srai/embedders/hex2vec/embedder.py index d85fdd74a..7d79c58dc 100644 --- a/srai/embedders/hex2vec/embedder.py +++ b/srai/embedders/hex2vec/embedder.py @@ -14,12 +14,12 @@ import numpy as np import pandas as pd +from srai._optional import import_optional_dependencies from srai.embedders import CountEmbedder, ModelT from srai.embedders.hex2vec.model import Hex2VecModel from srai.embedders.hex2vec.neighbour_dataset import NeighbourDataset from srai.exceptions import ModelNotFitException from srai.neighbourhoods import Neighbourhood -from srai.utils._optional import import_optional_dependencies T = TypeVar("T") diff --git a/srai/embedders/hex2vec/model.py b/srai/embedders/hex2vec/model.py index 8b8aca252..33df4db0e 100644 --- a/srai/embedders/hex2vec/model.py +++ b/srai/embedders/hex2vec/model.py @@ -8,8 +8,8 @@ """ from typing import TYPE_CHECKING, List, Tuple +from srai._optional import import_optional_dependencies from srai.embedders import Model -from srai.utils._optional import import_optional_dependencies if TYPE_CHECKING: # pragma: no cover import torch diff --git a/srai/embedders/hex2vec/neighbour_dataset.py b/srai/embedders/hex2vec/neighbour_dataset.py index 8e269dc13..88e796e3d 100644 --- a/srai/embedders/hex2vec/neighbour_dataset.py +++ b/srai/embedders/hex2vec/neighbour_dataset.py @@ -13,8 +13,8 @@ import pandas as pd from tqdm import tqdm +from srai._optional import import_optional_dependencies from srai.neighbourhoods import Neighbourhood -from srai.utils._optional import import_optional_dependencies if TYPE_CHECKING: # pragma: no cover import torch @@ -23,7 +23,7 @@ from torch.utils.data import Dataset except ImportError: - from srai.utils._pytorch_stubs import Dataset + from srai.embedders._pytorch_stubs import Dataset T = TypeVar("T") diff --git a/srai/embedders/highway2vec/embedder.py b/srai/embedders/highway2vec/embedder.py index cf6d665b3..15bcb8eb5 100644 --- a/srai/embedders/highway2vec/embedder.py +++ b/srai/embedders/highway2vec/embedder.py @@ -13,9 +13,9 @@ import geopandas as gpd import pandas as pd +from srai._optional import import_optional_dependencies from srai.embedders import Embedder, ModelT from srai.exceptions import ModelNotFitException -from srai.utils._optional import import_optional_dependencies from .model import Highway2VecModel diff --git a/srai/embedders/highway2vec/model.py b/srai/embedders/highway2vec/model.py index 5f1b4a329..31c0dccb7 100644 --- a/srai/embedders/highway2vec/model.py +++ b/srai/embedders/highway2vec/model.py @@ -8,8 +8,8 @@ """ from typing import TYPE_CHECKING +from srai._optional import import_optional_dependencies from srai.embedders import Model -from srai.utils._optional import import_optional_dependencies if TYPE_CHECKING: # pragma: no cover import torch diff --git a/srai/utils/geometry.py b/srai/geometry.py similarity index 69% rename from srai/utils/geometry.py rename to srai/geometry.py index eb96ae44e..aea07bb05 100644 --- a/srai/utils/geometry.py +++ b/srai/geometry.py @@ -1,10 +1,10 @@ """Utility geometry operations functions.""" -from typing import List +from typing import List, Union import geopandas as gpd import pyproj from functional import seq -from shapely.geometry import Polygon +from shapely.geometry import MultiPolygon, Polygon from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry from shapely.ops import transform as shapely_transform @@ -71,3 +71,39 @@ def buffer_geometry(geometry: BaseGeometry, meters: float) -> BaseGeometry: bufferred_projected_geometry = projected_geometry.buffer(meters) return shapely_transform(aeqd_to_wgs84, bufferred_projected_geometry) + + +def merge_disjointed_polygons(polygons: List[Union[Polygon, MultiPolygon]]) -> MultiPolygon: + """ + Merges all polygons into a single MultiPolygon. + + Input polygons are expected to be disjointed. + + Args: + polygons (List[Union[Polygon, MultiPolygon]]): List of polygons to merge + + Returns: + MultiPolygon: Merged polygon + """ + single_polygons = [] + for geom in polygons: + if isinstance(geom, Polygon): + single_polygons.append(geom) + else: + single_polygons.extend(geom.geoms) + return MultiPolygon(single_polygons) + + +def merge_disjointed_gdf_geometries(gdf: gpd.GeoDataFrame) -> MultiPolygon: + """ + Merges geometries from a GeoDataFrame into a single MultiPolygon. + + Input geometries are expected to be disjointed. + + Args: + gdf (gpd.GeoDataFrame): GeoDataFrame with geometries to merge. + + Returns: + MultiPolygon: Merged polygon + """ + return merge_disjointed_polygons(list(gdf.geometry)) diff --git a/srai/h3.py b/srai/h3.py new file mode 100644 index 000000000..758d24a6b --- /dev/null +++ b/srai/h3.py @@ -0,0 +1,252 @@ +"""Utility H3 related functions.""" + +from sys import platform +from typing import Iterable, List, Literal, Tuple, Union, overload + +import geopandas as gpd +import h3 +import numpy as np +import numpy.typing as npt +from h3ronpy.arrow.vector import cells_to_wkb_polygons, wkb_to_cells +from shapely.geometry import Point, Polygon +from shapely.geometry.base import BaseGeometry + +from srai.constants import GEOMETRY_COLUMN, WGS84_CRS +from srai.geometry import buffer_geometry + + +def shapely_geometry_to_h3( + geometry: Union[BaseGeometry, Iterable[BaseGeometry], gpd.GeoSeries, gpd.GeoDataFrame], + h3_resolution: int, + buffer: bool = True, +) -> List[str]: + """ + Convert Shapely geometry to H3 indexes. + + Args: + geometry (Union[BaseGeometry, Iterable[BaseGeometry], GeoSeries, GeoDataFrame]): + Shapely geometry to be converted. + h3_resolution (int): H3 resolution of the cells. See [1] for a full comparison. + buffer (bool, optional): Whether to fully cover the geometries with + H3 Cells (visible on the borders). Defaults to True. + + Returns: + List[str]: List of H3 indexes that cover a given geometry. + + Raises: + ValueError: If resolution is not between 0 and 15. + + References: + 1. https://h3geo.org/docs/core-library/restable/ + """ + if not (0 <= h3_resolution <= 15): + raise ValueError(f"Resolution {h3_resolution} is not between 0 and 15.") + + if _is_macos(): + return _polygon_to_h3_index_raw(geometry, h3_resolution, buffer) + + wkb = [] + if isinstance(geometry, gpd.GeoSeries): + wkb = geometry.to_wkb() + elif isinstance(geometry, gpd.GeoDataFrame): + wkb = geometry[GEOMETRY_COLUMN].to_wkb() + elif isinstance(geometry, Iterable): + wkb = [sub_geometry.wkb for sub_geometry in geometry] + else: + wkb = [geometry.wkb] + + h3_indexes = wkb_to_cells( + wkb, resolution=h3_resolution, all_intersecting=buffer, flatten=True + ).unique() + + return [h3.int_to_str(h3_index) for h3_index in h3_indexes.tolist()] + + +def _polygon_to_h3_index_raw( + geometry: Union[BaseGeometry, Iterable[BaseGeometry], gpd.GeoSeries, gpd.GeoDataFrame], + h3_resolution: int, + buffer: bool = True, +) -> List[str]: + def _polygon_shapely_to_h3( + geometry: Union[Point, Polygon], h3_resolution: int, buffer: bool + ) -> List[str]: + if isinstance(geometry, Point): + return [h3.latlng_to_cell(geometry.y, geometry.x, h3_resolution)] + + buffer_distance_meters = 2 * h3.average_hexagon_edge_length(h3_resolution, unit="m") + + buffered_geometry = ( + buffer_geometry(geometry, buffer_distance_meters) if buffer else geometry + ) + + exterior = [coord[::-1] for coord in list(buffered_geometry.exterior.coords)] + interiors = [ + [coord[::-1] for coord in list(interior.coords)] + for interior in buffered_geometry.interiors + ] + h3_cells: List[str] = h3.polygon_to_cells(h3.Polygon(exterior, *interiors), h3_resolution) + + if buffer: + h3_cells = [ + h3_cell + for h3_cell in h3_cells + if _h3_index_to_polygon_raw(h3_cell).intersects(geometry) + ] + + return h3_cells + + from functional import seq + + geoseries: gpd.GeoSeries + + if isinstance(geometry, gpd.GeoSeries): + geoseries = geometry + elif isinstance(geometry, gpd.GeoDataFrame): + geoseries = geometry[GEOMETRY_COLUMN] + elif isinstance(geometry, Iterable): + geoseries = gpd.GeoSeries(geometry, crs=WGS84_CRS) + else: + return _polygon_to_h3_index_raw([geometry], h3_resolution, buffer) + + geoseries = geoseries.explode(ignore_index=True, index_parts=True) + + h3_indexes: List[str] = ( + seq(geoseries) + .flat_map(lambda polygon: _polygon_shapely_to_h3(polygon, h3_resolution, buffer)) + .distinct() + .to_list() + ) + + return h3_indexes + + +# TODO: write tests (#322) +def h3_to_geoseries(h3_index: Union[int, str, Iterable[Union[int, str]]]) -> gpd.GeoSeries: + """ + Convert H3 index to GeoPandas GeoSeries. + + Args: + h3_index (Union[int, str, Iterable[Union[int, str]]]): H3 index (or list of indexes) + to be converted. + + Returns: + GeoSeries: Geometries as GeoSeries with default CRS applied. + """ + if isinstance(h3_index, (str, int)): + return h3_to_geoseries([h3_index]) + else: + if _is_macos(): + return gpd.GeoSeries( + [_h3_index_to_polygon_raw(h3_cell) for h3_cell in h3_index], crs=WGS84_CRS + ) + + h3_int_indexes = ( + h3_cell if isinstance(h3_cell, int) else h3.str_to_int(h3_cell) for h3_cell in h3_index + ) + return gpd.GeoSeries.from_wkb(cells_to_wkb_polygons(h3_int_indexes), crs=WGS84_CRS) + + +def _h3_index_to_polygon_raw(h3_index: Union[int, str]) -> Polygon: + if isinstance(h3_index, int): + h3_index = h3.int_to_str(h3_index) + h3_poly = h3.cells_to_polygons([h3_index])[0] + + return Polygon( + shell=[coord[::-1] for coord in h3_poly.outer], + holes=[[coord[::-1] for coord in hole] for hole in h3_poly.holes], + ) + + +@overload +def h3_to_shapely_geometry(h3_index: Union[int, str]) -> Polygon: + ... + + +@overload +def h3_to_shapely_geometry(h3_index: Iterable[Union[int, str]]) -> List[Polygon]: + ... + + +# TODO: write tests (#322) +def h3_to_shapely_geometry( + h3_index: Union[int, str, Iterable[Union[int, str]]] +) -> Union[Polygon, List[Polygon]]: + """ + Convert H3 index to Shapely polygon. + + Args: + h3_index (Union[int, str, Iterable[Union[int, str]]]): H3 index (or list of indexes) + to be converted. + + Returns: + Union[Polygon, List[Polygon]]: Converted polygon (or list of polygons). + """ + if isinstance(h3_index, (str, int)): + coords = h3.cell_to_boundary(h3_index, geo_json=True) + return Polygon(coords) + return h3_to_geoseries(h3_index).values.tolist() + + +@overload +def get_local_ij_index(origin_index: str, h3_index: str) -> Tuple[int, int]: + ... + + +@overload +def get_local_ij_index( + origin_index: str, h3_index: List[str], return_as_numpy: Literal[False] +) -> List[Tuple[int, int]]: + ... + + +@overload +def get_local_ij_index( + origin_index: str, h3_index: List[str], return_as_numpy: Literal[True] +) -> npt.NDArray[np.int8]: + ... + + +# Last fallback needed as per documentation: +# https://mypy.readthedocs.io/en/stable/literal_types.html#literal-types +@overload +def get_local_ij_index( + origin_index: str, h3_index: List[str], return_as_numpy: bool +) -> Union[List[Tuple[int, int]], npt.NDArray[np.int8]]: + ... + + +def get_local_ij_index( + origin_index: str, h3_index: Union[str, List[str]], return_as_numpy: bool = False +) -> Union[Tuple[int, int], List[Tuple[int, int]], npt.NDArray[np.int8]]: + """ + Calculate the local H3 ij index based on provided origin index. + + Wraps H3's cell_to_local_ij function and centers returned coordinates + around provided origin cell. + + Args: + origin_index (str): H3 index of the origin region. + h3_index (Union[str, List[str]]): H3 index of the second region or list of regions. + return_as_numpy (bool, optional): Flag whether to return calculated indexes as a Numpy array + or a list of tuples. + + Returns: + Union[Tuple[int, int], List[Tuple[int, int]], npt.NDArray[np.int8]]: The local ij index of + the second region (or regions) with respect to the first one. + """ + origin_coords = h3.cell_to_local_ij(origin_index, origin_index) + if isinstance(h3_index, str): + ijs = h3.cell_to_local_ij(origin_index, h3_index) + return (origin_coords[0] - ijs[0], origin_coords[1] - ijs[1]) + ijs = np.array([h3.cell_to_local_ij(origin_index, h3_cell) for h3_cell in h3_index]) + local_ijs = np.array(origin_coords) - ijs + + if not return_as_numpy: + local_ijs = [(coords[0], coords[1]) for coords in local_ijs] + + return local_ijs + + +def _is_macos() -> bool: + """Return flag if code is run on OS X.""" + return platform == "darwin" diff --git a/srai/loaders/__init__.py b/srai/loaders/__init__.py index a6da2fdd7..941793b79 100644 --- a/srai/loaders/__init__.py +++ b/srai/loaders/__init__.py @@ -7,6 +7,7 @@ """ from ._base import Loader +from .download import download_file from .geoparquet_loader import GeoparquetLoader from .gtfs_loader import GTFSLoader from .osm_loaders import OSMLoader, OSMOnlineLoader, OSMPbfLoader, OSMTileLoader @@ -22,4 +23,5 @@ "OSMPbfLoader", "OSMTileLoader", "OSMNetworkType", + "download_file", ] diff --git a/srai/utils/download.py b/srai/loaders/download.py similarity index 100% rename from srai/utils/download.py rename to srai/loaders/download.py diff --git a/srai/loaders/gtfs_loader.py b/srai/loaders/gtfs_loader.py index 8656152af..8c12e7ef1 100644 --- a/srai/loaders/gtfs_loader.py +++ b/srai/loaders/gtfs_loader.py @@ -17,9 +17,9 @@ import pandas as pd from shapely.geometry import Point +from srai._optional import import_optional_dependencies from srai.constants import GEOMETRY_COLUMN, WGS84_CRS from srai.loaders import Loader -from srai.utils._optional import import_optional_dependencies if TYPE_CHECKING: # pragma: no cover from gtfs_kit import Feed diff --git a/srai/loaders/osm_loaders/_base.py b/srai/loaders/osm_loaders/_base.py index b59c442f5..5a12b9816 100644 --- a/srai/loaders/osm_loaders/_base.py +++ b/srai/loaders/osm_loaders/_base.py @@ -8,13 +8,13 @@ import pandas as pd from tqdm import tqdm +from srai._typing import is_expected_type from srai.loaders import Loader from srai.loaders.osm_loaders.filters import ( GroupedOsmTagsFilter, OsmTagsFilter, merge_grouped_osm_tags_filter, ) -from srai.utils.typing import is_expected_type class OSMLoader(Loader, abc.ABC): diff --git a/srai/loaders/osm_loaders/filters/_typing.py b/srai/loaders/osm_loaders/filters/_typing.py index 83f4f4bf7..cdf95547b 100644 --- a/srai/loaders/osm_loaders/filters/_typing.py +++ b/srai/loaders/osm_loaders/filters/_typing.py @@ -1,7 +1,7 @@ """Module contains a dedicated type alias for OSM tags filter.""" from typing import Dict, List, Union, cast -from srai.utils.typing import is_expected_type +from srai._typing import is_expected_type OsmTagsFilter = Dict[str, Union[List[str], str, bool]] diff --git a/srai/loaders/osm_loaders/osm_online_loader.py b/srai/loaders/osm_loaders/osm_online_loader.py index 1894ac617..5df71f465 100644 --- a/srai/loaders/osm_loaders/osm_online_loader.py +++ b/srai/loaders/osm_loaders/osm_online_loader.py @@ -11,10 +11,10 @@ from functional import seq from tqdm import tqdm +from srai._optional import import_optional_dependencies from srai.constants import FEATURES_INDEX, GEOMETRY_COLUMN, WGS84_CRS from srai.loaders.osm_loaders._base import OSMLoader -from srai.loaders.osm_loaders.filters._typing import GroupedOsmTagsFilter, OsmTagsFilter -from srai.utils._optional import import_optional_dependencies +from srai.loaders.osm_loaders.filters import GroupedOsmTagsFilter, OsmTagsFilter class OSMOnlineLoader(OSMLoader): diff --git a/srai/loaders/osm_loaders/osm_pbf_loader.py b/srai/loaders/osm_loaders/osm_pbf_loader.py index c4b2454a6..29a5b889b 100644 --- a/srai/loaders/osm_loaders/osm_pbf_loader.py +++ b/srai/loaders/osm_loaders/osm_pbf_loader.py @@ -9,10 +9,10 @@ import geopandas as gpd import pandas as pd +from srai._optional import import_optional_dependencies from srai.constants import FEATURES_INDEX, GEOMETRY_COLUMN, WGS84_CRS from srai.loaders.osm_loaders._base import OSMLoader -from srai.loaders.osm_loaders.filters._typing import GroupedOsmTagsFilter, OsmTagsFilter -from srai.utils._optional import import_optional_dependencies +from srai.loaders.osm_loaders.filters import GroupedOsmTagsFilter, OsmTagsFilter class OSMPbfLoader(OSMLoader): diff --git a/srai/loaders/osm_loaders/osm_tile_loader.py b/srai/loaders/osm_loaders/osm_tile_loader.py index 36c777df5..d42a3d789 100644 --- a/srai/loaders/osm_loaders/osm_tile_loader.py +++ b/srai/loaders/osm_loaders/osm_tile_loader.py @@ -12,8 +12,8 @@ import pandas as pd import requests +from srai._optional import import_optional_dependencies from srai.regionalizers.slippy_map_regionalizer import SlippyMapRegionalizer -from srai.utils._optional import import_optional_dependencies from .osm_tile_data_collector import ( DataCollector, diff --git a/srai/loaders/osm_loaders/pbf_file_downloader.py b/srai/loaders/osm_loaders/pbf_file_downloader.py index d61849268..695f34414 100644 --- a/srai/loaders/osm_loaders/pbf_file_downloader.py +++ b/srai/loaders/osm_loaders/pbf_file_downloader.py @@ -19,7 +19,8 @@ from tqdm import tqdm from srai.constants import WGS84_CRS -from srai.utils import buffer_geometry, download_file, flatten_geometry, remove_interiors +from srai.geometry import buffer_geometry, flatten_geometry, remove_interiors +from srai.loaders import download_file class PbfFileDownloader: diff --git a/srai/loaders/osm_loaders/pbf_file_handler.py b/srai/loaders/osm_loaders/pbf_file_handler.py index bd58bc015..0461470c0 100644 --- a/srai/loaders/osm_loaders/pbf_file_handler.py +++ b/srai/loaders/osm_loaders/pbf_file_handler.py @@ -16,7 +16,7 @@ from tqdm import tqdm from srai.constants import FEATURES_INDEX, WGS84_CRS -from srai.loaders.osm_loaders.filters._typing import OsmTagsFilter +from srai.loaders.osm_loaders.filters import OsmTagsFilter if TYPE_CHECKING: import os diff --git a/srai/loaders/osm_way_loader/osm_way_loader.py b/srai/loaders/osm_way_loader/osm_way_loader.py index 6af725ada..886153117 100644 --- a/srai/loaders/osm_way_loader/osm_way_loader.py +++ b/srai/loaders/osm_way_loader/osm_way_loader.py @@ -14,10 +14,10 @@ from functional import seq from tqdm.auto import tqdm +from srai._optional import import_optional_dependencies from srai.constants import FEATURES_INDEX, GEOMETRY_COLUMN, WGS84_CRS from srai.exceptions import LoadedDataIsEmptyException from srai.loaders import Loader -from srai.utils._optional import import_optional_dependencies from . import constants diff --git a/srai/plotting/folium_wrapper.py b/srai/plotting/folium_wrapper.py index 329e504a5..082857437 100644 --- a/srai/plotting/folium_wrapper.py +++ b/srai/plotting/folium_wrapper.py @@ -14,10 +14,10 @@ import pandas as pd import plotly.express as px +from srai._optional import import_optional_dependencies from srai.constants import REGIONS_INDEX from srai.neighbourhoods import Neighbourhood from srai.neighbourhoods._base import IndexType -from srai.utils._optional import import_optional_dependencies import_optional_dependencies(dependency_group="plotting", modules=["folium", "plotly"]) diff --git a/srai/plotting/plotly_wrapper.py b/srai/plotting/plotly_wrapper.py index 63b2e7aca..fe47d13da 100644 --- a/srai/plotting/plotly_wrapper.py +++ b/srai/plotting/plotly_wrapper.py @@ -11,10 +11,10 @@ import plotly.graph_objs as go from shapely.geometry import Point +from srai._optional import import_optional_dependencies from srai.constants import REGIONS_INDEX, WGS84_CRS from srai.neighbourhoods import Neighbourhood from srai.neighbourhoods._base import IndexType -from srai.utils._optional import import_optional_dependencies import_optional_dependencies(dependency_group="plotting", modules=["plotly"]) diff --git a/srai/regionalizers/__init__.py b/srai/regionalizers/__init__.py index 7c75041f3..d373a4011 100644 --- a/srai/regionalizers/__init__.py +++ b/srai/regionalizers/__init__.py @@ -9,6 +9,7 @@ from ._base import Regionalizer from .administrative_boundary_regionalizer import AdministrativeBoundaryRegionalizer +from .geocode import geocode_to_region_gdf from .h3_regionalizer import H3Regionalizer from .s2_regionalizer import S2Regionalizer from .slippy_map_regionalizer import SlippyMapRegionalizer @@ -21,4 +22,5 @@ "S2Regionalizer", "VoronoiRegionalizer", "SlippyMapRegionalizer", + "geocode_to_region_gdf", ] diff --git a/srai/regionalizers/administrative_boundary_regionalizer.py b/srai/regionalizers/administrative_boundary_regionalizer.py index 703dcca2e..daf004a4f 100644 --- a/srai/regionalizers/administrative_boundary_regionalizer.py +++ b/srai/regionalizers/administrative_boundary_regionalizer.py @@ -17,10 +17,10 @@ from shapely.validation import make_valid from tqdm import tqdm +from srai._optional import import_optional_dependencies from srai.constants import GEOMETRY_COLUMN, REGIONS_INDEX, WGS84_CRS +from srai.geometry import flatten_geometry_series from srai.regionalizers import Regionalizer -from srai.utils import flatten_geometry_series -from srai.utils._optional import import_optional_dependencies class AdministrativeBoundaryRegionalizer(Regionalizer): diff --git a/srai/utils/geocode.py b/srai/regionalizers/geocode.py similarity index 100% rename from srai/utils/geocode.py rename to srai/regionalizers/geocode.py diff --git a/srai/regionalizers/h3_regionalizer.py b/srai/regionalizers/h3_regionalizer.py index 855c96b90..ac9171505 100644 --- a/srai/regionalizers/h3_regionalizer.py +++ b/srai/regionalizers/h3_regionalizer.py @@ -15,10 +15,9 @@ import geopandas as gpd -import h3 -from h3ronpy.arrow.vector import cells_to_wkb_polygons, wkb_to_cells from srai.constants import GEOMETRY_COLUMN, REGIONS_INDEX, WGS84_CRS +from srai.h3 import h3_to_geoseries, shapely_geometry_to_h3 from srai.regionalizers import Regionalizer @@ -71,15 +70,18 @@ def transform(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: gdf_exploded = self._explode_multipolygons(gdf_wgs84) - h3_indexes = wkb_to_cells( - gdf_exploded[GEOMETRY_COLUMN].to_wkb(), - resolution=self.resolution, - all_intersecting=self.buffer, - flatten=True, - ).unique() + h3_indexes = list( + set( + shapely_geometry_to_h3( + gdf_exploded[GEOMETRY_COLUMN], + h3_resolution=self.resolution, + buffer=self.buffer, + ) + ) + ) gdf_h3 = gpd.GeoDataFrame( - data={REGIONS_INDEX: [h3.int_to_str(h3_index) for h3_index in h3_indexes.tolist()]}, - geometry=gpd.GeoSeries.from_wkb(cells_to_wkb_polygons(h3_indexes)), + data={REGIONS_INDEX: h3_indexes}, + geometry=h3_to_geoseries(h3_indexes), crs=WGS84_CRS, ).set_index(REGIONS_INDEX) diff --git a/srai/regionalizers/voronoi_regionalizer.py b/srai/regionalizers/voronoi_regionalizer.py index 3ff51c30f..48c8e614d 100644 --- a/srai/regionalizers/voronoi_regionalizer.py +++ b/srai/regionalizers/voronoi_regionalizer.py @@ -9,9 +9,9 @@ import geopandas as gpd from shapely.geometry import Point, box +from srai._optional import import_optional_dependencies from srai.constants import GEOMETRY_COLUMN, REGIONS_INDEX, WGS84_CRS from srai.regionalizers import Regionalizer -from srai.utils._optional import import_optional_dependencies class VoronoiRegionalizer(Regionalizer): diff --git a/srai/utils/__init__.py b/srai/utils/__init__.py deleted file mode 100644 index 1bb34ddac..000000000 --- a/srai/utils/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Module containing different utility functions. - -Those are either used internally by other modules, or can be used to simplify spatial data -processing. -""" -from .download import download_file -from .geocode import geocode_to_region_gdf -from .geometry import buffer_geometry, flatten_geometry, flatten_geometry_series, remove_interiors -from .merge import merge_disjointed_gdf_geometries, merge_disjointed_polygons - -__all__ = [ - "download_file", - "geocode_to_region_gdf", - "buffer_geometry", - "flatten_geometry", - "flatten_geometry_series", - "remove_interiors", - "merge_disjointed_polygons", - "merge_disjointed_gdf_geometries", -] diff --git a/srai/utils/merge.py b/srai/utils/merge.py deleted file mode 100644 index 41ab5512b..000000000 --- a/srai/utils/merge.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Utility function for merging Shapely polygons.""" - -from typing import List, Union - -import geopandas as gpd -from shapely.geometry import MultiPolygon, Polygon - - -def merge_disjointed_polygons(polygons: List[Union[Polygon, MultiPolygon]]) -> MultiPolygon: - """ - Merges all polygons into a single MultiPolygon. - - Input polygons are expected to be disjointed. - - Args: - polygons (List[Union[Polygon, MultiPolygon]]): List of polygons to merge - - Returns: - MultiPolygon: Merged polygon - """ - single_polygons = [] - for geom in polygons: - if type(geom) is Polygon: - single_polygons.append(geom) - else: - single_polygons.extend(geom.geoms) - return MultiPolygon(single_polygons) - - -def merge_disjointed_gdf_geometries(gdf: gpd.GeoDataFrame) -> MultiPolygon: - """ - Merges geometries from a GeoDataFrame into a single MultiPolygon. - - Input geometries are expected to be disjointed. - - Args: - gdf (gpd.GeoDataFrame): GeoDataFrame with geometries to merge. - - Returns: - MultiPolygon: Merged polygon - """ - return merge_disjointed_polygons(list(gdf.geometry)) diff --git a/tests/embedders/hex2vec/generation.py b/tests/embedders/hex2vec/generation.py index 012246b19..8bdc90bfe 100644 --- a/tests/embedders/hex2vec/generation.py +++ b/tests/embedders/hex2vec/generation.py @@ -13,7 +13,7 @@ from srai.loaders.osm_loaders import OSMPbfLoader from srai.loaders.osm_loaders.filters import OsmTagsFilter from srai.neighbourhoods import H3Neighbourhood -from srai.utils import geocode_to_region_gdf +from srai.regionalizers import geocode_to_region_gdf from tests.embedders.hex2vec.constants import ENCODER_SIZES, TRAINER_KWARGS diff --git a/tests/h3/conftest.py b/tests/h3/conftest.py new file mode 100644 index 000000000..dda436deb --- /dev/null +++ b/tests/h3/conftest.py @@ -0,0 +1,109 @@ +"""Conftest for H3 tests.""" +from typing import List + +import geopandas as gpd +import pytest +from shapely import geometry + +from srai.constants import WGS84_CRS + + +@pytest.fixture # type: ignore +def gdf_single_point() -> gpd.GeoDataFrame: + """Get the point case.""" + return gpd.GeoDataFrame(geometry=[geometry.Point(17.9261, 50.6696)], crs=WGS84_CRS) + + +@pytest.fixture # type: ignore +def expected_point_h3_index() -> List[str]: + """Get expected h3 index for the point case.""" + return [ + "8a1e23c44b5ffff", + ] + + +@pytest.fixture # type: ignore +def gdf_polygons() -> gpd.GeoDataFrame: + """Get GeoDataFrame with two polygons.""" + return gpd.GeoDataFrame( + geometry=[ + geometry.Polygon( + shell=[ + (-1, 0), + (0, 0.5), + (1, 0), + (1, 1), + (0, 1), + ], + holes=[ + [ + (0.8, 0.9), + (0.9, 0.55), + (0.8, 0.3), + (0.5, 0.4), + ] + ], + ), + geometry.Polygon(shell=[(-0.25, 0), (0.25, 0), (0, 0.2)]), + ], + crs=WGS84_CRS, + ) + + +@pytest.fixture # type: ignore +def gdf_multipolygon() -> gpd.GeoDataFrame: + """Get GeoDataFrame with multipolygon.""" + return gpd.GeoDataFrame( + geometry=[ + geometry.MultiPolygon( + [ + ( + [ + (-1, 0), + (0, 0.5), + (1, 0), + (1, 1), + (0, 1), + ], + ( + [ + [ + (0.8, 0.9), + (0.9, 0.55), + (0.8, 0.3), + (0.5, 0.4), + ] + ] + ), + ), + ( + [(-0.25, 0), (0.25, 0), (0, 0.2)], + (), + ), + ] + ) + ], + crs=WGS84_CRS, + ) + + +@pytest.fixture # type: ignore +def expected_h3_indexes() -> List[str]: + """Get expected h3 indexes.""" + return [ + "837559fffffffff", + "83754efffffffff", + "83754cfffffffff", + "837541fffffffff", + "83755dfffffffff", + "837543fffffffff", + "83754afffffffff", + ] + + +@pytest.fixture # type: ignore +def expected_unbuffered_h3_indexes() -> List[str]: + """Get expected h3 index for the unbuffered case.""" + return [ + "83754efffffffff", + ] diff --git a/tests/h3/test_ij_coordinates.py b/tests/h3/test_ij_coordinates.py new file mode 100644 index 000000000..8ed34dbee --- /dev/null +++ b/tests/h3/test_ij_coordinates.py @@ -0,0 +1,106 @@ +"""H3 IJ coordinates tests.""" + +from typing import List + +import h3 +import numpy as np +import pytest + +from srai.h3 import get_local_ij_index + + +@pytest.mark.parametrize( + "h3_origin", + [ + "891e2040d4bffff", + "871e20400ffffff", + "821f77fffffffff", + "81743ffffffffff", + ], +) # type: ignore +def test_self_ok(h3_origin: str) -> None: + """Test checks if self coordinates are in origin.""" + coordinates = get_local_ij_index(origin_index=h3_origin, h3_index=h3_origin) + assert coordinates == (0, 0) + + +@pytest.mark.parametrize( + "h3_origin, h3_cell", + [ + ("871f53c93ffffff", "871f53c91ffffff"), + ("861fae207ffffff", "861fae22fffffff"), + ("81597ffffffffff", "813fbffffffffff"), + ("84be185ffffffff", "84be181ffffffff"), + ], +) # type: ignore +def test_string_ok(h3_origin: str, h3_cell: str) -> None: + """Test checks if pairs are in right orientation.""" + coordinates = get_local_ij_index(origin_index=h3_origin, h3_index=h3_cell) + assert coordinates == (0, 1) + + +@pytest.mark.parametrize( + "h3_origin, h3_cells", + [ + ( + "892a100d6d3ffff", + [ + "892a100896fffff", + "892a100d6d7ffff", + "892a100d6c3ffff", + "892a100d6dbffff", + "892a1008ba7ffff", + "892a100896bffff", + ], + ), + ( + "86195da4fffffff", + [ + "86194ad37ffffff", + "86194ad17ffffff", + "86194ada7ffffff", + "86195da5fffffff", + "86195da47ffffff", + "86195da6fffffff", + ], + ), + ( + "8a1e24aa5637fff", + [ + "8a1e24aa5627fff", + "8a1e24aa5607fff", + "8a1e24aa5617fff", + "8a1e24aa578ffff", + "8a1e24aa57affff", + "8a1e24aa571ffff", + ], + ), + ], +) # type: ignore +@pytest.mark.parametrize("return_as_numpy", [True, False]) # type: ignore +def test_list_ok(h3_origin: str, h3_cells: List[str], return_as_numpy: bool) -> None: + """Test checks if lists are parsed correctly.""" + coordinates = get_local_ij_index( + origin_index=h3_origin, h3_index=h3_cells, return_as_numpy=return_as_numpy + ) + + expected_coordinates = [(0, 1), (1, 1), (1, 0), (0, -1), (-1, -1), (-1, 0)] + + if return_as_numpy: + np.testing.assert_array_equal(coordinates, expected_coordinates) + else: + assert coordinates == expected_coordinates + + +@pytest.mark.parametrize( + "h3_origin, h3_cell", + [ + ("83a75dfffffffff", "83a791fffffffff"), + ("84a605bffffffff", "84a6021ffffffff"), + ("836200fffffffff", "837400fffffffff"), + ], +) # type: ignore +def test_pentagon_error(h3_origin: str, h3_cell: str) -> None: + """Test checks if method fails over pentagon pairs.""" + with pytest.raises(h3._cy.error_system.H3FailedError): + get_local_ij_index(origin_index=h3_origin, h3_index=h3_cell) diff --git a/tests/h3/test_shapely_conversion.py b/tests/h3/test_shapely_conversion.py new file mode 100644 index 000000000..b5c517158 --- /dev/null +++ b/tests/h3/test_shapely_conversion.py @@ -0,0 +1,90 @@ +"""H3 shapely conversion tests.""" + +from typing import Any, Callable, List +from unittest import TestCase + +import geopandas as gpd +import pytest +from shapely.geometry.base import BaseGeometry + +from srai.constants import GEOMETRY_COLUMN +from srai.h3 import shapely_geometry_to_h3 +from tests.regionalizers.test_h3_regionalizer import H3_RESOLUTION + +ut = TestCase() + + +def _gdf_noop(gdf_fixture: gpd.GeoDataFrame) -> gpd.GeoDataFrame: + return gdf_fixture + + +def _gdf_to_geoseries(gdf_fixture: gpd.GeoDataFrame) -> gpd.GeoSeries: + return gdf_fixture[GEOMETRY_COLUMN] + + +def _gdf_to_geometry_list(gdf_fixture: gpd.GeoDataFrame) -> List[BaseGeometry]: + return list(gdf_fixture[GEOMETRY_COLUMN]) + + +def _gdf_to_single_geometry(gdf_fixture: gpd.GeoDataFrame) -> BaseGeometry: + return gdf_fixture[GEOMETRY_COLUMN][0] + + +@pytest.mark.parametrize( + "geometry_fixture, resolution, expected_h3_cells_fixture", + [ + ("gdf_single_point", 10, "expected_point_h3_index"), + ("gdf_multipolygon", H3_RESOLUTION, "expected_unbuffered_h3_indexes"), + ("gdf_polygons", H3_RESOLUTION, "expected_unbuffered_h3_indexes"), + ], +) # type: ignore +@pytest.mark.parametrize( + "geometry_parser_function", + [_gdf_noop, _gdf_to_geoseries, _gdf_to_geometry_list], +) # type: ignore +def test_shapely_geometry_to_h3_unbuffered( + geometry_fixture: str, + resolution: int, + expected_h3_cells_fixture: str, + geometry_parser_function: Callable[[gpd.GeoDataFrame], Any], + request: pytest.FixtureRequest, +) -> None: + """Test checks if conversion from shapely to h3 works.""" + geometry = request.getfixturevalue(geometry_fixture) + expected_h3_cells = request.getfixturevalue(expected_h3_cells_fixture) + + parsed_geometry = geometry_parser_function(geometry) + h3_cells = shapely_geometry_to_h3( + geometry=parsed_geometry, h3_resolution=resolution, buffer=False + ) + ut.assertCountEqual(h3_cells, expected_h3_cells) + + +@pytest.mark.parametrize( + "geometry_fixture, resolution, expected_h3_cells_fixture", + [ + ("gdf_single_point", 10, "expected_point_h3_index"), + ("gdf_multipolygon", H3_RESOLUTION, "expected_h3_indexes"), + ("gdf_polygons", H3_RESOLUTION, "expected_h3_indexes"), + ], +) # type: ignore +@pytest.mark.parametrize( + "geometry_parser_function", + [_gdf_noop, _gdf_to_geoseries, _gdf_to_geometry_list], +) # type: ignore +def test_shapely_geometry_to_h3_buffered( + geometry_fixture: str, + resolution: int, + expected_h3_cells_fixture: str, + geometry_parser_function: Callable[[gpd.GeoDataFrame], Any], + request: pytest.FixtureRequest, +) -> None: + """Test checks if conversion from shapely to h3 with buffer works.""" + geometry = request.getfixturevalue(geometry_fixture) + expected_h3_cells = request.getfixturevalue(expected_h3_cells_fixture) + + parsed_geometry = geometry_parser_function(geometry) + h3_cells = shapely_geometry_to_h3( + geometry=parsed_geometry, h3_resolution=resolution, buffer=True + ) + ut.assertCountEqual(h3_cells, expected_h3_cells) diff --git a/tests/loaders/osm_loaders/filters/test_merge_filter_types.py b/tests/loaders/osm_loaders/filters/test_merge_filter_types.py index 448306caf..fb0a336cb 100644 --- a/tests/loaders/osm_loaders/filters/test_merge_filter_types.py +++ b/tests/loaders/osm_loaders/filters/test_merge_filter_types.py @@ -5,7 +5,7 @@ import pytest -from srai.loaders.osm_loaders.filters._typing import OsmTagsFilter, merge_grouped_osm_tags_filter +from srai.loaders.osm_loaders.filters import OsmTagsFilter, merge_grouped_osm_tags_filter ut = TestCase() diff --git a/tests/loaders/osm_loaders/test_osm_online_loader.py b/tests/loaders/osm_loaders/test_osm_online_loader.py index 4d4853ccc..56bed6f56 100644 --- a/tests/loaders/osm_loaders/test_osm_online_loader.py +++ b/tests/loaders/osm_loaders/test_osm_online_loader.py @@ -7,7 +7,7 @@ from srai.constants import WGS84_CRS from srai.loaders.osm_loaders import OSMOnlineLoader -from srai.loaders.osm_loaders.filters._typing import OsmTagsFilter +from srai.loaders.osm_loaders.filters import OsmTagsFilter if TYPE_CHECKING: from shapely.geometry import Polygon diff --git a/tests/miscellaneous/test_optional_dependencies.py b/tests/miscellaneous/test_optional_dependencies.py index 6c5b538b1..f56414d59 100644 --- a/tests/miscellaneous/test_optional_dependencies.py +++ b/tests/miscellaneous/test_optional_dependencies.py @@ -7,8 +7,8 @@ import pytest from shapely.geometry import box +from srai._optional import ImportErrorHandle, import_optional_dependency from srai.constants import GEOMETRY_COLUMN, REGIONS_INDEX, WGS84_CRS -from srai.utils._optional import ImportErrorHandle, import_optional_dependency @pytest.fixture # type: ignore diff --git a/tests/regionalizers/conftest.py b/tests/regionalizers/conftest.py index 47b2b22de..9320f1bf6 100644 --- a/tests/regionalizers/conftest.py +++ b/tests/regionalizers/conftest.py @@ -1,4 +1,4 @@ -"""Conftest for Regionalizers.""" +"""Fixtures for Regionalizers.""" from typing import List diff --git a/tests/regionalizers/test_administrative_boundary_regionalizer.py b/tests/regionalizers/test_administrative_boundary_regionalizer.py index 4592440c8..fdd264ad6 100644 --- a/tests/regionalizers/test_administrative_boundary_regionalizer.py +++ b/tests/regionalizers/test_administrative_boundary_regionalizer.py @@ -9,8 +9,8 @@ from shapely.geometry import Point, box from srai.constants import GEOMETRY_COLUMN, WGS84_CRS +from srai.geometry import merge_disjointed_gdf_geometries from srai.regionalizers import AdministrativeBoundaryRegionalizer -from srai.utils import merge_disjointed_gdf_geometries bbox = box(minx=-180, maxx=180, miny=-90, maxy=90) bbox_gdf = gpd.GeoDataFrame({GEOMETRY_COLUMN: [bbox]}, crs=WGS84_CRS) diff --git a/tests/regionalizers/test_voronoi_regionalizer.py b/tests/regionalizers/test_voronoi_regionalizer.py index 0d6b306ab..fcb6cd9e3 100644 --- a/tests/regionalizers/test_voronoi_regionalizer.py +++ b/tests/regionalizers/test_voronoi_regionalizer.py @@ -9,6 +9,7 @@ from shapely.geometry import Point, Polygon from srai.constants import GEOMETRY_COLUMN, WGS84_CRS +from srai.geometry import merge_disjointed_gdf_geometries from srai.regionalizers import VoronoiRegionalizer from srai.regionalizers._spherical_voronoi import ( _map_from_geocentric, @@ -16,7 +17,6 @@ _parse_num_of_multiprocessing_workers, generate_voronoi_regions, ) -from srai.utils import merge_disjointed_gdf_geometries def get_random_points(number_of_points: int) -> List[Point]: