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

MultiGeometry binary predicate support. #1220

Draft
wants to merge 16 commits into
base: branch-23.08
Choose a base branch
from
Draft
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: 1 addition & 1 deletion ci/test_notebooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ for nb in $(find . -name "*.ipynb"); do
echo "--------------------------------------------------------------------------------"
else
nvidia-smi
${NBTEST} ${nbBasename}
${NBTEST} ${nb}
fi
done

Expand Down
40 changes: 33 additions & 7 deletions python/cuspatial/cuspatial/core/binpreds/feature_contains.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from cuspatial.core.binpreds.basic_predicates import (
_basic_contains_count,
_basic_equals_all,
_basic_equals_any,
_basic_equals_count,
_basic_intersects,
Expand Down Expand Up @@ -154,31 +155,56 @@ def _preprocess(self, lhs, rhs):
return _basic_equals_any(lhs, rhs)


class MultiPointMultiPointContains(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_all(rhs, lhs)


class LineStringPointContains(BinPred):
def _preprocess(self, lhs, rhs):
intersects = _basic_intersects(lhs, rhs)
equals = _basic_equals_any(lhs, rhs)
equals = _basic_equals_count(lhs, rhs) == rhs.sizes
return intersects & ~equals


class LineStringLineStringContainsPredicate(BinPred):
def _preprocess(self, lhs, rhs):
# rhs_self_intersection is rhs where all colinear segments
# are compacted into a single segment. This is necessary
# because the intersection of lhs and rhs below will
# compact colinear segments into a single segment, so
# vertices in rhs will be lost and not countable.
# Consider the following example:
# lhs = [(0, 0), (1, 1)]
# rhs = [(0, 0), (0.5, 0.5), (1, 1)]
# The intersection of lhs and rhs is [(0, 0), (1, 1)].
# rhs is contained by lhs if all of the vertices in the
# intersection are in rhs. However, the intersection
# does not contain the vertex (0.5, 0.5) in the original rhs.
rhs_self_intersection = _basic_intersects_pli(rhs, rhs)
Copy link
Contributor

@isVoid isVoid Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like you need a separate API that handles linestring merging. Something like:

void merge_points_on_multilinestring_range(MultiLinestringRange range, OutputIt output);

Because calling the self intersection just to remove the additional points is too expensive.

rhs_no_segments = _points_and_lines_to_multipoints(
rhs_self_intersection[1], rhs_self_intersection[0]
)
# Now intersect lhs and rhs and collect only the segments.
pli = _basic_intersects_pli(lhs, rhs)
points = _points_and_lines_to_multipoints(pli[1], pli[0])
# Every point in B must be in the intersection
equals = _basic_equals_count(rhs, points) == rhs.sizes
lines = _pli_lines_to_multipoints(pli)
# Every segment in B must be in the intersection segments.
equals = (
_basic_equals_count(lines, rhs_no_segments)
== rhs_no_segments.sizes
)
return equals


"""DispatchDict listing the classes to use for each combination of
left and right hand side types. """
DispatchDict = {
(Point, Point): PointPointContains,
(Point, MultiPoint): ImpossiblePredicate,
(Point, MultiPoint): MultiPointMultiPointContains,
(Point, LineString): ImpossiblePredicate,
(Point, Polygon): ImpossiblePredicate,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, MultiPoint): NotImplementedPredicate,
(MultiPoint, Point): MultiPointMultiPointContains,
(MultiPoint, MultiPoint): MultiPointMultiPointContains,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): NotImplementedPredicate,
(LineString, Point): LineStringPointContains,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from cuspatial.core.binpreds.basic_predicates import (
_basic_equals_all,
_basic_equals_count,
_basic_intersects,
)
from cuspatial.core.binpreds.binpred_interface import (
Expand Down Expand Up @@ -185,6 +186,11 @@ def _preprocess(self, lhs, rhs):
return _basic_intersects(lhs, rhs)


class MultiPointMultiPointContainsProperly(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_count(rhs, lhs) == rhs.sizes


class LineStringLineStringContainsProperly(BinPred):
def _preprocess(self, lhs, rhs):
count = _basic_equals_all(lhs, rhs)
Expand All @@ -195,11 +201,11 @@ def _preprocess(self, lhs, rhs):
left and right hand side types. """
DispatchDict = {
(Point, Point): ContainsProperlyByIntersection,
(Point, MultiPoint): ContainsProperlyByIntersection,
(Point, MultiPoint): MultiPointMultiPointContainsProperly,
(Point, LineString): ImpossiblePredicate,
(Point, Polygon): ImpossiblePredicate,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, MultiPoint): NotImplementedPredicate,
(MultiPoint, Point): MultiPointMultiPointContainsProperly,
(MultiPoint, MultiPoint): MultiPointMultiPointContainsProperly,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): NotImplementedPredicate,
(LineString, Point): ContainsProperlyByIntersection,
Expand Down
19 changes: 11 additions & 8 deletions python/cuspatial/cuspatial/core/binpreds/feature_covers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,18 @@ class CoversPredicateBase(EqualsPredicateBase):
pass


class MultiPointMultiPointCovers(BinPred):
def _preprocess(self, lhs, rhs):
# A multipoint A covers another multipoint B iff
# every point in B is in A.
return lhs.contains(rhs)


class LineStringLineStringCovers(BinPred):
def _preprocess(self, lhs, rhs):
# A linestring A covers another linestring B iff
# no point in B is outside of A.
pli = _basic_intersects_pli(lhs, rhs)
points = _points_and_lines_to_multipoints(pli[1], pli[0])
# Every point in B must be in the intersection
equals = _basic_equals_count(rhs, points) == rhs.sizes
return equals
return lhs.contains(rhs)


class PolygonPointCovers(BinPred):
Expand Down Expand Up @@ -83,11 +86,11 @@ def _preprocess(self, lhs, rhs):

DispatchDict = {
(Point, Point): CoversPredicateBase,
(Point, MultiPoint): NotImplementedPredicate,
(Point, MultiPoint): MultiPointMultiPointCovers,
(Point, LineString): ImpossiblePredicate,
(Point, Polygon): ImpossiblePredicate,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, MultiPoint): NotImplementedPredicate,
(MultiPoint, Point): MultiPointMultiPointCovers,
(MultiPoint, MultiPoint): MultiPointMultiPointCovers,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): NotImplementedPredicate,
(LineString, Point): LineStringPointIntersects,
Expand Down
28 changes: 20 additions & 8 deletions python/cuspatial/cuspatial/core/binpreds/feature_crosses.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
Point,
Polygon,
_false_series,
_points_and_lines_to_multipoints,
_lines_to_boundary_multipoints,
_pli_lines_to_multipoints,
_pli_points_to_multipoints,
)


Expand All @@ -43,13 +45,23 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result):
# they intersect, and none of the points of the
# intersection are in the boundary of the other
pli = _basic_intersects_pli(rhs, lhs)
intersections = _points_and_lines_to_multipoints(pli[1], pli[0])
equals_lhs_count = _basic_equals_count(intersections, lhs)
equals_rhs_count = _basic_equals_count(intersections, rhs)
equals_lhs = equals_lhs_count != intersections.sizes
equals_rhs = equals_rhs_count != intersections.sizes
equals = equals_lhs & equals_rhs
return equals
points = _pli_points_to_multipoints(pli)
lines = _pli_lines_to_multipoints(pli)
# Optimization: only compute the subsequent boundaries and equalities
# of indexes that contain point intersections and do not contain line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# of indexes that contain point intersections and do not contain line
# of indices that contain point intersections and do not contain line

# intersections.
lhs_boundary = _lines_to_boundary_multipoints(lhs)
rhs_boundary = _lines_to_boundary_multipoints(rhs)
lhs_boundary_matches = _basic_equals_count(points, lhs_boundary)
rhs_boundary_matches = _basic_equals_count(points, rhs_boundary)
lhs_crosses = lhs_boundary_matches != points.sizes
rhs_crosses = rhs_boundary_matches != points.sizes
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It says "none of the points of intersection is in the boundary of the geometry". AKA, there is 0 points in the intersection results that are in the boundary. Shouldn't this be

lhs_boundary_matches == 0

?

crosses = (
(points.sizes > 0)
& (lhs_crosses & rhs_crosses)
& (lines.sizes == 0)
)
return crosses


class LineStringPolygonCrosses(BinPred):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ def _preprocess(self, lhs, rhs):

DispatchDict = {
(Point, Point): PointPointDisjoint,
(Point, MultiPoint): NotImplementedPredicate,
(Point, MultiPoint): PointPointDisjoint,
(Point, LineString): PointLineStringDisjoint,
(Point, Polygon): PointPolygonDisjoint,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, MultiPoint): NotImplementedPredicate,
(MultiPoint, Point): PointPointDisjoint,
(MultiPoint, MultiPoint): PointPointDisjoint,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): LineStringPolygonDisjoint,
(LineString, Point): LineStringPointDisjoint,
Expand Down
41 changes: 15 additions & 26 deletions python/cuspatial/cuspatial/core/binpreds/feature_equals.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from cudf import Series

import cuspatial
from cuspatial.core.binpreds.basic_predicates import _basic_equals_all
from cuspatial.core.binpreds.binpred_interface import (
BinPred,
EqualsOpResult,
Expand Down Expand Up @@ -289,6 +290,16 @@ def _postprocess(self, lhs, rhs, op_result):
return result


class PointMultiPointEquals(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_all(rhs, lhs)


class MultiPointPointEquals(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_all(lhs, rhs)


class MultiPointMultiPointEquals(PolygonComplexEquals):
def _compute_predicate(self, lhs, rhs, point_indices):
lengths_equal = self._offset_equals(
Expand All @@ -306,30 +317,8 @@ def _compute_predicate(self, lhs, rhs, point_indices):


class LineStringLineStringEquals(PolygonComplexEquals):
def _compute_predicate(self, lhs, rhs, preprocessor_result):
"""Linestrings can be compared either forward or reversed. We need
to compare both directions."""
lengths_equal = self._offset_equals(
lhs.lines.part_offset, rhs.lines.part_offset
)
lhs_lengths_equal = lhs[lengths_equal]
rhs_lengths_equal = rhs[lengths_equal]
lhs_reversed = self._reverse_linestrings(
lhs_lengths_equal.lines.xy, lhs_lengths_equal.lines.part_offset
)
forward_result = self._vertices_equals(
lhs_lengths_equal.lines.xy, rhs_lengths_equal.lines.xy
)
reverse_result = self._vertices_equals(
lhs_reversed, rhs_lengths_equal.lines.xy
)
result = forward_result | reverse_result
original_point_indices = cudf.Series(
lhs_lengths_equal.point_indices
).replace(cudf.Series(lhs_lengths_equal.index))
return self._postprocess(
lhs, rhs, EqualsOpResult(result, original_point_indices)
)
def _preprocess(self, lhs, rhs):
return lhs.contains(rhs) & rhs.contains(lhs)


class LineStringPointEquals(EqualsPredicateBase):
Expand All @@ -349,10 +338,10 @@ def _preprocess(self, lhs, rhs):
"""DispatchDict for Equals operations."""
DispatchDict = {
(Point, Point): EqualsPredicateBase,
(Point, MultiPoint): NotImplementedPredicate,
(Point, MultiPoint): PointMultiPointEquals,
(Point, LineString): ImpossiblePredicate,
(Point, Polygon): EqualsPredicateBase,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, Point): MultiPointPointEquals,
(MultiPoint, MultiPoint): MultiPointMultiPointEquals,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): NotImplementedPredicate,
Expand Down
12 changes: 9 additions & 3 deletions python/cuspatial/cuspatial/core/binpreds/feature_intersects.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from cuspatial.core.binops.intersection import pairwise_linestring_intersection
from cuspatial.core.binpreds.basic_predicates import (
_basic_contains_any,
_basic_equals_count,
_basic_intersects,
)
from cuspatial.core.binpreds.binpred_interface import (
Expand Down Expand Up @@ -111,6 +112,11 @@ def _preprocess(self, lhs, rhs):
return super()._preprocess(rhs, lhs)


class MultiPointMultiPointIntersects(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_count(lhs, rhs) > 0


class LineStringPolygonIntersects(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_contains_any(rhs, lhs)
Expand All @@ -132,11 +138,11 @@ def _preprocess(self, lhs, rhs):
""" Type dispatch dictionary for intersects binary predicates. """
DispatchDict = {
(Point, Point): IntersectsByEquals,
(Point, MultiPoint): NotImplementedPredicate,
(Point, MultiPoint): MultiPointMultiPointIntersects,
(Point, LineString): PointLineStringIntersects,
(Point, Polygon): PointPolygonIntersects,
(MultiPoint, Point): NotImplementedPredicate,
(MultiPoint, MultiPoint): NotImplementedPredicate,
(MultiPoint, Point): MultiPointMultiPointIntersects,
(MultiPoint, MultiPoint): MultiPointMultiPointIntersects,
(MultiPoint, LineString): NotImplementedPredicate,
(MultiPoint, Polygon): NotImplementedPredicate,
(LineString, Point): LineStringPointIntersects,
Expand Down
14 changes: 13 additions & 1 deletion python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from cuspatial.core.binpreds.basic_predicates import (
_basic_contains_properly_any,
_basic_equals_count,
_basic_intersects_pli,
)
from cuspatial.core.binpreds.binpred_interface import (
BinPred,
Expand All @@ -17,6 +19,7 @@
Point,
Polygon,
_false_series,
_pli_lines_to_multipoints,
)
from cuspatial.utils.column_utils import has_same_geometry

Expand All @@ -39,6 +42,15 @@ class OverlapsPredicateBase(EqualsPredicateBase):
pass


class LineStringLineStringOverlaps(BinPred):
def _preprocess(self, lhs, rhs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small docstring helps:

Suggested change
def _preprocess(self, lhs, rhs):
def _preprocess(self, lhs, rhs):
"""Linestring overlaps with another linestring if the overlapping part of
two linestring is not the entire section of the linestring.
"""

Sounds correct?

pli = _basic_intersects_pli(lhs, rhs)
lines = _pli_lines_to_multipoints(pli)
lhs_not_equal = _basic_equals_count(lhs, lines) != lhs.sizes
rhs_not_equal = _basic_equals_count(rhs, lines) != rhs.sizes
return (lines.sizes > 0) & lhs_not_equal & rhs_not_equal


class PolygonPolygonOverlaps(BinPred):
def _preprocess(self, lhs, rhs):
contains_lhs = lhs.contains(rhs)
Expand Down Expand Up @@ -85,7 +97,7 @@ def _postprocess(self, lhs, rhs, op_result):
(MultiPoint, Polygon): ImpossiblePredicate,
(LineString, Point): ImpossiblePredicate,
(LineString, MultiPoint): ImpossiblePredicate,
(LineString, LineString): ImpossiblePredicate,
(LineString, LineString): LineStringLineStringOverlaps,
(LineString, Polygon): ImpossiblePredicate,
(Polygon, Point): OverlapsPredicateBase,
(Polygon, MultiPoint): OverlapsPredicateBase,
Expand Down
16 changes: 10 additions & 6 deletions python/cuspatial/cuspatial/core/binpreds/feature_touches.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
_basic_contains_count,
_basic_contains_properly_any,
_basic_equals_all,
_basic_equals_any,
_basic_equals_count,
_basic_intersects,
_basic_intersects_count,
Expand Down Expand Up @@ -47,7 +46,12 @@ class TouchesPredicateBase(ContainsPredicate):
"""

def _preprocess(self, lhs, rhs):
return _basic_equals_any(lhs, rhs)
return _basic_equals_count(lhs, rhs) == rhs.sizes


class PointLineStringTouches(BinPred):
def _preprocess(self, lhs, rhs):
return _basic_equals_count(rhs, lhs) == lhs.sizes


class PointPolygonTouches(ContainsPredicate):
Expand Down Expand Up @@ -147,11 +151,11 @@ def _preprocess(self, lhs, rhs):

DispatchDict = {
(Point, Point): ImpossiblePredicate,
(Point, MultiPoint): TouchesPredicateBase,
(Point, LineString): TouchesPredicateBase,
(Point, MultiPoint): ImpossiblePredicate,
(Point, LineString): PointLineStringTouches,
(Point, Polygon): PointPolygonTouches,
(MultiPoint, Point): TouchesPredicateBase,
(MultiPoint, MultiPoint): TouchesPredicateBase,
(MultiPoint, Point): ImpossiblePredicate,
(MultiPoint, MultiPoint): ImpossiblePredicate,
(MultiPoint, LineString): TouchesPredicateBase,
(MultiPoint, Polygon): TouchesPredicateBase,
(LineString, Point): TouchesPredicateBase,
Expand Down
Loading