Skip to content

Commit

Permalink
ENH: add Graph.build_h3 (#694)
Browse files Browse the repository at this point in the history
* ENH: add Graph.build_h3

* lint

* don't test with oldest
  • Loading branch information
martinfleis authored May 20, 2024
1 parent 798ed8a commit 3afbab7
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 0 deletions.
2 changes: 2 additions & 0 deletions ci/310.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies:
# testing
- codecov
- matplotlib
- tobler
- h3-py
- pytest
- pytest-cov
- pytest-mpl
Expand Down
2 changes: 2 additions & 0 deletions ci/311.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies:
# testing
- codecov
- matplotlib
- tobler
- h3-py
- pytest
- pytest-cov
- pytest-mpl
Expand Down
2 changes: 2 additions & 0 deletions ci/312-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ dependencies:
# testing
- codecov
- matplotlib
- tobler
- h3-py
- pytest
- pytest-cov
- pytest-mpl
Expand Down
2 changes: 2 additions & 0 deletions ci/312.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies:
# testing
- codecov
- matplotlib
- tobler
- h3-py
- pytest
- pytest-cov
- pytest-mpl
Expand Down
43 changes: 43 additions & 0 deletions libpysal/graph/_indices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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
44 changes: 44 additions & 0 deletions libpysal/graph/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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
Expand Down Expand Up @@ -1216,6 +1217,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
Expand Down
35 changes: 35 additions & 0 deletions libpysal/graph/tests/test_indices.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3afbab7

Please sign in to comment.