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

Intercept Coincident points #548

Merged
merged 37 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a5c63c1
[WIP] start intercepting coincidence, probably should use a decorator…
ljwolf Aug 10, 2023
b22f2e5
finalize decorator implementation for _triangulation
ljwolf Aug 11, 2023
8aac540
finish jitter implementation
ljwolf Aug 11, 2023
d0b7c0d
propagate coincident to build_triangulation
ljwolf Aug 11, 2023
ca5ec9a
add reasons to assert clauses
ljwolf Aug 11, 2023
36a18e3
voronoi should still admit kernels
ljwolf Aug 11, 2023
2745af4
move to None for defaults, not boxcar/inf
ljwolf Aug 11, 2023
e1b243a
add distance_band
martinfleis Aug 7, 2023
cda1ef9
channel distance band through kernel
martinfleis Aug 7, 2023
ab207dd
fix isolates in distance band
martinfleis Aug 7, 2023
d4f6c73
lag, parquet IO, __getitem__, block placeholder
martinfleis Aug 10, 2023
3e20546
docstrings, optional pyarrow
martinfleis Aug 10, 2023
6ac2b7c
fix test
martinfleis Aug 10, 2023
bed12a8
block contiguity
martinfleis Aug 10, 2023
7671b77
add tests
martinfleis Aug 10, 2023
f6ecc00
to-dos
martinfleis Aug 10, 2023
6f283f8
Apply suggestions from code review
martinfleis Aug 10, 2023
c25db44
test block_contiguity
martinfleis Aug 10, 2023
3d27594
test parquet without meta
martinfleis Aug 10, 2023
ea180bd
test lag
martinfleis Aug 10, 2023
017ef80
haversine todo note
martinfleis Aug 11, 2023
d30f63b
comment on diag
martinfleis Aug 11, 2023
1754691
[WIP] work through integrations of kernel and triangulation in prep t…
ljwolf Aug 11, 2023
c318989
move back to upstream kernel
ljwolf Aug 11, 2023
fff6587
migrate checks for arrays, complete test battery
ljwolf Aug 11, 2023
a8be157
Merge remote-tracking branch 'upstream/geographs' into geographs
ljwolf Aug 11, 2023
c5cbe32
fix docstrings
ljwolf Aug 11, 2023
36d88b0
Update libpysal/graph/_triangulation.py
ljwolf Aug 11, 2023
2f18e35
document coincident option for graph
sjsrey Aug 25, 2023
d946fec
add coincidence checks to knn
ljwolf Aug 25, 2023
de99dd5
Merge pull request #2 from sjsrey/ljwgeographs
ljwolf Aug 25, 2023
6bdd4bc
Update libpysal/graph/_utils.py
ljwolf Aug 25, 2023
008b0e8
Update libpysal/graph/_utils.py
ljwolf Aug 25, 2023
ae59ad7
Update libpysal/graph/_utils.py
ljwolf Aug 25, 2023
cbf309b
finalize coincident checks in knn
ljwolf Aug 25, 2023
82f3e18
add the reorder table function to base, prepare for sorting inputs to…
ljwolf Aug 25, 2023
c4383e1
handle ids is none
ljwolf Aug 25, 2023
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
96 changes: 95 additions & 1 deletion libpysal/graph/_triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ def _delaunay(coordinates, ids=None, bandwidth=numpy.inf, kernel="boxcar"):
coordinates, ids=ids, valid_geometry_types=_VALID_GEOMETRY_TYPES
)

