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

Port GerryChain adjacency calcs to Shapely 2.0 #405

Merged
merged 8 commits into from Feb 8, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .circleci/config.yml
Expand Up @@ -34,7 +34,6 @@ jobs:
conda install pytest pytest-cov
conda install codecov
python -m pip install .[geo]
conda install shapely==1.6.4

# run tests
echo "backend: Agg" > "matplotlibrc"
Expand Down
14 changes: 7 additions & 7 deletions gerrychain/graph/adjacency.py
Expand Up @@ -16,8 +16,7 @@ def str_tree(geometries):
Use this for all spatial operations!
"""
from shapely.strtree import STRtree
for i in geometries.index:
geometries[i].id = i

try:
tree = STRtree(geometries)
except AttributeError:
Expand All @@ -26,17 +25,18 @@ def str_tree(geometries):


def neighboring_geometries(geometries, tree=None):
"""Generator yielding tuples of the form (id, (ids of neighbors)).
"""
"""Generator yielding tuples of the form (id, (ids of neighbors))."""
if tree is None:
tree = str_tree(geometries)

for geometry in geometries:
for geometry_id, geometry in geometries.items():
possible = tree.query(geometry)
actual = tuple(
p.id for p in possible if (not p.is_empty) and p.id != geometry.id
geometries.index[p]
for p in possible
if (not geometries.iloc[p].is_empty) and geometries.index[p] != geometry_id
)
yield (geometry.id, actual)
yield (geometry_id, actual)


def intersections_with_neighbors(geometries):
Expand Down
28 changes: 19 additions & 9 deletions gerrychain/graph/graph.py
Expand Up @@ -6,18 +6,27 @@
import networkx
from networkx.classes.function import frozen
from networkx.readwrite import json_graph
import pandas as pd

from .adjacency import neighbors
from .geo import GeometryError, invalid_geometries, reprojected


def json_serialize(input_object):
"""Serialize json so we can write to file
"""
if pd.api.types.is_integer_dtype(input_object): # handle int64
return int(input_object)


class Graph(networkx.Graph):
"""Represents a graph to be partitioned. It is based on :class:`networkx.Graph`.

We have added some classmethods to help construct graphs from shapefiles, and
to save and load graphs as JSON files.

"""

def __repr__(self):
return "<Graph [{} nodes, {} edges]>".format(len(self.nodes), len(self.edges))

Expand Down Expand Up @@ -57,7 +66,7 @@ def to_json(self, json_file, *, include_geometries_as_geojson=False):
remove_geometries(data)

with open(json_file, "w") as f:
json.dump(data, f)
json.dump(data, f, default=json_serialize)

@classmethod
def from_file(
Expand All @@ -76,12 +85,14 @@ def from_file(
add to the graph as node attributes. By default, all columns are added.
"""
import geopandas as gp

df = gp.read_file(filename)
graph = cls.from_geodataframe(
df, adjacency=adjacency,
df,
adjacency=adjacency,
cols_to_add=cols_to_add,
reproject=reproject,
ignore_errors=ignore_errors
ignore_errors=ignore_errors,
)
graph.graph["crs"] = df.crs.to_json()
return graph
Expand All @@ -93,7 +104,7 @@ def from_geodataframe(
adjacency="rook",
cols_to_add=None,
reproject=False,
ignore_errors=False
ignore_errors=False,
):
"""Creates the adjacency :class:`Graph` of geometries described by `dataframe`.
The areas of the polygons are included as node attributes (with key `area`).
Expand Down Expand Up @@ -275,6 +286,7 @@ def add_boundary_perimeters(graph, geometries):
"""
from shapely.ops import unary_union
from shapely.prepared import prep

prepared_boundary = prep(unary_union(geometries).boundary)

boundary_nodes = geometries.boundary.apply(prepared_boundary.intersects)
Expand Down Expand Up @@ -337,15 +349,13 @@ def convert_geometries_to_geojson(data):


class FrozenGraph:
""" Represents an immutable graph to be partitioned. It is based off :class:`Graph`.
"""Represents an immutable graph to be partitioned. It is based off :class:`Graph`.

This speeds up chain runs and prevents having to deal with cache invalidation issues.
This class behaves slightly differently than :class:`Graph` or :class:`networkx.Graph`.
"""
__slots__ = [
"graph",
"size"
]

__slots__ = ["graph", "size"]

def __init__(self, graph: Graph):
self.graph = networkx.classes.function.freeze(graph)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -36,6 +36,6 @@
"License :: OSI Approved :: BSD License",
],
extras_require={
'geo': ["shapely", "geopandas"]
'geo': ["shapely>=2.0.1", "geopandas>=0.12.2"]
}
)