From 247dc0cb776ad13e751feaf5fe003f9a79d0d62d Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 26 Mar 2024 16:21:04 +0100 Subject: [PATCH 1/3] ENH: add Graph.build_h3 --- ci/310-oldest.yaml | 2 ++ ci/310.yaml | 2 ++ ci/311.yaml | 2 ++ ci/312-dev.yaml | 2 ++ ci/312.yaml | 2 ++ libpysal/graph/_indices.py | 46 ++++++++++++++++++++++++++++ libpysal/graph/base.py | 44 ++++++++++++++++++++++++++ libpysal/graph/tests/test_indices.py | 35 +++++++++++++++++++++ 8 files changed, 135 insertions(+) create mode 100644 libpysal/graph/_indices.py create mode 100644 libpysal/graph/tests/test_indices.py diff --git a/ci/310-oldest.yaml b/ci/310-oldest.yaml index 9d0486fa5..284f6bafe 100644 --- a/ci/310-oldest.yaml +++ b/ci/310-oldest.yaml @@ -15,6 +15,8 @@ dependencies: # testing - codecov - matplotlib>=3.6 + - tobler + - h3-py - pytest - pytest-cov - pytest-mpl diff --git a/ci/310.yaml b/ci/310.yaml index 13ee976d6..c01bf902d 100644 --- a/ci/310.yaml +++ b/ci/310.yaml @@ -15,6 +15,8 @@ dependencies: # testing - codecov - matplotlib + - tobler + - h3-py - pytest - pytest-cov - pytest-mpl diff --git a/ci/311.yaml b/ci/311.yaml index 1dd04e983..2f114f0fc 100644 --- a/ci/311.yaml +++ b/ci/311.yaml @@ -15,6 +15,8 @@ dependencies: # testing - codecov - matplotlib + - tobler + - h3-py - pytest - pytest-cov - pytest-mpl diff --git a/ci/312-dev.yaml b/ci/312-dev.yaml index 33a4280bb..e075311cb 100644 --- a/ci/312-dev.yaml +++ b/ci/312-dev.yaml @@ -10,6 +10,8 @@ dependencies: # testing - codecov - matplotlib + - tobler + - h3-py - pytest - pytest-cov - pytest-mpl diff --git a/ci/312.yaml b/ci/312.yaml index e39764f08..27fa0daec 100644 --- a/ci/312.yaml +++ b/ci/312.yaml @@ -15,6 +15,8 @@ dependencies: # testing - codecov - matplotlib + - tobler + - h3-py - pytest - pytest-cov - pytest-mpl diff --git a/libpysal/graph/_indices.py b/libpysal/graph/_indices.py new file mode 100644 index 000000000..5a595f245 --- /dev/null +++ b/libpysal/graph/_indices.py @@ -0,0 +1,46 @@ +from libpysal import graph + + +def _build_from_h3(ids, order=1): + """Generate Graph from H3 hexagons. + + Encode a graph from a set of H3 hexagons. The graph is generated by + considering the H3 hexagons as nodes and connecting them based on their + contiguity. The contiguity is defined by the order parameter, which + specifies the number of steps to consider as neighbors. + + Requires the `h3` library. + + Parameters + ---------- + ids : array-like + Array of H3 IDs encoding focal geometries + order : int, optional + Order of contiguity, by default 1 + + Returns + ------- + tuple(dict, dict) + """ + try: + import h3 + except ImportError as e: + raise ImportError( + "This function requires the `h3` library. " + "You can install it with `conda install h3-py` or " + "`pip install h3`." + ) from e + + neighbors = {} + weights = {} + for ix in ids: + rings = h3.hex_range_distances(ix, order) + for i, ring in enumerate(rings): + if i == 0: + neighbors[ix] = [] + weights[ix] = [] + else: + neighbors[ix].extend(list(ring)) + weights[ix].extend([i] * len(ring)) + + return neighbors, weights diff --git a/libpysal/graph/base.py b/libpysal/graph/base.py index abbd0033d..03c5ffddd 100644 --- a/libpysal/graph/base.py +++ b/libpysal/graph/base.py @@ -14,6 +14,7 @@ _rook, _vertex_set_intersection, ) +from ._indices import _build_from_h3 from ._kernel import _distance_band, _kernel from ._matching import _spatial_matching from ._parquet import _read_parquet, _to_parquet @@ -867,6 +868,49 @@ def build_triangulation( return cls.from_arrays(head, tail, weights) + @classmethod + def build_h3(cls, ids, order=1, weight="distance"): + """Generate Graph from indices of H3 hexagons. + + Encode a graph from a set of H3 hexagons. The graph is generated by + considering the H3 hexagons as nodes and connecting them based on their + contiguity. The contiguity is defined by the order parameter, which + specifies the number of steps to consider as neighbors. The weight + parameter defines the type of weight to assign to the edges. + + Requires the `h3` library. + + Parameters + ---------- + ids : array-like + Array of H3 IDs encoding focal geometries + order : int, optional + Order of contiguity, by default 1 + weight : str, optional + Type of weight. Options are: + + * ``distance``: raw topological distance between cells + * ``binary``: 1 for neighbors, 0 for non-neighbors + * ``inverse``: 1 / distance between cells + + By default "distance". + + Returns + ------- + Graph + """ + neigbors, weights = _build_from_h3(ids, order=order) + g = cls.from_dicts(neigbors, weights) + + if weight == "distance": + return g + elif weight == "binary": + return g.transform("b") + elif weight == "inverse": + return cls(1 / g._adjacency, is_sorted=True) + else: + raise ValueError("weight must be one of 'distance', 'binary', or 'inverse'") + @cached_property def neighbors(self): """Get neighbors dictionary diff --git a/libpysal/graph/tests/test_indices.py b/libpysal/graph/tests/test_indices.py new file mode 100644 index 000000000..84a33caf6 --- /dev/null +++ b/libpysal/graph/tests/test_indices.py @@ -0,0 +1,35 @@ +import geopandas as gpd +import pytest +from geodatasets import get_path + +from libpysal import graph + +pytest.importorskip("h3") +pytest.importorskip("tobler") + + +class TestH3: + def setup_method(self): + from tobler.util import h3fy + + gdf = gpd.read_file(get_path("geoda guerry")) + h3_geoms = h3fy(gdf, resolution=4) + self.h3_ids = h3_geoms.index + + def test_h3(self): + g = graph.Graph.build_h3(self.h3_ids) + assert g.n == len(self.h3_ids) + assert g.pct_nonzero == 1.69921875 + assert len(g) == 1740 + assert g.adjacency.max() == 1 + + @pytest.mark.parametrize("order", range(2, 6)) + def test_h3_order(self, order): + g = graph.Graph.build_h3(self.h3_ids, order) + assert g.n == len(self.h3_ids) + assert g.adjacency.max() == order + + def test_h3_binary(self): + g = graph.Graph.build_h3(self.h3_ids, order=4, weight="binary") + assert g.n == len(self.h3_ids) + assert g.adjacency.max() == 1 From a7c713ac6546c7e20cfa40db0b19886372c93d89 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 26 Mar 2024 16:29:19 +0100 Subject: [PATCH 2/3] lint --- libpysal/graph/_indices.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/libpysal/graph/_indices.py b/libpysal/graph/_indices.py index 5a595f245..bbb7e3678 100644 --- a/libpysal/graph/_indices.py +++ b/libpysal/graph/_indices.py @@ -1,6 +1,3 @@ -from libpysal import graph - - def _build_from_h3(ids, order=1): """Generate Graph from H3 hexagons. From 88c3448f492eb63deb7e2db7b97611c7cb745dcc Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 26 Mar 2024 16:31:55 +0100 Subject: [PATCH 3/3] don't test with oldest --- ci/310-oldest.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ci/310-oldest.yaml b/ci/310-oldest.yaml index 284f6bafe..9d0486fa5 100644 --- a/ci/310-oldest.yaml +++ b/ci/310-oldest.yaml @@ -15,8 +15,6 @@ dependencies: # testing - codecov - matplotlib>=3.6 - - tobler - - h3-py - pytest - pytest-cov - pytest-mpl