n_coincident, coincident_lut = _validate_coincident(geoms)
if n_coincident > 0:
if coincident == "raise":
raise ValueError(
f"There are {(coincident_lut[geoms.index.name].str.len()>1).sum()}"
f"unique locations in the dataset, but {len(geoms)} observations."
"This means there are multiple points in the same location, which"
" is undefined for this graph type. To address this issue, consider setting "
" `coincident='clique' or consult the documentation about coincident points."
)
elif coincident == "jitter":
coordinates, geoms = _jitter_geoms(coordinates, geoms)
elif coincident == "clique":
input_coordinates, input_ids, input_geoms = coordinates, ids, geoms
coordinates, ids, geoms = _validate_geometry_input(
coincident_lut.geometry, ids=coincident_lut.index, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
else:
raise ValueError(
f"Recieved option `coincident='{coincident}', but only options 'raise','clique','jitter' are suppported."
)

edges, _ = _voronoi_edges(coordinates)

ids = numpy.asarray(ids)
Expand All @@ -100,6 +122,9 @@ def _delaunay(coordinates, ids=None, bandwidth=numpy.inf, kernel="boxcar"):
# dropped points from the triangulation and the
# misalignment of the weights and the attribute array

if (n_coincident > 0) & (coincident == "clique"):
graph = _induce_cliques(graph, coincident_lut, fill_value=1)

return head, tail, weights


Expand Down Expand Up @@ -151,6 +176,27 @@ def _gabriel(coordinates, ids=None, bandwidth=numpy.inf, kernel="boxcar"):
coordinates, ids, _ = _validate_geometry_input(
coordinates, ids=ids, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
n_coincident, coincident_lut = _validate_coincident(geoms)
if n_coincident > 0:
if coincident == "raise":
raise ValueError(
f"There are {(coincident_lut[geoms.index.name].str.len()>1).sum()}"
f"unique locations in the dataset, but {len(geoms)} observations."
"This means there are multiple points in the same location, which"
" is undefined for this graph type. To address this issue, consider setting "
" `coincident='clique' or consult the documentation about coincident points."
)
elif coincident == "jitter":
coordinates, geoms = _jitter_geoms(coordinates, geoms)
elif coincident == "clique":
input_coordinates, input_ids, input_geoms = coordinates, ids, geoms
coordinates, ids, geoms = _validate_geometry_input(
coincident_lut.geometry, ids=coincident_lut.index, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
else:
raise ValueError(
f"Recieved option `coincident='{coincident}', but only options 'raise','clique','jitter' are suppported."
)

edges, dt = _voronoi_edges(coordinates)
droplist = _filter_gabriel(
Expand Down Expand Up @@ -222,6 +268,27 @@ def _relative_neighborhood(coordinates, ids=None, bandwidth=numpy.inf, kernel="b
coordinates, ids, _ = _validate_geometry_input(
coordinates, ids=ids, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
n_coincident, coincident_lut = _validate_coincident(geoms)
if n_coincident > 0:
if coincident == "raise":
raise ValueError(
f"There are {(coincident_lut[geoms.index.name].str.len()>1).sum()}"
f"unique locations in the dataset, but {len(geoms)} observations."
"This means there are multiple points in the same location, which"
" is undefined for this graph type. To address this issue, consider setting "
" `coincident='clique' or consult the documentation about coincident points."
)
elif coincident == "jitter":
coordinates, geoms = _jitter_geoms(coordinates, geoms)
elif coincident == "clique":
input_coordinates, input_ids, input_geoms = coordinates, ids, geoms
coordinates, ids, geoms = _validate_geometry_input(
coincident_lut.geometry, ids=coincident_lut.index, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
else:
raise ValueError(
f"Recieved option `coincident='{coincident}', but only options 'raise','clique','jitter' are suppported."
)

edges, dt = _voronoi_edges(coordinates)
output, _ = _filter_relativehood(edges, dt.points, return_dkmax=False)
Expand Down Expand Up @@ -293,9 +360,36 @@ def _voronoi(coordinates, ids=None, clip="extent", rook=True):
coordinates, ids, _ = _validate_geometry_input(
coordinates, ids=ids, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
n_coincident, coincident_lut = _validate_coincident(geoms)
if n_coincident > 0:
if coincident == "raise":
raise ValueError(
f"There are {(coincident_lut[geoms.index.name].str.len()>1).sum()}"
f"unique locations in the dataset, but {len(geoms)} observations."
"This means there are multiple points in the same location, which"
" is undefined for this graph type. To address this issue, consider setting "
" `coincident='clique' or consult the documentation about coincident points."
)
elif coincident == "jitter":
coordinates, geoms = _jitter_geoms(coordinates, geoms)
elif coincident == "clique":
input_coordinates, input_ids, input_geoms = coordinates, ids, geoms
coordinates, ids, geoms = _validate_geometry_input(
coincident_lut.geometry, ids=coincident_lut.index, valid_geometry_types=_VALID_GEOMETRY_TYPES
)
else:
raise ValueError(
f"Recieved option `coincident='{coincident}', but only options 'raise','clique','jitter' are suppported."
)

cells, _ = voronoi_frames(coordinates, clip=clip)
return _vertex_set_intersection(cells, rook=rook, ids=ids)
graph = _vertex_set_intersection(cells, rook=rook, ids=ids)

if (n_coincident > 0) & (coincident == "clique"):
fill_value = _kernel_functions[kernel](0, bandwidth)
new_adjtable = _induce_cliques(graph, coincident_lut, fill_value=fill_value)
# from here, how to ensure ordering?
return graph


#### utilities
Expand Down
59 changes: 59 additions & 0 deletions libpysal/graph/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
import pandas as pd
import shapely

def _jitter_geoms(*args,**kwargs):
raise NotImplementedError()

def _induce_cliques(graph, clique_to_members, fill_value=1):
"""
HERE BE DRAGONS! the "members" array must be in the same order as
the intended output graph.
"""
across_clique = graph.adjacency.merge(
clique_to_members['index'], left_index=True, right_index=True
).explode('index').rename(
columns=dict(index='subclique_focal')
).merge(
clique_to_members['index'], left_on='neighbor', right_index=True
).explode('index').rename(
columns=dict(index='subclique_neighbor')
).reset_index().drop(
['index','neighbor'], axis=1
).rename(
columns=dict(subclique_focal="focal", subclique_neighbor='neighbor')
)
is_clique = lut['index'].str.len()>1
within_clique = lut[
is_clique
]['index'].apply(
lambda x: list(combinations(x, 2))
).explode().apply(
pandas.Series
).rename(columns={0:"focal", 1:"neighbor"}).assign(weight=fill_value)

new_adj = pandas.concat((across_clique, within_clique), ignore_index=True, axis=0).reset_index(drop=True)

return new_adj



# try building on top of earlier blockweights code for cliques
# then use index tricks iterating over cliques




def _neighbor_dict_to_edges(neighbors, weights=None):
"""
Expand All @@ -22,6 +63,24 @@ def _neighbor_dict_to_edges(neighbors, weights=None):
data_array[heads == tails] = 0
return heads, tails, data_array

def _validate_coincident(geoms):
"""
Identify coincident points and create a look-up table for the coincident geometries.
"""
valid_coincident_geom_types = set(("Point",))
jGaboardi marked this conversation as resolved.
Show resolved Hide resolved
if not set(geoms.geom_type) <= valid_coincident_geom_types:
raise ValueError(
f"coindicence checks are only well-defined for geom_types: {valid_coincident_geom_types}"
)
wkb = geoms.to_frame('geometry').assign(hash = geoms.apply(lambda x: x.wkb)).reset_index(names='index')
ljwolf marked this conversation as resolved.
Show resolved Hide resolved
if len(wkb) > len(wkb.hash.unique()):
ljwolf marked this conversation as resolved.
Show resolved Hide resolved
grouper = wkb.groupby("hash")
ids = grouper['index'].agg(list)
max_coincident = ids.str.len().max()
unique_locs = grouper['geometry'].agg("first")
return max_coincident, geopandas.GeoDataFrame(ids.to_frame("index"), geometry=unique_locs)
else:
return 0, wkb

def _validate_geometry_input(geoms, ids=None, valid_geometry_types=None):
"""
Expand Down
Loading