Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add Graph.build_h3 #694

Merged
merged 3 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
Expand Down Expand Up @@ -867,6 +868,49 @@

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)

Check warning on line 910 in libpysal/graph/base.py

View check run for this annotation

Codecov / codecov/patch

libpysal/graph/base.py#L909-L910

Added lines #L909 - L910 were not covered by tests
else:
raise ValueError("weight must be one of 'distance', 'binary', or 'inverse'")

Check warning on line 912 in libpysal/graph/base.py

View check run for this annotation

Codecov / codecov/patch

libpysal/graph/base.py#L912

Added line #L912 was not covered by tests

@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