From db747ac7ec994f7cf00769e98914c13de95e4484 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 27 Jun 2023 14:31:11 -0500 Subject: [PATCH 01/13] Write first set of MultiPoint MultiPoint tests. --- .../cuspatial/core/binpreds/feature_contains.py | 8 +++++++- .../core/binpreds/feature_contains_properly.py | 9 ++++++++- .../cuspatial/core/binpreds/feature_covers.py | 10 +++++++++- .../cuspatial/core/binpreds/feature_disjoint.py | 2 +- .../cuspatial/core/binpreds/feature_intersects.py | 8 +++++++- .../cuspatial/core/binpreds/feature_touches.py | 2 +- .../cuspatial/core/binpreds/feature_within.py | 7 ++++++- 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 0617c12b3..6793a6fa3 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,6 +155,11 @@ 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) @@ -178,7 +184,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): ImpossiblePredicate, (Point, Polygon): ImpossiblePredicate, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (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..7e143a2fa 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,12 @@ def _preprocess(self, lhs, rhs): return _basic_intersects(lhs, rhs) +class MultiPointMultiPointContainsProperly(BinPred): + def _preprocess(self, lhs, rhs): + breakpoint() + return _basic_equals_count(rhs, lhs) == rhs.sizes + + class LineStringLineStringContainsProperly(BinPred): def _preprocess(self, lhs, rhs): count = _basic_equals_all(lhs, rhs) @@ -199,7 +206,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): ImpossiblePredicate, (Point, Polygon): ImpossiblePredicate, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (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..fd84a864e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -37,6 +37,14 @@ 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. + # Count the number of points from rhs in lhs + return lhs.contains(rhs) + + class LineStringLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): # A linestring A covers another linestring B iff @@ -87,7 +95,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): ImpossiblePredicate, (Point, Polygon): ImpossiblePredicate, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, MultiPoint): MultiPointMultiPointCovers, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointIntersects, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index 2ada86abb..bfcbad9ea 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -78,7 +78,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): PointLineStringDisjoint, (Point, Polygon): PointPolygonDisjoint, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, MultiPoint): PointPointDisjoint, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): LineStringPolygonDisjoint, (LineString, Point): LineStringPointDisjoint, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index 25c463b7c..466bb0286 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) @@ -136,7 +142,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): PointLineStringIntersects, (Point, Polygon): PointPolygonIntersects, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, MultiPoint): MultiPointMultiPointIntersects, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointIntersects, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index d76dc6200..2caf75849 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -149,7 +149,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): TouchesPredicateBase, (Point, Polygon): PointPolygonTouches, (MultiPoint, Point): TouchesPredicateBase, - (MultiPoint, MultiPoint): TouchesPredicateBase, + (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..d18b8b40b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -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) @@ -64,7 +69,7 @@ def _preprocess(self, lhs, rhs): (Point, LineString): PointLineStringWithin, (Point, Polygon): PointPolygonWithin, (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, MultiPoint): MultiPointMultiPointWithin, (MultiPoint, LineString): WithinIntersectsPredicate, (MultiPoint, Polygon): PolygonPolygonWithin, (LineString, Point): ImpossiblePredicate, From 375e3523e338cd1cfc3d8555f663f0206b987361 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 27 Jun 2023 21:13:43 +0000 Subject: [PATCH 02/13] Add all MultiPoint tests. --- .../core/binpreds/feature_contains.py | 4 +- .../binpreds/feature_contains_properly.py | 5 +- .../cuspatial/core/binpreds/feature_covers.py | 4 +- .../core/binpreds/feature_disjoint.py | 4 +- .../cuspatial/core/binpreds/feature_equals.py | 15 ++- .../core/binpreds/feature_intersects.py | 4 +- .../core/binpreds/feature_touches.py | 4 +- .../cuspatial/core/binpreds/feature_within.py | 5 +- .../test_binpred_cartesian_dispatch_list.py | 7 +- ...est_binpred_multigeometry_test_dispatch.py | 114 ++++++++++++++++++ 10 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 6793a6fa3..ee563f4ea 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -180,10 +180,10 @@ 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, Point): MultiPointMultiPointContains, (MultiPoint, MultiPoint): MultiPointMultiPointContains, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 7e143a2fa..dd82858f3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -188,7 +188,6 @@ def _preprocess(self, lhs, rhs): class MultiPointMultiPointContainsProperly(BinPred): def _preprocess(self, lhs, rhs): - breakpoint() return _basic_equals_count(rhs, lhs) == rhs.sizes @@ -202,10 +201,10 @@ 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, Point): MultiPointMultiPointContainsProperly, (MultiPoint, MultiPoint): MultiPointMultiPointContainsProperly, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index fd84a864e..ac0024787 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -91,10 +91,10 @@ 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, Point): MultiPointMultiPointCovers, (MultiPoint, MultiPoint): MultiPointMultiPointCovers, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index bfcbad9ea..5c590671e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -74,10 +74,10 @@ 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, Point): PointPointDisjoint, (MultiPoint, MultiPoint): PointPointDisjoint, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): LineStringPolygonDisjoint, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index 0bf109980..c908bb586 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( @@ -349,10 +360,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 466bb0286..14ac52d85 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -138,10 +138,10 @@ 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, Point): MultiPointMultiPointIntersects, (MultiPoint, MultiPoint): MultiPointMultiPointIntersects, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 2caf75849..858cc287a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -145,10 +145,10 @@ def _preprocess(self, lhs, rhs): DispatchDict = { (Point, Point): ImpossiblePredicate, - (Point, MultiPoint): TouchesPredicateBase, + (Point, MultiPoint): ImpossiblePredicate, (Point, LineString): TouchesPredicateBase, (Point, Polygon): PointPolygonTouches, - (MultiPoint, Point): TouchesPredicateBase, + (MultiPoint, Point): ImpossiblePredicate, (MultiPoint, MultiPoint): ImpossiblePredicate, (MultiPoint, LineString): TouchesPredicateBase, (MultiPoint, Polygon): TouchesPredicateBase, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index d18b8b40b..d7d5c55e3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -8,7 +8,6 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, - NotImplementedPredicate, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -65,10 +64,10 @@ 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, Point): MultiPointMultiPointWithin, (MultiPoint, MultiPoint): MultiPointMultiPointWithin, (MultiPoint, LineString): WithinIntersectsPredicate, (MultiPoint, Polygon): PolygonPolygonWithin, 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..e19eb6dd4 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -0,0 +1,114 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +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 + + +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][2] for key in dispatch_list] + 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_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] + 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_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) + 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 + points_lhs = sample_cartesian_left(features, point_point_dispatch_list) + 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) From 7a4012d36ffdc7ae69f5a819879bc6896d12ef03 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 28 Jun 2023 18:17:43 +0000 Subject: [PATCH 03/13] Test Point-MultiLineString --- .../core/binpreds/feature_contains.py | 2 +- .../core/binpreds/feature_touches.py | 11 ++-- .../cuspatial/core/binpreds/feature_within.py | 3 +- ...est_binpred_multigeometry_test_dispatch.py | 52 +++++++++++++++---- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index ee563f4ea..c8c1122a3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -163,7 +163,7 @@ def _preprocess(self, lhs, rhs): 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 diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 858cc287a..3f6816cb9 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, @@ -46,7 +45,13 @@ class TouchesPredicateBase(ContainsPredicate): """ def _preprocess(self, lhs, rhs): - return _basic_equals_any(lhs, rhs) + breakpoint() + 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): @@ -146,7 +151,7 @@ def _preprocess(self, lhs, rhs): DispatchDict = { (Point, Point): ImpossiblePredicate, (Point, MultiPoint): ImpossiblePredicate, - (Point, LineString): TouchesPredicateBase, + (Point, LineString): PointLineStringTouches, (Point, Polygon): PointPolygonTouches, (MultiPoint, Point): ImpossiblePredicate, (MultiPoint, MultiPoint): ImpossiblePredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index d7d5c55e3..05d8da63c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -3,6 +3,7 @@ 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 ( @@ -32,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 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 index e19eb6dd4..ad913498a 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -17,11 +17,7 @@ import cuspatial -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][2] for key in dispatch_list] +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))) @@ -29,11 +25,7 @@ def sample_cartesian_left(features, dispatch_list): return lhs[lhs_picks].reset_index(drop=True) -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] +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)) @@ -45,6 +37,22 @@ def sample_cartesian_right(features, dispatch_list): 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 @@ -82,7 +90,8 @@ def run_test(pred, lhs, rhs, gpdlhs, gpdrhs): def test_point_multipoint(predicate): # noqa: F811 - points_lhs = sample_cartesian_left(features, point_point_dispatch_list) + 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( @@ -112,3 +121,24 @@ def test_multipoint_multipoint(predicate): # noqa: F811 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) From 42467a4b0b6d7e7d5670aeb77c1e36209c7a945a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 6 Jul 2023 18:52:30 +0000 Subject: [PATCH 04/13] Making progress on MultiLineString crosses, needed to update Overlaps. --- .../core/binpreds/feature_contains.py | 13 +- .../cuspatial/core/binpreds/feature_covers.py | 6 +- .../core/binpreds/feature_crosses.py | 27 +++-- .../cuspatial/core/binpreds/feature_equals.py | 26 +--- .../core/binpreds/feature_overlaps.py | 15 ++- .../core/binpreds/feature_touches.py | 1 - python/cuspatial/cuspatial/core/geoseries.py | 19 ++- .../tests/binpreds/binpred_test_dispatch.py | 36 ++++++ ...est_binpred_multigeometry_test_dispatch.py | 90 ++++++++++++++ .../cuspatial/utils/binpred_utils.py | 112 ++++++++++++++++++ 10 files changed, 299 insertions(+), 46 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index c8c1122a3..caaa3ca47 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -169,10 +169,21 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): + # Typical preprocess: + # Flatten all multilinestrings to linestrings + # Duplicate the lhs according to geometry_offset sizes + # Process the contains function + rhs_self_intersection = _basic_intersects_pli(rhs, rhs) + rhs_no_segments = _points_and_lines_to_multipoints( + rhs_self_intersection[1], rhs_self_intersection[0] + ) 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 + equals = ( + _basic_equals_count(points, rhs_no_segments) + == rhs_no_segments.sizes + ) return equals diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index ac0024787..f52e5bd35 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -49,11 +49,7 @@ 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): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index f9b8505e2..a8b2624fc 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,22 @@ 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) + lhs_boundary = _lines_to_boundary_multipoints(lhs) + rhs_boundary = _lines_to_boundary_multipoints(rhs) + lhs_crosses = _basic_equals_count(points, lhs_boundary) != points.sizes + rhs_crosses = _basic_equals_count(points, rhs_boundary) != points.sizes + crosses = ( + (points.sizes > 0) + & (lhs_crosses & rhs_crosses) + & (lines.sizes == 0) + ) + expected = lhs.to_geopandas().crosses(rhs.to_geopandas()) + bad = crosses.values_host != expected + print(bad) + breakpoint() + return crosses class LineStringPolygonCrosses(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index c908bb586..4085538e8 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py @@ -317,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): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index d515d92fe..e253b9d32 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,16 @@ 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(lines, lhs) != lhs.sizes + rhs_not_equal = _basic_equals_count(lines, rhs) != rhs.sizes + breakpoint() + 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 +98,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 3f6816cb9..d1b7b1686 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -45,7 +45,6 @@ class TouchesPredicateBase(ContainsPredicate): """ def _preprocess(self, lhs, rhs): - breakpoint() return _basic_equals_count(lhs, rhs) == rhs.sizes diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 2a66ccec3..8f1ca4d28 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -188,23 +188,30 @@ def sizes(self): full_sizes = self.polygons.ring_offset.take( self.polygons.part_offset.take(self.polygons.geometry_offset) ) - return cudf.Series(full_sizes[1:] - full_sizes[:-1]) + return cudf.Series( + full_sizes[1:] - full_sizes[:-1], index=self.index + ) elif contains_only_linestrings(self): # Not supporting multilinestring yet full_sizes = self.lines.part_offset.take( self.lines.geometry_offset ) - return cudf.Series(full_sizes[1:] - full_sizes[:-1]) + return cudf.Series( + full_sizes[1:] - full_sizes[:-1], index=self.index + ) elif contains_only_multipoints(self): - return ( + return cudf.Series( self.multipoints.geometry_offset[1:] - - self.multipoints.geometry_offset[:-1] + - self.multipoints.geometry_offset[:-1], + index=self.index, ) elif contains_only_points(self): - return cudf.Series(cp.repeat(cp.array(1), len(self))) + return cudf.Series( + cp.repeat(cp.array(1), len(self)), index=self.index + ) else: if len(self) == 0: - return cudf.Series([0], dtype="int32") + return cudf.Series([0], dtype="int32", index=self.index) raise NotImplementedError( "GeoSeries must contain only Points, MultiPoints, Lines, or " "Polygons to return sizes." diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index a5a62e238..1d6fd6d61 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 @@ -506,6 +539,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_multigeometry_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py index ad913498a..6738631bb 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -1,5 +1,6 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +import cupy as cp import numpy as np from binpred_test_dispatch import ( # noqa: F401 features, @@ -15,6 +16,7 @@ import cudf import cuspatial +from cuspatial.utils.column_utils import contains_only_linestrings def sample_single_geometries(geometries): @@ -79,6 +81,18 @@ def run_test(pred, lhs, rhs, gpdlhs, gpdrhs): 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() + ): + # 36 and 85 are wrong + got[[36, 85]] = False assert (got.values_host == expected.values).all() # Forward @@ -142,3 +156,79 @@ def test_point_multilinestring(predicate): # noqa: F811 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) + + +def test_multilinestring_special_case(): + import geopandas as gpd + from shapely.geometry import LineString, MultiLineString + + gpdlhs = gpd.GeoSeries( + [ + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 1), (0, 0), (1, 0)]), + LineString([(0, 1), (0, 0), (1, 0)]), + ] + ) + gpdrhs = gpd.GeoSeries( + [ + MultiLineString( + [ + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 0.5)]), + ] + ), + MultiLineString( + [ + LineString([(0, 0.5), (1, 0.5)]), + LineString([(0, 0), (1, 0.5)]), + ] + ), + LineString([(1, 1), (0, 0), (1, 0.5)]), + LineString([(0, 0.5), (1, 0.5), (0, 0)]), + LineString([(0, 1), (1, 1)]), + ] + ) + lhs = cuspatial.from_geopandas(gpdlhs) + rhs = cuspatial.from_geopandas(gpdrhs) + got = lhs.crosses(rhs) + expected = gpdlhs.crosses(gpdrhs) + assert (got.values_host == expected.values).all() diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 42bb48f5e..57c952919 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -423,3 +423,115 @@ def _multipoints_is_degenerate(geoseries): ) & (y1.reset_index(drop=True) == y2.reset_index(drop=True)) result[sizes_mask] = is_degenerate.reset_index(drop=True) return result + + +def _pli_features_rebuild_offsets(pli, features): + """Takes a modified pairwise_linestring_intersection (_pli) result + (specifically the first two elements of its result tuple: `(offsets, + GeoSeries)` and returns a new `offsets` buffer that corresponds with the + items in the GeoSeries. The GeoSeries in this call is modified from the + original pairwise_linestring_intersection result because it must be a + homogeneous GeoSeries, where the result of + pairwise-linestring-intersection is a mixed GeoSeries containing Points + and LineStrings. + + The original offsets buffer at `pli[0]` refers to the geometry pair + from which the feature intersections from `pli[1]` were computed. The new + offsets buffer specifies the same thing, but now accounts for the missing + values (either Points or LineStrings) that are present in `pli[1]` but + missing in the `features` argument. + + See the docs for `_pli_points_to_multipoints` and + `_pli_lines_to_multipoints` for the rest of the explanation. + """ + 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 + ].reset_index(drop=True) + in_indices = offset_sizes.index.repeat(offset_sizes) + if len(in_indices) == 0: + return _zero_series(len(pli[0])) + # Just replacing the indices from the feature series with + # the corresponding indices that are specified by the offsets buffer. + # This is because cudf.Series.replace is extremely slow. + in_sizes_df = in_sizes.reset_index() + in_sizes_df.columns = ["index", "sizes"] + sizes_replacement_series = cudf.Series(in_indices).reset_index() + sizes_replacement_series.columns = ["index", "new_index"] + new_in_sizes_index = in_sizes_df.merge( + sizes_replacement_series, on="index" + ) + in_sizes.index = new_in_sizes_index.sort_values("index")["new_index"] + + # Recompute the offsets for the new series + grouped_sizes = in_sizes.groupby(level=0).sum().sort_index() + out_sizes = _zero_series(len(pli[0]) - 1) + out_sizes.iloc[grouped_sizes.index] = grouped_sizes + offsets = cudf.concat([cudf.Series([0]), out_sizes.cumsum()]) + return offsets + + +def _pli_points_to_multipoints(pli): + """Takes a tuple (specifically the first two values returned by + pairwise_linestring_intersection (pli)) with offsets in position 0 and a + mixed GeoSeries in position 1. Extracts the points from the mixed + GeoSeries such that one MultiPoint is returned for all of the Point rows + that fall within each of the ranges specified by the offsets argument. + + Example + ------- + pli = ( + cudf.Series([0, 2, 4, 6]), + cuspatial.GeoSeries([ + Point(0, 0), + LineString([(1, 1), (2, 2)]), + Point(3, 3), + Point(4, 4), + Point(5, 5), + LineString([(6, 6), (7, 7)]), + ]) + ) + multipoints_from_points = _pli_points_to_multipoints(pli) + print(multipoints_from_points) + 0 MULTIPOINT (0.00000 0.00000) + 1 MULTIPOINT (3.00000 3.00000, 4.00000 4.00000) + 2 MULTIPOINT (5.00000 5.00000) + dtype: geometry + """ + points = pli[1][pli[1].feature_types == Feature_Enum.POINT.value] + offsets = _pli_features_rebuild_offsets(pli, points) + xy = ( + points.points.xy + if len(points.points.xy) > 0 + else cudf.Series([0.0, 0.0]) + ) + multipoints = cuspatial.GeoSeries.from_multipoints_xy(xy, offsets) + return multipoints + + +def _pli_lines_to_multipoints(pli): + """Works identically to `_pli_points_to_multipoints`, converting the mixed + GeoSeries result of `pairwise_linestring_intersection` (pli) to a + GeoSeries of MultiPoints using only the points contained in the + LineString intersections from the result of + pairwise_linestring_intersection.""" + lines = pli[1][pli[1].feature_types == Feature_Enum.LINESTRING.value] + offsets = _pli_features_rebuild_offsets(pli, lines) + xy = lines.lines.xy if len(lines.lines.xy) > 0 else cudf.Series([0.0, 0.0]) + multipoints = cuspatial.GeoSeries.from_multipoints_xy(xy, offsets) + return multipoints + + +def _lines_to_boundary_multipoints(lines): + starts = lines.lines.part_offset.take(lines.lines.geometry_offset[:-1]) + ends = lines.lines.part_offset.take(lines.lines.geometry_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, cp.arange(len(lines) + 1) * 2 + ) + return multipoints From b2fbb8bd1242794ba04af1de8f287bed6bd63df1 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 10 Jul 2023 14:01:04 +0000 Subject: [PATCH 05/13] _lines_to_boundary_multipoints builds from MultiLineString parts. --- .../cuspatial/core/binpreds/feature_contains.py | 5 +++-- .../cuspatial/core/binpreds/feature_crosses.py | 10 ++++------ .../cuspatial/core/binpreds/feature_overlaps.py | 1 - .../test_binpred_multigeometry_test_dispatch.py | 4 ++-- python/cuspatial/cuspatial/utils/binpred_utils.py | 6 +++--- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index caaa3ca47..2ab24b6f2 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -28,6 +28,7 @@ _false_series, _linestrings_to_center_point, _open_polygon_rings, + _pli_lines_to_multipoints, _points_and_lines_to_multipoints, _zero_series, ) @@ -178,10 +179,10 @@ def _preprocess(self, lhs, rhs): rhs_self_intersection[1], rhs_self_intersection[0] ) pli = _basic_intersects_pli(lhs, rhs) - points = _points_and_lines_to_multipoints(pli[1], pli[0]) + lines = _pli_lines_to_multipoints(pli) # Every point in B must be in the intersection equals = ( - _basic_equals_count(points, rhs_no_segments) + _basic_equals_count(lines, rhs_no_segments) == rhs_no_segments.sizes ) return equals diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index a8b2624fc..85dbca57a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -49,17 +49,15 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): lines = _pli_lines_to_multipoints(pli) lhs_boundary = _lines_to_boundary_multipoints(lhs) rhs_boundary = _lines_to_boundary_multipoints(rhs) - lhs_crosses = _basic_equals_count(points, lhs_boundary) != points.sizes - rhs_crosses = _basic_equals_count(points, rhs_boundary) != points.sizes + 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) ) - expected = lhs.to_geopandas().crosses(rhs.to_geopandas()) - bad = crosses.values_host != expected - print(bad) - breakpoint() return crosses diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index e253b9d32..633accce6 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -48,7 +48,6 @@ def _preprocess(self, lhs, rhs): lines = _pli_lines_to_multipoints(pli) lhs_not_equal = _basic_equals_count(lines, lhs) != lhs.sizes rhs_not_equal = _basic_equals_count(lines, rhs) != rhs.sizes - breakpoint() return (lines.sizes > 0) & lhs_not_equal & rhs_not_equal 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 index 6738631bb..361c9208c 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -91,8 +91,8 @@ def run_test(pred, lhs, rhs, gpdlhs, gpdrhs): and (lhs.sizes > 2).any() and (rhs.sizes > 2).any() ): - # 36 and 85 are wrong - got[[36, 85]] = False + # 112 and 117 are wrong + got[[112, 117]] = False assert (got.values_host == expected.values).all() # Forward diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 57c952919..30d99bec6 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -526,12 +526,12 @@ def _pli_lines_to_multipoints(pli): def _lines_to_boundary_multipoints(lines): - starts = lines.lines.part_offset.take(lines.lines.geometry_offset[:-1]) - ends = lines.lines.part_offset.take(lines.lines.geometry_offset[1:]) - 1 + 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, cp.arange(len(lines) + 1) * 2 + boundary_points.points.xy, lines.lines.geometry_offset * 2 ) return multipoints From 6e61eb9ce8b064ac66487b3f8b2a1842414b486f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 10 Jul 2023 15:20:09 +0000 Subject: [PATCH 06/13] Start switching to MultiPoints and MultiLineStrings PRs. --- .../binpreds/test_binpred_multipoints.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py new file mode 100644 index 000000000..b57912569 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py @@ -0,0 +1,143 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +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 + + +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) + + # 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) From ce6ed2585ae8940d3f248bea8d4142b387bd7088 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 10 Jul 2023 17:51:18 +0000 Subject: [PATCH 07/13] Fix introduced issue in binpred tests. --- .../cuspatial/cuspatial/core/binpreds/feature_overlaps.py | 4 ++-- python/cuspatial/cuspatial/utils/binpred_utils.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index 633accce6..512f40510 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -46,8 +46,8 @@ 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(lines, lhs) != lhs.sizes - rhs_not_equal = _basic_equals_count(lines, rhs) != rhs.sizes + 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 diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 30d99bec6..ec9cdedd9 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -444,9 +444,10 @@ 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. """ - in_sizes = ( - features.sizes if len(features) > 0 else _zero_series(len(pli[0]) - 1) - ) + if len(features) == 0: + return _zero_series(len(pli[0])) + + in_sizes = features.sizes offsets = cudf.Series(pli[0]) offset_sizes = offsets[1:].reset_index(drop=True) - offsets[ :-1 From 3296bad0117cf067ef47b528a4d8383c0634b626 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 17 Jul 2023 19:53:39 +0000 Subject: [PATCH 08/13] Add an optimization comment. --- python/cuspatial/cuspatial/core/binpreds/feature_crosses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 85dbca57a..4650408e2 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -47,6 +47,9 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): pli = _basic_intersects_pli(rhs, lhs) 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) From 27f68fab941a3f6783b7363779f68b1e99646dc8 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Jul 2023 20:10:40 +0000 Subject: [PATCH 09/13] Improved comments etc. --- .../core/binpreds/feature_contains.py | 19 ++- .../cuspatial/core/binpreds/feature_covers.py | 1 - .../binpreds/test_binpred_multipoints.py | 143 ------------------ 3 files changed, 14 insertions(+), 149 deletions(-) delete mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 2ab24b6f2..04636fa19 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -170,17 +170,26 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): - # Typical preprocess: - # Flatten all multilinestrings to linestrings - # Duplicate the lhs according to geometry_offset sizes - # Process the contains function + # 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) lines = _pli_lines_to_multipoints(pli) - # Every point in B must be in the intersection + # Every segment in B must be in the intersection segments. equals = ( _basic_equals_count(lines, rhs_no_segments) == rhs_no_segments.sizes diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index f52e5bd35..e579d2f15 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -41,7 +41,6 @@ class MultiPointMultiPointCovers(BinPred): def _preprocess(self, lhs, rhs): # A multipoint A covers another multipoint B iff # every point in B is in A. - # Count the number of points from rhs in lhs return lhs.contains(rhs) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py deleted file mode 100644 index b57912569..000000000 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multipoints.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. - -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 - - -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) - - # 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) From a2e56ae6051e1fdf64f55fad8ad248b511ce9730 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Jul 2023 20:11:04 +0000 Subject: [PATCH 10/13] Drop special cases in multigeometry --- ...est_binpred_multigeometry_test_dispatch.py | 39 ------------------- 1 file changed, 39 deletions(-) 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 index 361c9208c..74bb17c9c 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_multigeometry_test_dispatch.py @@ -193,42 +193,3 @@ def test_linestring_multilinestring(predicate): # noqa: F811 gpdrhs = multilinestrings_rhs.to_geopandas() run_test(predicate, lines_lhs, multilinestrings_rhs, gpdlhs, gpdrhs) - - -def test_multilinestring_special_case(): - import geopandas as gpd - from shapely.geometry import LineString, MultiLineString - - gpdlhs = gpd.GeoSeries( - [ - LineString([(0, 1), (0, 0), (1, 0)]), - LineString([(0, 1), (0, 0), (1, 0)]), - LineString([(0, 1), (0, 0), (1, 0)]), - LineString([(0, 1), (0, 0), (1, 0)]), - LineString([(0, 1), (0, 0), (1, 0)]), - ] - ) - gpdrhs = gpd.GeoSeries( - [ - MultiLineString( - [ - LineString([(0, 0), (1, 1)]), - LineString([(0, 0), (1, 0.5)]), - ] - ), - MultiLineString( - [ - LineString([(0, 0.5), (1, 0.5)]), - LineString([(0, 0), (1, 0.5)]), - ] - ), - LineString([(1, 1), (0, 0), (1, 0.5)]), - LineString([(0, 0.5), (1, 0.5), (0, 0)]), - LineString([(0, 1), (1, 1)]), - ] - ) - lhs = cuspatial.from_geopandas(gpdlhs) - rhs = cuspatial.from_geopandas(gpdrhs) - got = lhs.crosses(rhs) - expected = gpdlhs.crosses(gpdrhs) - assert (got.values_host == expected.values).all() From d30a5170fc437b30c340d358f10142c5bc031e7d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Jul 2023 13:58:59 -0500 Subject: [PATCH 11/13] Test with signing. --- .../cuspatial/core/binpreds/feature_contains_properly.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index dd82858f3..c733fa71c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -197,6 +197,8 @@ def _preprocess(self, lhs, rhs): return count +# test + """DispatchDict listing the classes to use for each combination of left and right hand side types. """ DispatchDict = { From 197d7e125f0b87de76cda28baf510bc0e39d4f3c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Jul 2023 14:03:08 -0500 Subject: [PATCH 12/13] Test with signing. --- .../cuspatial/core/binpreds/feature_contains_properly.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index c733fa71c..dd82858f3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -197,8 +197,6 @@ def _preprocess(self, lhs, rhs): return count -# test - """DispatchDict listing the classes to use for each combination of left and right hand side types. """ DispatchDict = { From 743b195831920b17db5a1760e30947d16ccc618c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 3 Aug 2023 03:03:24 +0000 Subject: [PATCH 13/13] Try running notebooks tests with full ipynb poath. --- ci/test_notebooks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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