Source code for modelly.space.geo.regions
"""Region neighbourhoods of a ``GeoSpace``: contiguity (queen, rook) and distance."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
import shapely
if TYPE_CHECKING:
import geopandas as gpd
from scipy.sparse import csr_matrix
# Region row of a point that lies in no region.
NO_REGION = -1
CONTIGUITY_KINDS = ("queen", "rook")
[docs]
class RegionQueriesMixin:
"""Neighbourhoods between the polygons of ``regions``, built on one STRtree.
Relies on the host providing ``regions`` (a GeoDataFrame), and the cache slots
``_region_tree_cache`` and ``_contiguity_cache``.
"""
regions: gpd.GeoDataFrame
_region_tree_cache: shapely.STRtree | None
_contiguity_cache: dict[str, Any]
@property
def _region_tree(self) -> shapely.STRtree:
"""The STRtree over the region polygons, built on first use."""
if self._region_tree_cache is None:
self._region_tree_cache = shapely.STRtree(self.regions.geometry.values)
return self._region_tree_cache
[docs]
def contiguity(self, kind: str = "queen") -> csr_matrix:
"""The region adjacency as a symmetric CSR matrix, rows in ``regions`` order.
Args:
kind: ``"queen"``: regions sharing at least one point are neighbours;
``"rook"``: they must share a border of positive length.
"""
if kind not in CONTIGUITY_KINDS:
raise ValueError(f"kind must be one of {CONTIGUITY_KINDS}, not {kind!r}.")
if kind not in self._contiguity_cache:
self._contiguity_cache[kind] = self._build_contiguity(kind)
return self._contiguity_cache[kind]
def _build_contiguity(self, kind: str) -> csr_matrix:
from scipy.sparse import csr_matrix # noqa: PLC0415 - scipy is heavy
geoms = self.regions.geometry.values
n = len(geoms)
left, right = self._region_tree.query(geoms, predicate="intersects")
keep = left != right
left, right = left[keep], right[keep]
if kind == "rook" and left.size:
shared = shapely.intersection(
shapely.boundary(geoms[left]), shapely.boundary(geoms[right])
)
keep = shapely.length(shared) > 0
left, right = left[keep], right[keep]
data = np.ones(left.shape[0], dtype=np.int8)
return csr_matrix((data, (left, right)), shape=(n, n))
[docs]
def region_neighbors(
self, label: Any, kind: str = "queen", distance: float | None = None
) -> list:
"""Labels of the regions next to region ``label``.
Args:
label: the region label.
kind: ``"queen"`` or ``"rook"`` contiguity, used when ``distance`` is ``None``.
distance: if given, every other region within this distance of ``label``.
"""
row = self.regions.index.get_loc(label)
if distance is None:
adjacency = self.contiguity(kind)
rows = adjacency.indices[adjacency.indptr[row] : adjacency.indptr[row + 1]]
else:
polygon = self.regions.geometry.values[row]
rows = self._region_tree.query(
polygon, predicate="dwithin", distance=distance
)
rows = rows[rows != row]
return list(self.regions.index[np.sort(rows)])
[docs]
def regions_within(self, geometry: Any, distance: float | None = None) -> list:
"""Labels of the regions that intersect ``geometry``, or lie within ``distance`` of it."""
if distance is None:
rows = self._region_tree.query(geometry, predicate="intersects")
else:
rows = self._region_tree.query(
geometry, predicate="dwithin", distance=distance
)
return list(self.regions.index[np.sort(rows)])