diff --git a/ci/test_notebooks.sh b/ci/test_notebooks.sh index 8db134282..1f24c7180 100755 --- a/ci/test_notebooks.sh +++ b/ci/test_notebooks.sh @@ -48,7 +48,7 @@ for nb in $(find . -name "*.ipynb"); do echo "--------------------------------------------------------------------------------" else nvidia-smi - ${NBTEST} ${nbBasename} + ${NBTEST} ${nb} fi done diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 0a4824dd2..e551313ec 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -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, @@ -154,19 +155,44 @@ 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) + 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 @@ -174,11 +200,11 @@ def _preprocess(self, lhs, rhs): 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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 04fb5788c..dd82858f3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -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 ( @@ -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) @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 94e25c254..e579d2f15 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -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): @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index e145c6485..a8c869c79 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -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, ) @@ -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 + # 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 + crosses = ( + (points.sizes > 0) + & (lhs_crosses & rhs_crosses) + & (lines.sizes == 0) + ) + return crosses class LineStringPolygonCrosses(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index 2ada86abb..5c590671e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index 0bf109980..4085538e8 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py @@ -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, @@ -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( @@ -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): @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index 25c463b7c..14ac52d85 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -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 ( @@ -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) @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index d515d92fe..512f40510 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -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, @@ -17,6 +19,7 @@ Point, Polygon, _false_series, + _pli_lines_to_multipoints, ) from cuspatial.utils.column_utils import has_same_geometry @@ -39,6 +42,15 @@ class OverlapsPredicateBase(EqualsPredicateBase): pass +class LineStringLineStringOverlaps(BinPred): + def _preprocess(self, lhs, rhs): + 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) @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 3071fdf7a..f1493e495 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -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, @@ -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): @@ -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, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 3b6ea133d..05d8da63c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -3,12 +3,12 @@ from cuspatial.core.binpreds.basic_predicates import ( _basic_equals_all, _basic_equals_any, + _basic_equals_count, _basic_intersects, ) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, - NotImplementedPredicate, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -33,7 +33,7 @@ def _preprocess(self, lhs, rhs): class PointLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs.intersects(rhs) - equals = _basic_equals_any(lhs, rhs) + equals = _basic_equals_count(rhs, lhs) == lhs.sizes return intersects & ~equals @@ -42,6 +42,11 @@ def _preprocess(self, lhs, rhs): return rhs.contains_properly(lhs) +class MultiPointMultiPointWithin(BinPred): + def _preprocess(self, lhs, rhs): + return rhs.contains(lhs) + + class LineStringLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): contains = rhs.contains(lhs) @@ -60,11 +65,11 @@ def _preprocess(self, lhs, rhs): DispatchDict = { (Point, Point): WithinPredicateBase, - (Point, MultiPoint): WithinIntersectsPredicate, + (Point, MultiPoint): MultiPointMultiPointWithin, (Point, LineString): PointLineStringWithin, (Point, Polygon): PointPolygonWithin, - (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, Point): MultiPointMultiPointWithin, + (MultiPoint, MultiPoint): MultiPointMultiPointWithin, (MultiPoint, LineString): WithinIntersectsPredicate, (MultiPoint, Polygon): PolygonPolygonWithin, (LineString, Point): ImpossiblePredicate, diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 05b38d702..7469a0325 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -183,6 +183,39 @@ def predicate(request): LineString([(0.0, 0.0), (1.0, 1.0)]), LineString([(0.5, 0.5), (1.0, 0.1), (-1.0, 0.1)]), ), + "linestring-linestring-touch-corner": ( + """ + x x + | / + | //x + |// + x---x + """, + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(1, 1), (0, 0), (1, 0.5)]), + ), + "linestring-linestring-touch-twice-with-corner": ( + """ + x + | + x---x + | / + x---x + """, + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 0.5), (1, 0.5), (0, 0)]), + ), + "linestring-linestring-touch-top-edge": ( + """ + x---x + | + | + | + x---x + """, + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 1), (1, 1)]), + ), "linestring-polygon-disjoint": ( """ point_polygon above is drawn as @@ -578,6 +611,9 @@ def predicate(request): "linestring-linestring-touch-edge-twice", "linestring-linestring-crosses", "linestring-linestring-touch-and-cross", + "linestring-linestring-touch-corner", + "linestring-linestring-touch-twice-with-corner", + "linestring-linestring-touch-top-edge", ] linestring_polygon_dispatch_list = [ diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_cartesian_dispatch_list.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_cartesian_dispatch_list.py index 772853ef2..8aa3851ca 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_cartesian_dispatch_list.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_cartesian_dispatch_list.py @@ -16,7 +16,7 @@ import cuspatial -def sample_test_data(features, dispatch_list, size, lib=cuspatial): +def sample_test_data(features, dispatch_list, lib=cuspatial): """Creates either a cuspatial or geopandas GeoSeries object using the Feature objects in `features`, the list of features to sample from in `dispatch_list`, and the size of the resultant GeoSeries. @@ -37,9 +37,8 @@ def sample_test_data(features, dispatch_list, size, lib=cuspatial): def run_test(pred, dispatch_list): - size = 10000 - lhs, rhs = sample_test_data(features, dispatch_list, size, cuspatial) - gpdlhs, gpdrhs = sample_test_data(features, dispatch_list, size, geopandas) + lhs, rhs = sample_test_data(features, dispatch_list, cuspatial) + gpdlhs, gpdrhs = sample_test_data(features, dispatch_list, geopandas) # Reverse pred_fn = getattr(rhs, pred) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py new file mode 100644 index 000000000..74bb17c9c --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +import cupy as cp +import numpy as np +from binpred_test_dispatch import ( # noqa: F401 + features, + linestring_linestring_dispatch_list, + linestring_polygon_dispatch_list, + point_linestring_dispatch_list, + point_point_dispatch_list, + point_polygon_dispatch_list, + polygon_polygon_dispatch_list, + predicate, +) + +import cudf + +import cuspatial +from cuspatial.utils.column_utils import contains_only_linestrings + + +def sample_single_geometries(geometries): + lhs = cuspatial.GeoSeries(list(geometries)) + singles = cudf.Series(np.arange(len(lhs))) + multis = cudf.Series(np.repeat(np.arange(len(lhs)), len(lhs))) + lhs_picks = cudf.concat([singles, multis]).reset_index(drop=True) + return lhs[lhs_picks].reset_index(drop=True) + + +def sample_multi_geometries(geometries): + rhs = cuspatial.GeoSeries(list(geometries)) + tiles = np.tile(np.arange(len(rhs)), len(rhs)) + repeats = np.repeat(np.arange(len(rhs)), len(rhs)) + singles = cudf.Series(np.arange(len(rhs))) + multis = cudf.DataFrame( + {"tile": tiles, "repeat": repeats} + ).interleave_columns() + rhs_picks = cudf.concat([singles, multis]).reset_index(drop=True) + return rhs[rhs_picks].reset_index(drop=True) + + +def sample_cartesian_left(features, dispatch_list): + """Returns each left geometry in `features` as a cuspatial GeoSeries object + repeated the number of times in `dispatch_list`. + """ + geometries = [features[key][1] for key in dispatch_list] + return sample_single_geometries(geometries) + + +def sample_cartesian_right(features, dispatch_list): + """Returns each right geometry in `features` as a cuspatial GeoSeries + object tiled the number of times in `dispatch_list`. + """ + geometries = [features[key][2] for key in dispatch_list] + return sample_multi_geometries(geometries) + + +def sample_test_data(features, dispatch_list): + """Creates either a cuSpatial or geopandas GeoSeries object using the + features in `features` and the list of features to sample from + `dispatch_list`. + """ + geometry_tuples = [features[key][1:3] for key in dispatch_list] + geometries = [ + [lhs_geo for lhs_geo, _ in geometry_tuples], + [rhs_geo for _, rhs_geo in geometry_tuples], + ] + lhs = cuspatial.GeoSeries(list(geometries[0])) + rhs = cuspatial.GeoSeries(list(geometries[1])) + lhs_picks = np.repeat(np.arange(len(lhs)), len(lhs)) + rhs_picks = np.tile(np.arange(len(rhs)), len(rhs)) + return ( + lhs[lhs_picks].reset_index(drop=True), + rhs[rhs_picks].reset_index(drop=True), + ) + + +def run_test(pred, lhs, rhs, gpdlhs, gpdrhs): + # Reverse + pred_fn = getattr(rhs, pred) + got = pred_fn(lhs) + gpd_pred_fn = getattr(gpdrhs, pred) + expected = gpd_pred_fn(gpdlhs) + + # Special case for error in GEOS MultiLineString contains + # https://github.com/libgeos/geos/issues/933 + if ( + pred != "intersects" + and contains_only_linestrings(lhs) + and contains_only_linestrings(rhs) + and (lhs.sizes > 2).any() + and (rhs.sizes > 2).any() + ): + # 112 and 117 are wrong + got[[112, 117]] = False + assert (got.values_host == expected.values).all() + + # Forward + pred_fn = getattr(lhs, pred) + got = pred_fn(rhs) + gpd_pred_fn = getattr(gpdlhs, pred) + expected = gpd_pred_fn(gpdrhs) + assert (got.values_host == expected.values).all() + + +def test_point_multipoint(predicate): # noqa: F811 + geometries = [features[key][2] for key in point_point_dispatch_list] + points_lhs = sample_single_geometries(geometries) + points_rhs = sample_cartesian_right(features, point_point_dispatch_list) + size = len(point_point_dispatch_list) + multipoint_offsets = np.concatenate( + [np.arange(size), np.arange((size * size) + 1) * size + size] + ) + multipoints_rhs = cuspatial.GeoSeries.from_multipoints_xy( + points_rhs.points.xy, multipoint_offsets + ) + gpdlhs = points_lhs.to_geopandas() + gpdrhs = multipoints_rhs.to_geopandas() + + run_test(predicate, points_lhs, multipoints_rhs, gpdlhs, gpdrhs) + + +def test_multipoint_multipoint(predicate): # noqa: F811 + points_lhs, points_rhs = sample_test_data( + features, point_point_dispatch_list + ) + lhs = cuspatial.GeoSeries.from_multipoints_xy( + points_lhs.points.xy, + np.arange(len(points_lhs)), + ) + rhs = cuspatial.GeoSeries.from_multipoints_xy( + points_rhs.points.xy, np.arange(len(points_rhs)) + ) + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + + run_test(predicate, lhs, rhs, gpdlhs, gpdrhs) + + +def test_point_multilinestring(predicate): # noqa: F811 + points_lhs = sample_cartesian_left( + features, point_linestring_dispatch_list + ) + lines_rhs = sample_cartesian_right( + features, point_linestring_dispatch_list + ) + size = len(point_linestring_dispatch_list) + part_offset = np.concatenate([np.arange(len(lines_rhs) + 1) * 2]) + geometry_offset = np.concatenate( + [np.arange(size), np.arange((size * size) + 1) * 2 + size] + ) + multilinestrings_rhs = cuspatial.GeoSeries.from_linestrings_xy( + lines_rhs.lines.xy, part_offset, geometry_offset + ) + gpdlhs = points_lhs.to_geopandas() + gpdrhs = multilinestrings_rhs.to_geopandas() + + run_test(predicate, points_lhs, multilinestrings_rhs, gpdlhs, gpdrhs) + + +def test_linestring_multilinestring(predicate): # noqa: F811 + lines_lhs = sample_cartesian_left( + features, linestring_linestring_dispatch_list + ) + lines_rhs = sample_cartesian_right( + features, linestring_linestring_dispatch_list + ) + size = len(linestring_linestring_dispatch_list) + top_sizes = cp.concatenate( + [cp.array([0]), lines_rhs[:size].sizes.cumsum()] + ) + l_sizes = lines_rhs[size:][::2].sizes + r_sizes = lines_rhs[size:][1::2].sizes + tail_sizes = ( + cudf.DataFrame( + { + "x": l_sizes._column, + "y": r_sizes._column, + } + ) + .interleave_columns() + .cumsum() + + top_sizes.max() + ) + part_offset = cp.concatenate([top_sizes, tail_sizes]) + geometry_offset = np.concatenate( + [np.arange(size), np.arange((size * size) + 1) * 2 + size] + ) + multilinestrings_rhs = cuspatial.GeoSeries.from_linestrings_xy( + lines_rhs.lines.xy, part_offset, geometry_offset + ) + gpdlhs = lines_lhs.to_geopandas() + gpdrhs = multilinestrings_rhs.to_geopandas() + + run_test(predicate, lines_lhs, multilinestrings_rhs, gpdlhs, gpdrhs) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 5a1a0fb0b..7945e7837 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -421,9 +421,13 @@ def _pli_features_rebuild_offsets(pli, features): See the docs for `_pli_points_to_multipoints` and `_pli_lines_to_multipoints` for the rest of the explanation. """ + if len(features) == 0: + return _zero_series(len(pli[0])) + in_sizes = ( features.sizes if len(features) > 0 else _zero_series(len(pli[0]) - 1) ) + offsets = cudf.Series(pli[0]) offset_sizes = offsets[1:].reset_index(drop=True) - offsets[ :-1 @@ -504,3 +508,15 @@ def _pli_lines_to_multipoints(pli): ) multipoints = cuspatial.GeoSeries.from_multipoints_xy(xy, offsets) return multipoints + + +def _lines_to_boundary_multipoints(lines): + starts = lines.lines.part_offset[:-1] + ends = lines.lines.part_offset[1:] - 1 + indices = cudf.DataFrame({"x": starts, "y": ends}).interleave_columns() + all_points = cuspatial.GeoSeries.from_points_xy(lines.lines.xy) + boundary_points = all_points.take(indices) + multipoints = cuspatial.GeoSeries.from_multipoints_xy( + boundary_points.points.xy, lines.lines.geometry_offset * 2 + ) + return multipoints