From 758c66b0701c59cb9c0c477d651ea3f0af2f8b89 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 17 Oct 2022 16:27:44 -0500 Subject: [PATCH 01/90] Add tests to verify border-exclusion and modify point_in_polygon.cuh to produce the correct results. --- cpp/src/utility/point_in_polygon.cuh | 10 +++- .../cuspatial/tests/test_contains.py | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 python/cuspatial/cuspatial/tests/test_contains.py diff --git a/cpp/src/utility/point_in_polygon.cuh b/cpp/src/utility/point_in_polygon.cuh index 8070a3b09..8b6ed9ccf 100644 --- a/cpp/src/utility/point_in_polygon.cuh +++ b/cpp/src/utility/point_in_polygon.cuh @@ -56,7 +56,15 @@ inline __device__ bool is_point_in_polygon(T const x, T rise = y1 - y0; T rise_to_point = y - y0; - if (y_in_bounds && x < (run / rise) * rise_to_point + x0) { in_polygon = not in_polygon; } + // colinearity test + T d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); + T d1 = (x - x0) * (x - x1) + (y - y0) * (y - y1); + T d2 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y); + bool colinear = d1 + d2 == d; + + if (!colinear && y_in_bounds && x < (run / rise) * rise_to_point + x0) { + in_polygon = not in_polygon; + } } } diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py new file mode 100644 index 000000000..737953b9a --- /dev/null +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -0,0 +1,49 @@ +import geopandas as gpd +import pandas as pd +from shapely.geometry import Point, Polygon + +import cudf + +import cuspatial + + +def test_point_shared_with_polygon(): + point = Point([0, 0]) + polygon = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) + point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) + polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) + result = cuspatial.point_in_polygon( + point_series.points.x, + point_series.points.y, + polygon_series.polygons.ring_offset[:-1], + polygon_series.polygons.part_offset[:-1], + polygon_series.polygons.x, + polygon_series.polygons.y, + ) + cudf.testing.assert_frame_equal(result, cudf.DataFrame({0: False})) + gpdpoint = point_series.to_pandas() + gpdpolygon = polygon_series.to_pandas() + pd.testing.assert_series_equal( + gpdpolygon.contains(gpdpoint), pd.Series([False]) + ) + + +def test_point_collinear_with_polygon(): + point = Point([0.5, 0.0]) + polygon = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) + point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) + polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) + result = cuspatial.point_in_polygon( + point_series.points.x, + point_series.points.y, + polygon_series.polygons.ring_offset[:-1], + polygon_series.polygons.part_offset[:-1], + polygon_series.polygons.x, + polygon_series.polygons.y, + ) + cudf.testing.assert_frame_equal(result, cudf.DataFrame({0: False})) + gpdpoint = point_series.to_pandas() + gpdpolygon = polygon_series.to_pandas() + pd.testing.assert_series_equal( + gpdpolygon.contains(gpdpoint), pd.Series([False]) + ) From 76c8b1da5ac42e2db161a979bd25cbd45f557497 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Oct 2022 13:07:07 -0500 Subject: [PATCH 02/90] Trying to write contains tests, having difficulties. --- python/cuspatial/cuspatial/core/geoseries.py | 31 +++++++++++++++++++ .../cuspatial/cuspatial/utils/column_utils.py | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index b2f5450c2..4b8c295ca 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -24,6 +24,8 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta +from cuspatial.core.spatial.join import point_in_polygon +from cuspatial.utils.column_utils import contains_only_polygons T = TypeVar("T", bound="GeoSeries") @@ -517,3 +519,32 @@ def to_arrow(self): arrow_polygons, ], ) + + def contains(self, other, align=True): + if contains_only_polygons(self) is False: + raise TypeError("left series contains non-polygons.") + # RHS conditioning: + # point in polygon + # mpoint in polygon + # linestring in polygon + # polygon in polygon + + # call pip on the three subtypes on the right: + point_result = point_in_polygon( + other.points.x, + other.points.y, + self.polygons.ring_offset[:-1], + self.polygons.part_offset[:-1], + self.polygons.x, + self.polygons.y, + ) + return point_result + """ + # Apply binpreds rules on results: + # point in polygon = true for row + # reverse index, points indices refer back to row + # indices + # mpoint in polygon for all points = true + # linestring in polygon for all points = true + # polygon in polygon for all points = true + """ diff --git a/python/cuspatial/cuspatial/utils/column_utils.py b/python/cuspatial/cuspatial/utils/column_utils.py index 333e59bf5..3e5ac322f 100644 --- a/python/cuspatial/cuspatial/utils/column_utils.py +++ b/python/cuspatial/cuspatial/utils/column_utils.py @@ -1,10 +1,12 @@ # Copyright (c) 2020, NVIDIA CORPORATION. +from typing import TypeVar + import numpy as np from cudf.api.types import is_datetime_dtype -from cuspatial.core.geoseries import GeoSeries +GeoSeries = TypeVar("GeoSeries", bound="GeoSeries") def normalize_point_columns(*cols): From beacf7689faefb0c210b58d493853d9ddfad75e4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Oct 2022 15:45:39 -0500 Subject: [PATCH 03/90] Pass all clockwise polygon tests. --- .../cuspatial/experimental/detail/point_in_polygon.cuh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index e9e3412f8..95fac20c8 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -57,8 +57,11 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, Cart2dItDiffType const& num_poly_points) { using T = iterator_vec_base_type; + const T EPSILON = 0.00000001; + bool point_is_within = false; + bool is_colinear = false; // for each ring for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { int32_t ring_idx_next = ring_idx + 1; @@ -80,14 +83,19 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; + auto lhs = (test_point.x - a.x) * rise + EPSILON; auto rhs = run * rise_to_point; if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) point_is_within = not point_is_within; + + // colinearity test + is_colinear = run * rise_to_point - rise * (test_point.x - a.x) == 0; } b = a; y0_flag = y1_flag; } + if(is_colinear) + point_is_within = false; } return point_is_within; From 67d61534f0b78889cd6d8b57c1c7b4eb88ce67b4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Oct 2022 20:21:43 -0500 Subject: [PATCH 04/90] Test of boundary exclusion. --- .../experimental/detail/point_in_polygon.cuh | 7 +- .../cuspatial/tests/test_gpd_contains.py | 111 ++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 python/cuspatial/cuspatial/tests/test_gpd_contains.py diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 95fac20c8..45942c5b2 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -62,6 +62,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, bool point_is_within = false; bool is_colinear = false; + bool is_same = false; // for each ring for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { int32_t ring_idx_next = ring_idx + 1; @@ -80,6 +81,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T run = b.x - a.x; T rise = b.y - a.y; T rise_to_point = test_point.y - a.y; + T run_to_point = test_point.x - a.x; // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x @@ -89,12 +91,13 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, point_is_within = not point_is_within; // colinearity test - is_colinear = run * rise_to_point - rise * (test_point.x - a.x) == 0; + is_colinear = (run * rise_to_point - run_to_point * rise) * + (run * rise_to_point - run_to_point * rise) <= EPSILON; } b = a; y0_flag = y1_flag; } - if(is_colinear) + if(is_colinear || is_same) point_is_within = false; } diff --git a/python/cuspatial/cuspatial/tests/test_gpd_contains.py b/python/cuspatial/cuspatial/tests/test_gpd_contains.py new file mode 100644 index 000000000..593305b77 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/test_gpd_contains.py @@ -0,0 +1,111 @@ + +import pytest + +import geopandas as gpd +import pandas as pd +from shapely.geometry import Point, Polygon + +import cudf + +import cuspatial + + +@pytest.mark.parametrize( + "point, polygon, expects", + [ + # wound clockwise, should be false + [ + Point([0, 0]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + [ + Point([0.0, 1.0]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + [ + Point([1.0, 1.0]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + [ + Point([0.0, 0.5]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + [ + Point([0.5, 0.5]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + [ + Point([0.5, 1]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + False + ], + # wound clockwise, should be true + [ + Point([0.25, 0.5]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + True + ], + [ + Point([0.75, 0.9]), + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + True + ], + # wound counter clockwise, should be false + [ + Point([0.0, 0.0]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + False + ], + [ + Point([1.0, 0.0]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + False + ], + [ + Point([1.0, 1.0]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + False + ], + [ + Point([0.5, 0.0]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + False + ], + [ + Point([0.5, 0.5]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + False + ], + # wound counter clockwise, should be true + [ + Point([0.5, 0.25]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + True + ], + [ + Point([0.9, 0.75]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + True + ], + ] +) +def test_point_in_polygon(point, polygon, expects): + point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) + polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) + result = cuspatial.point_in_polygon( + point_series.points.x, + point_series.points.y, + polygon_series.polygons.part_offset[:-1], + polygon_series.polygons.ring_offset[:-1], + polygon_series.polygons.x, + polygon_series.polygons.y, + ) + result[0].name = None + gpdpoint = point_series.to_pandas() + gpdpolygon = polygon_series.to_pandas() + assert gpdpolygon.contains(gpdpoint).values == result[0].values_host From 8a19829aa3b180a8111155c44253ca0bd150b4ee Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Oct 2022 10:33:35 -0500 Subject: [PATCH 05/90] Create is_point_colinear_with_polygon method. Get colinearity working. --- .../experimental/detail/point_in_polygon.cuh | 80 ++++++++---- .../cuspatial/tests/test_gpd_contains.py | 116 +++++------------- 2 files changed, 92 insertions(+), 104 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 45942c5b2..11d6f7f48 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -29,7 +29,6 @@ namespace cuspatial { namespace detail { - /** * @brief Kernel to test if a point is inside a polygon. * @@ -57,12 +56,9 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, Cart2dItDiffType const& num_poly_points) { using T = iterator_vec_base_type; - const T EPSILON = 0.00000001; - bool point_is_within = false; - bool is_colinear = false; - bool is_same = false; + bool is_colinear = false; // for each ring for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { int32_t ring_idx_next = ring_idx + 1; @@ -70,39 +66,81 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, int32_t ring_end = (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - Cart2d b = poly_points_first[ring_end - 1]; + Cart2d b = poly_points_first[ring_begin]; bool y0_flag = b.y > test_point.y; bool y1_flag; // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - y1_flag = a.y > test_point.y; - if (y1_flag != y0_flag) { - T run = b.x - a.x; - T rise = b.y - a.y; - T rise_to_point = test_point.y - a.y; - T run_to_point = test_point.x - a.x; + for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { + Cart2d const a = poly_points_first[point_idx]; + y1_flag = a.y > test_point.y; + T run = b.x - a.x; + T rise = b.y - a.y; + T rise_to_point = test_point.y - a.y; + T run_to_point = test_point.x - a.x; + is_colinear = is_colinear || (run * rise_to_point - run_to_point * rise) * + (run * rise_to_point - run_to_point * rise) == + 0; + if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise + EPSILON; + auto lhs = (test_point.x - a.x) * rise; auto rhs = run * rise_to_point; if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) point_is_within = not point_is_within; - - // colinearity test - is_colinear = (run * rise_to_point - run_to_point * rise) * - (run * rise_to_point - run_to_point * rise) <= EPSILON; } b = a; y0_flag = y1_flag; } - if(is_colinear || is_same) - point_is_within = false; + if (is_colinear) point_is_within = false; } return point_is_within; } +/** + * @brief Kernel to test if a point colinear with a polygon + */ +template ::difference_type, + class Cart2dItDiffType = typename std::iterator_traits::difference_type> +__device__ inline bool is_point_colinear_with_polygon(Cart2d const& test_point, + OffsetType poly_begin, + OffsetType poly_end, + OffsetIterator ring_offsets_first, + OffsetItDiffType const& num_rings, + Cart2dIt poly_points_first, + Cart2dItDiffType const& num_poly_points) +{ + using T = iterator_vec_base_type; + + bool point_is_within = false; + bool is_colinear = false; + // for each ring + for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { + int32_t ring_idx_next = ring_idx + 1; + int32_t ring_begin = ring_offsets_first[ring_idx]; + int32_t ring_end = + (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; + + Cart2d b = poly_points_first[ring_begin]; + // for each line segment, including the segment between the last and first vertex + for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { + Cart2d const a = poly_points_first[point_idx]; + T run = b.x - a.x; + T rise = b.y - a.y; + T rise_to_point = test_point.y - a.y; + T run_to_point = test_point.x - a.x; + is_colinear = + (run * rise_to_point - run_to_point * rise) * (run * rise_to_point - run_to_point * rise) == + 0; + if (is_colinear) break; + } + } + return is_colinear; +} template Date: Fri, 21 Oct 2022 10:40:33 -0500 Subject: [PATCH 06/90] Tweak one test because it shares a point with the offending polygon. --- .../cuspatial/tests/spatial/join/test_point_in_polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py b/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py index 7c7010728..c01f37e05 100644 --- a/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py +++ b/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py @@ -236,7 +236,7 @@ def test_three_points_two_features(): ) expected = cudf.DataFrame() expected[0] = [True, True, False] - expected[1] = [True, False, True] + expected[1] = [False, False, True] cudf.testing.assert_frame_equal(expected, result) From 11be1352370ea92bdd6b8659731ecc9367cdf16f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Oct 2022 11:24:11 -0500 Subject: [PATCH 07/90] Modify colinearity test to early terminate. --- .../experimental/detail/point_in_polygon.cuh | 16 +++++++++++----- python/cuspatial/benchmarks/api/bench_api.py | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 11d6f7f48..32818f40b 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -71,15 +71,18 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, bool y1_flag; // for each line segment, including the segment between the last and first vertex for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - y1_flag = a.y > test_point.y; - T run = b.x - a.x; - T rise = b.y - a.y; + Cart2d const a = poly_points_first[point_idx]; + y1_flag = a.y > test_point.y; + T run = b.x - a.x; + T rise = b.y - a.y; + + // colinerity test T rise_to_point = test_point.y - a.y; T run_to_point = test_point.x - a.x; is_colinear = is_colinear || (run * rise_to_point - run_to_point * rise) * (run * rise_to_point - run_to_point * rise) == 0; + if (is_colinear) { break; } if (y1_flag != y0_flag) { // Transform the following inequality to avoid division @@ -92,7 +95,10 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, b = a; y0_flag = y1_flag; } - if (is_colinear) point_is_within = false; + if (is_colinear) { + point_is_within = false; + break; + } } return point_is_within; diff --git a/python/cuspatial/benchmarks/api/bench_api.py b/python/cuspatial/benchmarks/api/bench_api.py index 829be7447..0209d9c1f 100644 --- a/python/cuspatial/benchmarks/api/bench_api.py +++ b/python/cuspatial/benchmarks/api/bench_api.py @@ -165,8 +165,8 @@ def bench_quadtree_on_points(benchmark, gpu_dataframe): def bench_quadtree_point_in_polygon(benchmark, polygons): polygons = polygons["geometry"].polygons - x_points = (cupy.random.random(10000000) - 0.5) * 360 - y_points = (cupy.random.random(10000000) - 0.5) * 180 + x_points = (cupy.random.random(50000000) - 0.5) * 360 + y_points = (cupy.random.random(50000000) - 0.5) * 180 scale = 5 max_depth = 7 min_size = 125 @@ -263,8 +263,8 @@ def bench_quadtree_point_to_nearest_polyline(benchmark): def bench_point_in_polygon(benchmark, gpu_dataframe): - x_points = (cupy.random.random(10000000) - 0.5) * 360 - y_points = (cupy.random.random(10000000) - 0.5) * 180 + x_points = (cupy.random.random(50000000) - 0.5) * 360 + y_points = (cupy.random.random(50000000) - 0.5) * 180 short_dataframe = gpu_dataframe.iloc[0:32] geometry = short_dataframe["geometry"] benchmark( From 5aa9c09c37e88361711c5a0bbad7348315ccbcfe Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Oct 2022 13:24:30 -0500 Subject: [PATCH 08/90] Create a point_in_polygon_one_to_one for better correspondence with contains and performance. --- cpp/CMakeLists.txt | 1 + .../experimental/detail/point_in_polygon.cuh | 63 +------ .../detail/point_in_polygon_one_to_one.cuh | 136 +++++++++++++++ .../point_in_polygon_one_to_one.cuh | 112 +++++++++++++ .../cuspatial/point_in_polygon_one_to_one.hpp | 87 ++++++++++ .../spatial/point_in_polygon_one_to_one.cu | 158 ++++++++++++++++++ cpp/src/utility/point_in_polygon.cuh | 10 +- .../cuspatial/cuspatial/_lib/CMakeLists.txt | 1 + python/cuspatial/cuspatial/_lib/contains.pyx | 42 +++++ .../cuspatial/cuspatial/_lib/cpp/contains.pxd | 16 ++ python/cuspatial/cuspatial/core/geoseries.py | 52 +++--- .../cuspatial/core/spatial/binops.py | 97 +++++++++++ .../cuspatial/tests/test_contains.py | 46 +++-- 13 files changed, 704 insertions(+), 117 deletions(-) create mode 100644 cpp/include/cuspatial/experimental/detail/point_in_polygon_one_to_one.cuh create mode 100644 cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh create mode 100644 cpp/include/cuspatial/point_in_polygon_one_to_one.hpp create mode 100644 cpp/src/spatial/point_in_polygon_one_to_one.cu create mode 100644 python/cuspatial/cuspatial/_lib/contains.pyx create mode 100644 python/cuspatial/cuspatial/_lib/cpp/contains.pxd create mode 100644 python/cuspatial/cuspatial/core/spatial/binops.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d2f50d69f..9201053b1 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -126,6 +126,7 @@ add_library(cuspatial src/spatial/polygon_bounding_box.cu src/spatial/polyline_bounding_box.cu src/spatial/point_in_polygon.cu + src/spatial/point_in_polygon_one_to_one.cu src/spatial_window/spatial_window.cu src/spatial/haversine.cu src/spatial/hausdorff.cu diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 32818f40b..0ae2db5b8 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -29,6 +29,7 @@ namespace cuspatial { namespace detail { + /** * @brief Kernel to test if a point is inside a polygon. * @@ -71,19 +72,17 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, bool y1_flag; // for each line segment, including the segment between the last and first vertex for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - y1_flag = a.y > test_point.y; - T run = b.x - a.x; - T rise = b.y - a.y; - - // colinerity test + Cart2d const a = poly_points_first[point_idx]; + T run = b.x - a.x; + T rise = b.y - a.y; T rise_to_point = test_point.y - a.y; - T run_to_point = test_point.x - a.x; - is_colinear = is_colinear || (run * rise_to_point - run_to_point * rise) * - (run * rise_to_point - run_to_point * rise) == - 0; + + // colinearity test + T run_to_point = test_point.x - a.x; + is_colinear = (run * rise_to_point - run_to_point * rise) == 0; if (is_colinear) { break; } + y1_flag = a.y > test_point.y; if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x @@ -103,50 +102,6 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, return point_is_within; } -/** - * @brief Kernel to test if a point colinear with a polygon - */ -template ::difference_type, - class Cart2dItDiffType = typename std::iterator_traits::difference_type> -__device__ inline bool is_point_colinear_with_polygon(Cart2d const& test_point, - OffsetType poly_begin, - OffsetType poly_end, - OffsetIterator ring_offsets_first, - OffsetItDiffType const& num_rings, - Cart2dIt poly_points_first, - Cart2dItDiffType const& num_poly_points) -{ - using T = iterator_vec_base_type; - - bool point_is_within = false; - bool is_colinear = false; - // for each ring - for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { - int32_t ring_idx_next = ring_idx + 1; - int32_t ring_begin = ring_offsets_first[ring_idx]; - int32_t ring_end = - (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - - Cart2d b = poly_points_first[ring_begin]; - // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - T run = b.x - a.x; - T rise = b.y - a.y; - T rise_to_point = test_point.y - a.y; - T run_to_point = test_point.x - a.x; - is_colinear = - (run * rise_to_point - run_to_point * rise) * (run * rise_to_point - run_to_point * rise) == - 0; - if (is_colinear) break; - } - } - return is_colinear; -} template +#include +#include +#include + +#include + +#include + +#include +#include + +namespace cuspatial { +namespace detail { + +template ::difference_type, + class Cart2dItBDiffType = typename std::iterator_traits::difference_type, + class OffsetItADiffType = typename std::iterator_traits::difference_type, + class OffsetItBDiffType = typename std::iterator_traits::difference_type> +__global__ void point_in_polygon_one_to_one_kernel(Cart2dItA test_points_first, + Cart2dItADiffType const num_test_points, + OffsetIteratorA poly_offsets_first, + OffsetItADiffType const num_polys, + OffsetIteratorB ring_offsets_first, + OffsetItBDiffType const num_rings, + Cart2dItB poly_points_first, + Cart2dItBDiffType const num_poly_points, + OutputIt result) +{ + using Cart2d = iterator_value_type; + using OffsetType = iterator_value_type; + auto idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_test_points) { return; } + + int32_t hit_mask = 0; + Cart2d const test_point = test_points_first[idx]; + // for the matching polygon + OffsetType poly_begin = poly_offsets_first[idx]; + OffsetType poly_end = (idx + 1 < num_polys) ? poly_offsets_first[idx + 1] : num_rings; + bool const point_is_within = is_point_in_polygon(test_point, + poly_begin, + poly_end, + ring_offsets_first, + num_rings, + poly_points_first, + num_poly_points); + hit_mask |= point_is_within << idx; + result[idx] = hit_mask; +} + +} // namespace detail + +template +OutputIt point_in_polygon_one_to_one(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream) +{ + using T = iterator_vec_base_type; + + auto const num_test_points = std::distance(test_points_first, test_points_last); + auto const num_polys = std::distance(polygon_offsets_first, polygon_offsets_last); + auto const num_rings = std::distance(poly_ring_offsets_first, poly_ring_offsets_last); + auto const num_poly_points = std::distance(polygon_points_first, polygon_points_last); + + static_assert(is_same_floating_point>(), + "Underlying type of Cart2dItA and Cart2dItB must be the same floating point type"); + static_assert( + is_same, iterator_value_type, iterator_value_type>(), + "Inputs must be cuspatial::vec_2d"); + + static_assert(cuspatial::is_integral, + iterator_value_type>(), + "OffsetIterators must point to integral type."); + + static_assert(std::is_same_v, int32_t>, + "OutputIt must point to 32 bit integer type."); + + CUSPATIAL_EXPECTS(num_rings >= num_polys, "Each polygon must have at least one ring"); + CUSPATIAL_EXPECTS(num_poly_points >= num_polys * 4, "Each ring must have at least four vertices"); + + // TODO: introduce a validation function that checks the rings of the polygon are + // actually closed. (i.e. the first and last vertices are the same) + + auto constexpr block_size = 256; + auto const num_blocks = (num_test_points + block_size - 1) / block_size; + + detail::point_in_polygon_one_to_one_kernel<<>>( + test_points_first, + num_test_points, + polygon_offsets_first, + num_polys, + poly_ring_offsets_first, + num_rings, + polygon_points_first, + num_poly_points, + output); + CUSPATIAL_CUDA_TRY(cudaGetLastError()); + + return output + num_test_points; +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh b/cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh new file mode 100644 index 000000000..bf0f43078 --- /dev/null +++ b/cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace cuspatial { + +/** + * @ingroup spatial_relationship + * + * @brief Tests whether the specified points are inside their corresponding polygon. + * + * Tests whether points are inside a corresponding polygon. Polygons are a collection of one or + * more rings. Rings are a collection of three or more vertices. + * + * Each input point will map to one `int32_t` element in the output. Each bit (except the sign bit) + * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. + * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * + * + * @tparam Cart2dItA iterator type for point array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam Cart2dItB iterator type for point array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OffsetIteratorA iterator type for offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OffsetIteratorB iterator type for offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OutputIt iterator type for output array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI], be device-accessible, mutable and + * iterate on `int32_t` type. + * + * @param test_points_first begin of range of test points + * @param test_points_last end of range of test points + * @param polygon_offsets_first begin of range of indices to the first ring in each polygon + * @param polygon_offsets_last end of range of indices to the first ring in each polygon + * @param ring_offsets_first begin of range of indices to the first point in each ring + * @param ring_offsets_last end of range of indices to the first point in each ring + * @param polygon_points_first begin of range of polygon points + * @param polygon_points_last end of range of polygon points + * @param output begin iterator to the output buffer + * @param stream The CUDA stream to use for kernel launches. + * @return iterator to one past the last element in the output buffer + * + * @note Direction of rings does not matter. + * @note This algorithm supports the ESRI shapefile format, but assumes all polygons are "clean" (as + * defined by the format), and does _not_ verify whether the input adheres to the shapefile format. + * @note The points of the rings must be explicitly closed. + * @note Overlapping rings negate each other. This behavior is not limited to a single negation, + * allowing for "islands" within the same polygon. + * @note `poly_ring_offsets` must contain only the rings that make up the polygons indexed by + * `poly_offsets`. If there are rings in `poly_ring_offsets` that are not part of the polygons in + * `poly_offsets`, results are likely to be incorrect and behavior is undefined. + * + * ``` + * poly w/two rings poly w/four rings + * +-----------+ +------------------------+ + * :███████████: :████████████████████████: + * :███████████: :██+------------------+██: + * :██████+----:------+ :██: +----+ +----+ :██: + * :██████: :██████: :██: :████: :████: :██: + * +------;----+██████: :██: :----: :----: :██: + * :███████████: :██+------------------+██: + * :███████████: :████████████████████████: + * +-----------+ +------------------------+ + * ``` + * + * @pre All point iterators must have the same `vec_2d` value type, with the same underlying + * floating-point coordinate type (e.g. `cuspatial::vec_2d`). + * @pre All offset iterators must have the same integral value type. + * @pre Output iterator must be mutable and iterate on int32_t type. + * + * @throw cuspatial::logic_error polygon has less than 1 ring. + * @throw cuspatial::logic_error polygon has less than 4 vertices. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +OutputIt point_in_polygon_one_to_one(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); + +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/point_in_polygon_one_to_one.hpp b/cpp/include/cuspatial/point_in_polygon_one_to_one.hpp new file mode 100644 index 000000000..3184bb3d4 --- /dev/null +++ b/cpp/include/cuspatial/point_in_polygon_one_to_one.hpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace cuspatial { + +/** + * @addtogroup spatial_relationship + * @{ + */ + +/** + * @brief Tests whether the specified points are inside any of the specified polygons. + * + * Tests whether points are inside at most 31 polygons. Polygons are a collection of one or more + * rings. Rings are a collection of three or more vertices. + * + * @param[in] test_points_x: x-coordinates of points to test + * @param[in] test_points_y: y-coordinates of points to test + * @param[in] poly_offsets: beginning index of the first ring in each polygon + * @param[in] poly_ring_offsets: beginning index of the first point in each ring + * @param[in] poly_points_x: x-coordinates of polygon points + * @param[in] poly_points_y: y-coordinates of polygon points + * + * @returns A column of INT32 containing one element per input point. Each bit (except the sign bit) + * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. + * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * + * @note Limit 31 polygons per call. Polygons may contain multiple rings. + * @note Direction of rings does not matter. + * @note This algorithm supports the ESRI shapefile format, but assumes all polygons are "clean" (as + * defined by the format), and does _not_ verify whether the input adheres to the shapefile format. + * @note Overlapping rings negate each other. This behavior is not limited to a single negation, + * allowing for "islands" within the same polygon. + * @note `poly_ring_offsets` must contain only the rings that make up the polygons indexed by + * `poly_offsets`. If there are rings in `poly_ring_offsets` that are not part of the polygons in + * `poly_offsets`, results are likely to be incorrect and behavior is undefined. + * + * ``` + * poly w/two rings poly w/four rings + * +-----------+ +------------------------+ + * :███████████: :████████████████████████: + * :███████████: :██+------------------+██: + * :██████+----:------+ :██: +----+ +----+ :██: + * :██████: :██████: :██: :████: :████: :██: + * +------;----+██████: :██: :----: :----: :██: + * :███████████: :██+------------------+██: + * :███████████: :████████████████████████: + * +-----------+ +------------------------+ + * ``` + */ +std::unique_ptr point_in_polygon_one_to_one( + cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +/** + * @} // end of doxygen group + */ + +} // namespace cuspatial diff --git a/cpp/src/spatial/point_in_polygon_one_to_one.cu b/cpp/src/spatial/point_in_polygon_one_to_one.cu new file mode 100644 index 000000000..b916063d1 --- /dev/null +++ b/cpp/src/spatial/point_in_polygon_one_to_one.cu @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace { + +struct point_in_polygon_one_to_one_functor { + template + static constexpr bool is_supported() + { + return std::is_floating_point::value; + } + + template ()>* = nullptr, typename... Args> + std::unique_ptr operator()(Args&&...) + { + CUSPATIAL_FAIL("Non-floating point operation is not supported"); + } + + template ()>* = nullptr> + std::unique_ptr operator()(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto size = test_points_x.size(); + auto tid = cudf::type_to_id(); + auto type = cudf::data_type{tid}; + auto results = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); + + if (results->size() == 0) { return results; } + + auto points_begin = + cuspatial::make_vec_2d_iterator(test_points_x.begin(), test_points_y.begin()); + auto polygon_offsets_begin = poly_offsets.begin(); + auto ring_offsets_begin = poly_ring_offsets.begin(); + auto polygon_points_begin = + cuspatial::make_vec_2d_iterator(poly_points_x.begin(), poly_points_y.begin()); + auto results_begin = results->mutable_view().begin(); + + cuspatial::point_in_polygon(points_begin, + points_begin + test_points_x.size(), + polygon_offsets_begin, + polygon_offsets_begin + poly_offsets.size(), + ring_offsets_begin, + ring_offsets_begin + poly_ring_offsets.size(), + polygon_points_begin, + polygon_points_begin + poly_points_x.size(), + results_begin, + stream); + + return results; + } +}; +} // anonymous namespace + +namespace cuspatial { + +namespace detail { + +std::unique_ptr point_in_polygon_one_to_one( + cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + return cudf::type_dispatcher(test_points_x.type(), + point_in_polygon_one_to_one_functor(), + test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + stream, + mr); +} + +} // namespace detail + +std::unique_ptr point_in_polygon_one_to_one( + cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS( + test_points_x.size() == test_points_y.size() and poly_points_x.size() == poly_points_y.size(), + "All points must have both x and y values"); + + CUSPATIAL_EXPECTS(test_points_x.type() == test_points_y.type() and + test_points_x.type() == poly_points_x.type() and + test_points_x.type() == poly_points_y.type(), + "All points much have the same type for both x and y"); + + CUSPATIAL_EXPECTS(not test_points_x.has_nulls() && not test_points_y.has_nulls(), + "Test points must not contain nulls"); + + CUSPATIAL_EXPECTS(not poly_points_x.has_nulls() && not poly_points_y.has_nulls(), + "Polygon points must not contain nulls"); + + CUSPATIAL_EXPECTS(poly_ring_offsets.size() >= poly_offsets.size(), + "Each polygon must have at least one ring"); + + CUSPATIAL_EXPECTS(poly_points_x.size() >= poly_offsets.size() * 4, + "Each ring must have at least four vertices"); + + return cuspatial::detail::point_in_polygon_one_to_one(test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + rmm::cuda_stream_default, + mr); +} + +} // namespace cuspatial diff --git a/cpp/src/utility/point_in_polygon.cuh b/cpp/src/utility/point_in_polygon.cuh index 8b6ed9ccf..8070a3b09 100644 --- a/cpp/src/utility/point_in_polygon.cuh +++ b/cpp/src/utility/point_in_polygon.cuh @@ -56,15 +56,7 @@ inline __device__ bool is_point_in_polygon(T const x, T rise = y1 - y0; T rise_to_point = y - y0; - // colinearity test - T d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); - T d1 = (x - x0) * (x - x1) + (y - y0) * (y - y1); - T d2 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y); - bool colinear = d1 + d2 == d; - - if (!colinear && y_in_bounds && x < (run / rise) * rise_to_point + x0) { - in_polygon = not in_polygon; - } + if (y_in_bounds && x < (run / rise) * rise_to_point + x0) { in_polygon = not in_polygon; } } } diff --git a/python/cuspatial/cuspatial/_lib/CMakeLists.txt b/python/cuspatial/cuspatial/_lib/CMakeLists.txt index bb9eb290c..1e331e22c 100644 --- a/python/cuspatial/cuspatial/_lib/CMakeLists.txt +++ b/python/cuspatial/cuspatial/_lib/CMakeLists.txt @@ -17,6 +17,7 @@ set(cython_sources interpolate.pyx nearest_points.pyx point_in_polygon.pyx + contains.pyx polygon_bounding_boxes.pyx polyline_bounding_boxes.pyx linestring_distance.pyx diff --git a/python/cuspatial/cuspatial/_lib/contains.pyx b/python/cuspatial/cuspatial/_lib/contains.pyx new file mode 100644 index 000000000..807710578 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/contains.pyx @@ -0,0 +1,42 @@ +# Copyright (c) 2020, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move + +from cudf._lib.column cimport Column, column, column_view + +from cuspatial._lib.cpp.contains cimport ( + point_in_polygon_one_to_one as cpp_point_in_polygon_one_to_one, +) + + +def contains( + Column test_points_x, + Column test_points_y, + Column poly_offsets, + Column poly_ring_offsets, + Column poly_points_x, + Column poly_points_y +): + cdef column_view c_test_points_x = test_points_x.view() + cdef column_view c_test_points_y = test_points_y.view() + cdef column_view c_poly_offsets = poly_offsets.view() + cdef column_view c_poly_ring_offsets = poly_ring_offsets.view() + cdef column_view c_poly_points_x = poly_points_x.view() + cdef column_view c_poly_points_y = poly_points_y.view() + + cdef unique_ptr[column] result + + with nogil: + result = move( + cpp_point_in_polygon_one_to_one( + c_test_points_x, + c_test_points_y, + c_poly_offsets, + c_poly_ring_offsets, + c_poly_points_x, + c_poly_points_y + ) + ) + + return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/_lib/cpp/contains.pxd b/python/cuspatial/cuspatial/_lib/cpp/contains.pxd new file mode 100644 index 000000000..66d487965 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/cpp/contains.pxd @@ -0,0 +1,16 @@ +# Copyright (c) 2020, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.column cimport column, column_view + + +cdef extern from "cuspatial/point_in_polygon_one_to_one.hpp" namespace "cuspatial" nogil: + cdef unique_ptr[column] point_in_polygon_one_to_one( + const column_view & test_points_x, + const column_view & test_points_y, + const column_view & poly_offsets, + const column_view & poly_ring_offsets, + const column_view & poly_points_x, + const column_view & poly_points_y + ) except + diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 4b8c295ca..2f907f515 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -24,7 +24,9 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.spatial.join import point_in_polygon +from cuspatial.core.spatial.binops import contains + +# from cuspatial.core.spatial.join import point_in_polygon from cuspatial.utils.column_utils import contains_only_polygons T = TypeVar("T", bound="GeoSeries") @@ -51,9 +53,7 @@ class GeoSeries(cudf.Series): def __init__( self, - data: Optional[ - Union[gpd.GeoSeries, Tuple, T, pd.Series, GeoColumn, list] - ], + data: Optional[Union[gpd.GeoSeries, Tuple, T, pd.Series, GeoColumn, list]], index: Union[cudf.Index, pd.Index] = None, dtype=None, name=None, @@ -84,14 +84,10 @@ def __init__( GeoMeta( { "input_types": cp.repeat( - cp.array( - [Feature_Enum.POLYGON.value], dtype="int8" - ), + cp.array([Feature_Enum.POLYGON.value], dtype="int8"), len(data[0]), ), - "union_offsets": cp.arange( - len(data[0]), dtype="int32" - ), + "union_offsets": cp.arange(len(data[0]), dtype="int32"), } ), from_read_polygon_shapefile=True, @@ -107,9 +103,7 @@ def __init__( index = data.index if index is None: index = cudf.RangeIndex(0, len(column)) - super().__init__( - column, index, dtype=dtype, name=name, nan_as_null=nan_as_null - ) + super().__init__(column, index, dtype=dtype, name=name, nan_as_null=nan_as_null) @property def type(self): @@ -209,18 +203,14 @@ def lines(self): """ Access the `LineArray` of the underlying `GeoArrowBuffers`. """ - return self.LineStringGeoColumnAccessor( - self._column.lines, self._column._meta - ) + return self.LineStringGeoColumnAccessor(self._column.lines, self._column._meta) @property def polygons(self): """ Access the `PolygonArray` of the underlying `GeoArrowBuffers`. """ - return self.PolygonGeoColumnAccessor( - self._column.polygons, self._column._meta - ) + return self.PolygonGeoColumnAccessor(self._column.polygons, self._column._meta) def __repr__(self): # TODO: Implement Iloc with slices so that we can use `Series.__repr__` @@ -366,9 +356,7 @@ def _linestring_to_shapely(self, geom): else: linestrings = [] for linestring in geom: - linestrings.append( - LineString([tuple(child) for child in linestring]) - ) + linestrings.append(LineString([tuple(child) for child in linestring])) return MultiLineString(linestrings) def _polygon_to_shapely(self, geom): @@ -402,8 +390,7 @@ def to_shapely(self): # Get the shapely serialization methods we'll use here. shapely_fns = [ - self._arrow_to_shapely[Feature_Enum(x)] - for x in result_types.to_numpy() + self._arrow_to_shapely[Feature_Enum(x)] for x in result_types.to_numpy() ] union = self.to_arrow() @@ -413,9 +400,7 @@ def to_shapely(self): for (result_index, shapely_serialization_fn) in zip( range(0, len(self)), shapely_fns ): - results.append( - shapely_serialization_fn(union[result_index].as_py()) - ) + results.append(shapely_serialization_fn(union[result_index].as_py())) # Finally, a slice determines that we return a list, otherwise # an object. @@ -529,15 +514,24 @@ def contains(self, other, align=True): # linestring in polygon # polygon in polygon + if len(self) != len(other) and len(other) != 1: + raise ValueError( + """.contains method is either one to one or many to + one. Number of polygons must equal number of rhs rows, or + rhs must have length 1.""" + ) # call pip on the three subtypes on the right: - point_result = point_in_polygon( + point_result = contains( other.points.x, other.points.y, - self.polygons.ring_offset[:-1], self.polygons.part_offset[:-1], + self.polygons.ring_offset[:-1], self.polygons.x, self.polygons.y, ) + # flatten our "all to all" into its diagonal + + # Apply diag mask to point_result data return point_result """ # Apply binpreds rules on results: diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/spatial/binops.py new file mode 100644 index 000000000..ed8ce5470 --- /dev/null +++ b/python/cuspatial/cuspatial/core/spatial/binops.py @@ -0,0 +1,97 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. + +import cupy as cp + +from cudf import Series +from cudf.core.column import as_column + +from cuspatial._lib.contains import contains as cpp_contains +from cuspatial.utils import gis_utils +from cuspatial.utils.column_utils import normalize_point_columns + + +def contains( + test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, +): + """Compute from a set of points and a set of polygons which points fall + within each polygon. Note that `polygons_(x,y)` must be specified as + closed polygons: the first and last coordinate of each polygon must be + the same. + + Parameters + ---------- + test_points_x + x-coordinate of test points + test_points_y + y-coordinate of test points + poly_offsets + beginning index of the first ring in each polygon + poly_ring_offsets + beginning index of the first point in each ring + poly_points_x + x closed-coordinate of polygon points + poly_points_y + y closed-coordinate of polygon points + + Examples + -------- + + Test whether 3 points fall within either of two polygons + + # TODO: Examples + + note + input Series x and y will not be index aligned, but computed as + sequential arrays. + + note + poly_ring_offsets must contain only the rings that make up the polygons + indexed by poly_offsets. If there are rings in poly_ring_offsets that + are not part of the polygons in poly_offsets, results are likely to be + incorrect and behavior is undefined. + + Returns + ------- + result : cudf.DataFrame + A DataFrame of boolean values indicating whether each point falls + within each polygon. + """ + + if len(poly_offsets) == 0: + return Series() + + ( + test_points_x, + test_points_y, + poly_points_x, + poly_points_y, + ) = normalize_point_columns( + as_column(test_points_x), + as_column(test_points_y), + as_column(poly_points_x), + as_column(poly_points_y), + ) + + contains_bitmap = cpp_contains( + test_points_x, + test_points_y, + as_column(poly_offsets, dtype="int32"), + as_column(poly_ring_offsets, dtype="int32"), + poly_points_x, + poly_points_y, + ) + + # TODO: Should be able to make these changes at the C++ level instead + # of here. + to_bool = gis_utils.pip_bitmap_column_to_binary_array( + polygon_bitmap_column=contains_bitmap, width=len(poly_offsets) + ) + flattened = (to_bool[::-1] * cp.identity(len(poly_offsets))).diagonal() + result = Series(flattened, dtype="bool") + breakpoint() + return result diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 737953b9a..afb2ce89f 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -2,29 +2,33 @@ import pandas as pd from shapely.geometry import Point, Polygon -import cudf - import cuspatial def test_point_shared_with_polygon(): - point = Point([0, 0]) - polygon = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) - point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) - polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) - result = cuspatial.point_in_polygon( - point_series.points.x, - point_series.points.y, - polygon_series.polygons.ring_offset[:-1], - polygon_series.polygons.part_offset[:-1], - polygon_series.polygons.x, - polygon_series.polygons.y, + point_series = cuspatial.from_geopandas( + gpd.GeoSeries( + [ + Point([0.25, 0.5]), + Point([1, 1]), + Point([0.5, 0.25]), + ] + ) + ) + polygon_series = cuspatial.from_geopandas( + gpd.GeoSeries( + [ + Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + ] + ) ) - cudf.testing.assert_frame_equal(result, cudf.DataFrame({0: False})) gpdpoint = point_series.to_pandas() gpdpolygon = polygon_series.to_pandas() pd.testing.assert_series_equal( - gpdpolygon.contains(gpdpoint), pd.Series([False]) + gpdpolygon.contains(gpdpoint), + polygon_series.contains(point_series).to_pandas(), ) @@ -33,17 +37,9 @@ def test_point_collinear_with_polygon(): polygon = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) - result = cuspatial.point_in_polygon( - point_series.points.x, - point_series.points.y, - polygon_series.polygons.ring_offset[:-1], - polygon_series.polygons.part_offset[:-1], - polygon_series.polygons.x, - polygon_series.polygons.y, - ) - cudf.testing.assert_frame_equal(result, cudf.DataFrame({0: False})) gpdpoint = point_series.to_pandas() gpdpolygon = polygon_series.to_pandas() pd.testing.assert_series_equal( - gpdpolygon.contains(gpdpoint), pd.Series([False]) + gpdpolygon.contains(gpdpoint), + polygon_series.contains(point_series).to_pandas(), ) From a9c180609fc89ba3fcc576788f884898968948de Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Oct 2022 13:24:43 -0500 Subject: [PATCH 09/90] Wresling with pre-commit. --- python/cuspatial/cuspatial/core/geoseries.py | 37 ++++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 2f907f515..1a338c542 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -25,8 +25,6 @@ from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta from cuspatial.core.spatial.binops import contains - -# from cuspatial.core.spatial.join import point_in_polygon from cuspatial.utils.column_utils import contains_only_polygons T = TypeVar("T", bound="GeoSeries") @@ -53,7 +51,9 @@ class GeoSeries(cudf.Series): def __init__( self, - data: Optional[Union[gpd.GeoSeries, Tuple, T, pd.Series, GeoColumn, list]], + data: Optional[ + Union[gpd.GeoSeries, Tuple, T, pd.Series, GeoColumn, list] + ], index: Union[cudf.Index, pd.Index] = None, dtype=None, name=None, @@ -84,10 +84,14 @@ def __init__( GeoMeta( { "input_types": cp.repeat( - cp.array([Feature_Enum.POLYGON.value], dtype="int8"), + cp.array( + [Feature_Enum.POLYGON.value], dtype="int8" + ), len(data[0]), ), - "union_offsets": cp.arange(len(data[0]), dtype="int32"), + "union_offsets": cp.arange( + len(data[0]), dtype="int32" + ), } ), from_read_polygon_shapefile=True, @@ -103,7 +107,9 @@ def __init__( index = data.index if index is None: index = cudf.RangeIndex(0, len(column)) - super().__init__(column, index, dtype=dtype, name=name, nan_as_null=nan_as_null) + super().__init__( + column, index, dtype=dtype, name=name, nan_as_null=nan_as_null + ) @property def type(self): @@ -203,14 +209,18 @@ def lines(self): """ Access the `LineArray` of the underlying `GeoArrowBuffers`. """ - return self.LineStringGeoColumnAccessor(self._column.lines, self._column._meta) + return self.LineStringGeoColumnAccessor( + self._column.lines, self._column._meta + ) @property def polygons(self): """ Access the `PolygonArray` of the underlying `GeoArrowBuffers`. """ - return self.PolygonGeoColumnAccessor(self._column.polygons, self._column._meta) + return self.PolygonGeoColumnAccessor( + self._column.polygons, self._column._meta + ) def __repr__(self): # TODO: Implement Iloc with slices so that we can use `Series.__repr__` @@ -356,7 +366,9 @@ def _linestring_to_shapely(self, geom): else: linestrings = [] for linestring in geom: - linestrings.append(LineString([tuple(child) for child in linestring])) + linestrings.append( + LineString([tuple(child) for child in linestring]) + ) return MultiLineString(linestrings) def _polygon_to_shapely(self, geom): @@ -390,7 +402,8 @@ def to_shapely(self): # Get the shapely serialization methods we'll use here. shapely_fns = [ - self._arrow_to_shapely[Feature_Enum(x)] for x in result_types.to_numpy() + self._arrow_to_shapely[Feature_Enum(x)] + for x in result_types.to_numpy() ] union = self.to_arrow() @@ -400,7 +413,9 @@ def to_shapely(self): for (result_index, shapely_serialization_fn) in zip( range(0, len(self)), shapely_fns ): - results.append(shapely_serialization_fn(union[result_index].as_py())) + results.append( + shapely_serialization_fn(union[result_index].as_py()) + ) # Finally, a slice determines that we return a list, otherwise # an object. From 1b4f8501b7d959916f2a0652ff9cfe37b44123ce Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Oct 2022 16:01:34 -0500 Subject: [PATCH 10/90] Rename to pairwise. --- cpp/CMakeLists.txt | 2 +- ..._one.cuh => pairwise_point_in_polygon.cuh} | 40 +++++++------- ..._one.cuh => pairwise_point_in_polygon.cuh} | 20 +++---- ..._one.hpp => pairwise_point_in_polygon.hpp} | 2 +- ...to_one.cu => pairwise_point_in_polygon.cu} | 54 +++++++++---------- python/cuspatial/cuspatial/_lib/contains.pyx | 4 +- .../cuspatial/cuspatial/_lib/cpp/contains.pxd | 5 +- 7 files changed, 63 insertions(+), 64 deletions(-) rename cpp/include/cuspatial/experimental/detail/{point_in_polygon_one_to_one.cuh => pairwise_point_in_polygon.cuh} (74%) rename cpp/include/cuspatial/experimental/{point_in_polygon_one_to_one.cuh => pairwise_point_in_polygon.cuh} (88%) rename cpp/include/cuspatial/{point_in_polygon_one_to_one.hpp => pairwise_point_in_polygon.hpp} (98%) rename cpp/src/spatial/{point_in_polygon_one_to_one.cu => pairwise_point_in_polygon.cu} (70%) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 9201053b1..34c6c6fd3 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -126,7 +126,7 @@ add_library(cuspatial src/spatial/polygon_bounding_box.cu src/spatial/polyline_bounding_box.cu src/spatial/point_in_polygon.cu - src/spatial/point_in_polygon_one_to_one.cu + src/spatial/pairwise_point_in_polygon.cu src/spatial_window/spatial_window.cu src/spatial/haversine.cu src/spatial/hausdorff.cu diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon_one_to_one.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh similarity index 74% rename from cpp/include/cuspatial/experimental/detail/point_in_polygon_one_to_one.cuh rename to cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index a7fb03381..56fb38fe1 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon_one_to_one.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -40,15 +40,15 @@ template ::difference_type, class OffsetItADiffType = typename std::iterator_traits::difference_type, class OffsetItBDiffType = typename std::iterator_traits::difference_type> -__global__ void point_in_polygon_one_to_one_kernel(Cart2dItA test_points_first, - Cart2dItADiffType const num_test_points, - OffsetIteratorA poly_offsets_first, - OffsetItADiffType const num_polys, - OffsetIteratorB ring_offsets_first, - OffsetItBDiffType const num_rings, - Cart2dItB poly_points_first, - Cart2dItBDiffType const num_poly_points, - OutputIt result) +__global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, + Cart2dItADiffType const num_test_points, + OffsetIteratorA poly_offsets_first, + OffsetItADiffType const num_polys, + OffsetIteratorB ring_offsets_first, + OffsetItBDiffType const num_rings, + Cart2dItB poly_points_first, + Cart2dItBDiffType const num_poly_points, + OutputIt result) { using Cart2d = iterator_value_type; using OffsetType = iterator_value_type; @@ -78,16 +78,16 @@ template -OutputIt point_in_polygon_one_to_one(Cart2dItA test_points_first, - Cart2dItA test_points_last, - OffsetIteratorA polygon_offsets_first, - OffsetIteratorA polygon_offsets_last, - OffsetIteratorB poly_ring_offsets_first, - OffsetIteratorB poly_ring_offsets_last, - Cart2dItB polygon_points_first, - Cart2dItB polygon_points_last, - OutputIt output, - rmm::cuda_stream_view stream) +OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream) { using T = iterator_vec_base_type; @@ -118,7 +118,7 @@ OutputIt point_in_polygon_one_to_one(Cart2dItA test_points_first, auto constexpr block_size = 256; auto const num_blocks = (num_test_points + block_size - 1) / block_size; - detail::point_in_polygon_one_to_one_kernel<<>>( + detail::pairwise_point_in_polygon_kernel<<>>( test_points_first, num_test_points, polygon_offsets_first, diff --git a/cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh similarity index 88% rename from cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh rename to cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index bf0f43078..cfdaca121 100644 --- a/cpp/include/cuspatial/experimental/point_in_polygon_one_to_one.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -96,16 +96,16 @@ template -OutputIt point_in_polygon_one_to_one(Cart2dItA test_points_first, - Cart2dItA test_points_last, - OffsetIteratorA polygon_offsets_first, - OffsetIteratorA polygon_offsets_last, - OffsetIteratorB poly_ring_offsets_first, - OffsetIteratorB poly_ring_offsets_last, - Cart2dItB polygon_points_first, - Cart2dItB polygon_points_last, - OutputIt output, - rmm::cuda_stream_view stream = rmm::cuda_stream_default); +OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); } // namespace cuspatial diff --git a/cpp/include/cuspatial/point_in_polygon_one_to_one.hpp b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp similarity index 98% rename from cpp/include/cuspatial/point_in_polygon_one_to_one.hpp rename to cpp/include/cuspatial/pairwise_point_in_polygon.hpp index 3184bb3d4..ccc628ed7 100644 --- a/cpp/include/cuspatial/point_in_polygon_one_to_one.hpp +++ b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp @@ -71,7 +71,7 @@ namespace cuspatial { * +-----------+ +------------------------+ * ``` */ -std::unique_ptr point_in_polygon_one_to_one( +std::unique_ptr pairwise_point_in_polygon( cudf::column_view const& test_points_x, cudf::column_view const& test_points_y, cudf::column_view const& poly_offsets, diff --git a/cpp/src/spatial/point_in_polygon_one_to_one.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu similarity index 70% rename from cpp/src/spatial/point_in_polygon_one_to_one.cu rename to cpp/src/spatial/pairwise_point_in_polygon.cu index b916063d1..afa7edc6a 100644 --- a/cpp/src/spatial/point_in_polygon_one_to_one.cu +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include @@ -32,7 +32,7 @@ namespace { -struct point_in_polygon_one_to_one_functor { +struct pairwise_point_in_polygon_functor { template static constexpr bool is_supported() { @@ -91,18 +91,17 @@ namespace cuspatial { namespace detail { -std::unique_ptr point_in_polygon_one_to_one( - cudf::column_view const& test_points_x, - cudf::column_view const& test_points_y, - cudf::column_view const& poly_offsets, - cudf::column_view const& poly_ring_offsets, - cudf::column_view const& poly_points_x, - cudf::column_view const& poly_points_y, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) +std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) { return cudf::type_dispatcher(test_points_x.type(), - point_in_polygon_one_to_one_functor(), + pairwise_point_in_polygon_functor(), test_points_x, test_points_y, poly_offsets, @@ -115,14 +114,13 @@ std::unique_ptr point_in_polygon_one_to_one( } // namespace detail -std::unique_ptr point_in_polygon_one_to_one( - cudf::column_view const& test_points_x, - cudf::column_view const& test_points_y, - cudf::column_view const& poly_offsets, - cudf::column_view const& poly_ring_offsets, - cudf::column_view const& poly_points_x, - cudf::column_view const& poly_points_y, - rmm::mr::device_memory_resource* mr) +std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::mr::device_memory_resource* mr) { CUSPATIAL_EXPECTS( test_points_x.size() == test_points_y.size() and poly_points_x.size() == poly_points_y.size(), @@ -145,14 +143,14 @@ std::unique_ptr point_in_polygon_one_to_one( CUSPATIAL_EXPECTS(poly_points_x.size() >= poly_offsets.size() * 4, "Each ring must have at least four vertices"); - return cuspatial::detail::point_in_polygon_one_to_one(test_points_x, - test_points_y, - poly_offsets, - poly_ring_offsets, - poly_points_x, - poly_points_y, - rmm::cuda_stream_default, - mr); + return cuspatial::detail::pairwise_point_in_polygon(test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + rmm::cuda_stream_default, + mr); } } // namespace cuspatial diff --git a/python/cuspatial/cuspatial/_lib/contains.pyx b/python/cuspatial/cuspatial/_lib/contains.pyx index 807710578..a5c768a51 100644 --- a/python/cuspatial/cuspatial/_lib/contains.pyx +++ b/python/cuspatial/cuspatial/_lib/contains.pyx @@ -6,7 +6,7 @@ from libcpp.utility cimport move from cudf._lib.column cimport Column, column, column_view from cuspatial._lib.cpp.contains cimport ( - point_in_polygon_one_to_one as cpp_point_in_polygon_one_to_one, + pairwise_point_in_polygon as cpp_pairwise_point_in_polygon, ) @@ -29,7 +29,7 @@ def contains( with nogil: result = move( - cpp_point_in_polygon_one_to_one( + cpp_pairwise_point_in_polygon( c_test_points_x, c_test_points_y, c_poly_offsets, diff --git a/python/cuspatial/cuspatial/_lib/cpp/contains.pxd b/python/cuspatial/cuspatial/_lib/cpp/contains.pxd index 66d487965..2d246453b 100644 --- a/python/cuspatial/cuspatial/_lib/cpp/contains.pxd +++ b/python/cuspatial/cuspatial/_lib/cpp/contains.pxd @@ -5,8 +5,9 @@ from libcpp.memory cimport unique_ptr from cudf._lib.column cimport column, column_view -cdef extern from "cuspatial/point_in_polygon_one_to_one.hpp" namespace "cuspatial" nogil: - cdef unique_ptr[column] point_in_polygon_one_to_one( +cdef extern from "cuspatial/pairwise_point_in_polygon.hpp" \ + namespace "cuspatial" nogil: + cdef unique_ptr[column] pairwise_point_in_polygon( const column_view & test_points_x, const column_view & test_points_y, const column_view & poly_offsets, From cba43a43ce5e89d0e30f84eb64d19ee29b16a07b Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Fri, 21 Oct 2022 17:08:15 -0500 Subject: [PATCH 11/90] Need to include the proper file. --- .../pairwise_point_in_polygon.cuh | 2 +- cpp/src/spatial/pairwise_point_in_polygon.cu | 20 +++++++++---------- .../cuspatial/core/spatial/binops.py | 12 +---------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index cfdaca121..cfe0f5d8f 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -109,4 +109,4 @@ OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, } // namespace cuspatial -#include +#include diff --git a/cpp/src/spatial/pairwise_point_in_polygon.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu index afa7edc6a..09590830e 100644 --- a/cpp/src/spatial/pairwise_point_in_polygon.cu +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -71,16 +71,16 @@ struct pairwise_point_in_polygon_functor { cuspatial::make_vec_2d_iterator(poly_points_x.begin(), poly_points_y.begin()); auto results_begin = results->mutable_view().begin(); - cuspatial::point_in_polygon(points_begin, - points_begin + test_points_x.size(), - polygon_offsets_begin, - polygon_offsets_begin + poly_offsets.size(), - ring_offsets_begin, - ring_offsets_begin + poly_ring_offsets.size(), - polygon_points_begin, - polygon_points_begin + poly_points_x.size(), - results_begin, - stream); + cuspatial::pairwise_point_in_polygon(points_begin, + points_begin + test_points_x.size(), + polygon_offsets_begin, + polygon_offsets_begin + poly_offsets.size(), + ring_offsets_begin, + ring_offsets_begin + poly_ring_offsets.size(), + polygon_points_begin, + polygon_points_begin + poly_points_x.size(), + results_begin, + stream); return results; } diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/spatial/binops.py index ed8ce5470..d005e85fd 100644 --- a/python/cuspatial/cuspatial/core/spatial/binops.py +++ b/python/cuspatial/cuspatial/core/spatial/binops.py @@ -1,12 +1,9 @@ # Copyright (c) 2022, NVIDIA CORPORATION. -import cupy as cp - from cudf import Series from cudf.core.column import as_column from cuspatial._lib.contains import contains as cpp_contains -from cuspatial.utils import gis_utils from cuspatial.utils.column_utils import normalize_point_columns @@ -86,12 +83,5 @@ def contains( poly_points_y, ) - # TODO: Should be able to make these changes at the C++ level instead - # of here. - to_bool = gis_utils.pip_bitmap_column_to_binary_array( - polygon_bitmap_column=contains_bitmap, width=len(poly_offsets) - ) - flattened = (to_bool[::-1] * cp.identity(len(poly_offsets))).diagonal() - result = Series(flattened, dtype="bool") - breakpoint() + result = Series(contains_bitmap, dtype="bool") return result From fbfd0b7f182b3cddf4a75275f425f3088a7a649c Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Sun, 23 Oct 2022 19:48:43 -0500 Subject: [PATCH 12/90] Move shared is_point_in_polygon to its own file. --- .../detail/is_point_in_polygon_kernel.cuh | 106 ++++++++++++++++++ .../detail/pairwise_point_in_polygon.cuh | 6 +- .../experimental/detail/point_in_polygon.cuh | 74 +----------- 3 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh new file mode 100644 index 000000000..36746210c --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +#include +#include + +namespace cuspatial { +namespace detail { + +/** + * @brief Kernel to test if a point is inside a polygon. + * + * Implemented based on Eric Haines's crossings-multiply algorithm: + * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ + * The improvement in addenda is also addopted to remove divisions in this kernel. + * + * TODO: the ultimate goal of refactoring this as independent function is to remove + * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't + * possible today without further work to refactor quadtree_point_in_polygon into header only + * API. + */ +template ::difference_type, + class Cart2dItDiffType = typename std::iterator_traits::difference_type> +__device__ inline bool is_point_in_polygon(Cart2d const& test_point, + OffsetType poly_begin, + OffsetType poly_end, + OffsetIterator ring_offsets_first, + OffsetItDiffType const& num_rings, + Cart2dIt poly_points_first, + Cart2dItDiffType const& num_poly_points) +{ + using T = iterator_vec_base_type; + + bool point_is_within = false; + bool is_colinear = false; + // for each ring + for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { + int32_t ring_idx_next = ring_idx + 1; + int32_t ring_begin = ring_offsets_first[ring_idx]; + int32_t ring_end = + (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; + + Cart2d b = poly_points_first[ring_begin]; + bool y0_flag = b.y > test_point.y; + bool y1_flag; + // for each line segment, including the segment between the last and first vertex + for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { + Cart2d const a = poly_points_first[point_idx]; + T run = b.x - a.x; + T rise = b.y - a.y; + T rise_to_point = test_point.y - a.y; + + // colinearity test + T run_to_point = test_point.x - a.x; + is_colinear = (run * rise_to_point - run_to_point * rise) == 0; + if (is_colinear) { break; } + + y1_flag = a.y > test_point.y; + if (y1_flag != y0_flag) { + // Transform the following inequality to avoid division + // test_point.x < (run / rise) * rise_to_point + a.x + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) + point_is_within = not point_is_within; + } + b = a; + y0_flag = y1_flag; + } + if (is_colinear) { + point_is_within = false; + break; + } + } + + return point_is_within; +} +} // namespace detail +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index 56fb38fe1..4827fdd30 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -17,7 +17,7 @@ #pragma once #include -#include +#include #include #include @@ -55,7 +55,6 @@ __global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, auto idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= num_test_points) { return; } - int32_t hit_mask = 0; Cart2d const test_point = test_points_first[idx]; // for the matching polygon OffsetType poly_begin = poly_offsets_first[idx]; @@ -67,8 +66,7 @@ __global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, num_rings, poly_points_first, num_poly_points); - hit_mask |= point_is_within << idx; - result[idx] = hit_mask; + result[idx] = point_is_within; } } // namespace detail diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 0ae2db5b8..e4e16263f 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -30,79 +31,6 @@ namespace cuspatial { namespace detail { -/** - * @brief Kernel to test if a point is inside a polygon. - * - * Implemented based on Eric Haines's crossings-multiply algorithm: - * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ - * The improvement in addenda is also addopted to remove divisions in this kernel. - * - * TODO: the ultimate goal of refactoring this as independent function is to remove - * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't - * possible today without further work to refactor quadtree_point_in_polygon into header only - * API. - */ -template ::difference_type, - class Cart2dItDiffType = typename std::iterator_traits::difference_type> -__device__ inline bool is_point_in_polygon(Cart2d const& test_point, - OffsetType poly_begin, - OffsetType poly_end, - OffsetIterator ring_offsets_first, - OffsetItDiffType const& num_rings, - Cart2dIt poly_points_first, - Cart2dItDiffType const& num_poly_points) -{ - using T = iterator_vec_base_type; - - bool point_is_within = false; - bool is_colinear = false; - // for each ring - for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { - int32_t ring_idx_next = ring_idx + 1; - int32_t ring_begin = ring_offsets_first[ring_idx]; - int32_t ring_end = - (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - - Cart2d b = poly_points_first[ring_begin]; - bool y0_flag = b.y > test_point.y; - bool y1_flag; - // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - T run = b.x - a.x; - T rise = b.y - a.y; - T rise_to_point = test_point.y - a.y; - - // colinearity test - T run_to_point = test_point.x - a.x; - is_colinear = (run * rise_to_point - run_to_point * rise) == 0; - if (is_colinear) { break; } - - y1_flag = a.y > test_point.y; - if (y1_flag != y0_flag) { - // Transform the following inequality to avoid division - // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) - point_is_within = not point_is_within; - } - b = a; - y0_flag = y1_flag; - } - if (is_colinear) { - point_is_within = false; - break; - } - } - - return point_is_within; -} - template Date: Sun, 23 Oct 2022 19:52:44 -0500 Subject: [PATCH 13/90] Remove unneeded includes. --- .../experimental/detail/is_point_in_polygon_kernel.cuh | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 36746210c..9fd418f03 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -16,16 +16,7 @@ #pragma once -#include #include -#include - -#include - -#include - -#include -#include namespace cuspatial { namespace detail { From 046e4a11b4bc1bf21a9c87d1833ac7fd65d61b39 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 10:08:08 -0500 Subject: [PATCH 14/90] Fix the tests that should no longer pass. --- cpp/tests/experimental/spatial/point_in_polygon_test.cu | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cpp/tests/experimental/spatial/point_in_polygon_test.cu b/cpp/tests/experimental/spatial/point_in_polygon_test.cu index 1a769c24a..9dac3c5b4 100644 --- a/cpp/tests/experimental/spatial/point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/point_in_polygon_test.cu @@ -169,9 +169,7 @@ TYPED_TEST(PointInPolygonTest, EdgesOfSquare) {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); - // point is included in rects on min x and y sides, but not on max x or y sides. - // this behavior is inconsistent, and not necessarily intentional. - auto expected = std::vector{0b1010}; + auto expected = std::vector{0b0000}; auto got = rmm::device_vector(test_point.size()); auto ret = point_in_polygon(test_point.begin(), @@ -205,7 +203,7 @@ TYPED_TEST(PointInPolygonTest, CornersOfSquare) // point is only included on the max x max y corner. // this behavior is inconsistent, and not necessarily intentional. - auto expected = std::vector{0b1000}; + auto expected = std::vector{0b0000}; auto got = rmm::device_vector(test_point.size()); auto ret = point_in_polygon(test_point.begin(), From c87e741c172daa98e5641692aa5b120f9a518b15 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 10:08:20 -0500 Subject: [PATCH 15/90] Fix the tests that should no longer pass. --- cpp/tests/experimental/spatial/point_in_polygon_test.cu | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/tests/experimental/spatial/point_in_polygon_test.cu b/cpp/tests/experimental/spatial/point_in_polygon_test.cu index 9dac3c5b4..cf9971b82 100644 --- a/cpp/tests/experimental/spatial/point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/point_in_polygon_test.cu @@ -201,8 +201,6 @@ TYPED_TEST(PointInPolygonTest, CornersOfSquare) {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); - // point is only included on the max x max y corner. - // this behavior is inconsistent, and not necessarily intentional. auto expected = std::vector{0b0000}; auto got = rmm::device_vector(test_point.size()); From 7f1b632c3acb4a01705bb7b14913b8495561e847 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 10:12:12 -0500 Subject: [PATCH 16/90] Now we allow open or closed polygons again. --- .../detail/is_point_in_polygon_kernel.cuh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 9fd418f03..20a46a96b 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -58,14 +58,19 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, int32_t ring_end = (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - Cart2d b = poly_points_first[ring_begin]; + Cart2d b = poly_points_first[ring_end - 1]; bool y0_flag = b.y > test_point.y; bool y1_flag; // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin + 1; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - T run = b.x - a.x; - T rise = b.y - a.y; + for (auto point_idx = ring_begin; point_idx < ring_end; point_idx++) { + Cart2d const a = poly_points_first[point_idx]; + T run = b.x - a.x; + T rise = b.y - a.y; + + // points on the line segment are the same, so intersection is impossible. + // This is possible because we allow closed or unclosed polygons. + if (run == 0.0 && rise == 0.0) continue; + T rise_to_point = test_point.y - a.y; // colinearity test From cd205b2168e71a99b981ecf89bed6489a2ced01b Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 11:42:34 -0500 Subject: [PATCH 17/90] Write tests for pairwise point in polygon. --- .../detail/is_point_in_polygon_kernel.cuh | 2 +- .../detail/pairwise_point_in_polygon.cuh | 3 + cpp/src/spatial/pairwise_point_in_polygon.cu | 5 + cpp/tests/CMakeLists.txt | 6 + .../spatial/pairwise_point_in_polygon_test.cu | 382 ++++++++++++++++++ .../pairwise_point_in_polygon_test.cpp | 224 ++++++++++ 6 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu create mode 100644 cpp/tests/spatial/pairwise_point_in_polygon_test.cpp diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 20a46a96b..4f7e31bce 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -67,7 +67,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T run = b.x - a.x; T rise = b.y - a.y; - // points on the line segment are the same, so intersection is impossible. + // Points on the line segment are the same, so intersection is impossible. // This is possible because we allow closed or unclosed polygons. if (run == 0.0 && rise == 0.0) continue; diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index 4827fdd30..47f92d2ec 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -110,6 +110,9 @@ OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, CUSPATIAL_EXPECTS(num_rings >= num_polys, "Each polygon must have at least one ring"); CUSPATIAL_EXPECTS(num_poly_points >= num_polys * 4, "Each ring must have at least four vertices"); + CUSPATIAL_EXPECTS(num_test_points == num_polys, + "Must pass in an equal number of points and polygons"); + // TODO: introduce a validation function that checks the rings of the polygon are // actually closed. (i.e. the first and last vertices are the same) diff --git a/cpp/src/spatial/pairwise_point_in_polygon.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu index 09590830e..a8fef0548 100644 --- a/cpp/src/spatial/pairwise_point_in_polygon.cu +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -143,6 +143,11 @@ std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& CUSPATIAL_EXPECTS(poly_points_x.size() >= poly_offsets.size() * 4, "Each ring must have at least four vertices"); + CUSPATIAL_EXPECTS(test_points_x.size() == poly_offsets.size(), + "Must pass in the same number of points as polygons."); + std::cout << "Test points size: " << test_points_x.size() << std::endl; + std::cout << "Poly offsets size: " << poly_offsets.size() << std::endl; + return cuspatial::detail::pairwise_point_in_polygon(test_points_x, test_points_y, poly_offsets, diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index e94138a4f..b61af45ce 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -68,6 +68,9 @@ ConfigureTest(JOIN_POINT_TO_POLYLINE_SMALL_TEST ConfigureTest(POINT_IN_POLYGON_TEST spatial/point_in_polygon_test.cpp) +ConfigureTest(PAIRWISE_POINT_IN_POLYGON_TEST + spatial/pairwise_point_in_polygon_test.cpp) + ConfigureTest(POINT_QUADTREE_TEST indexing/point_quadtree_test.cu) @@ -137,6 +140,9 @@ ConfigureTest(POINTS_IN_RANGE_TEST_EXP ConfigureTest(POINT_IN_POLYGON_TEST_EXP experimental/spatial/point_in_polygon_test.cu) +ConfigureTest(PAIRWISE_POINT_IN_POLYGON_TEST_EXP + experimental/spatial/pairwise_point_in_polygon_test.cu) + ConfigureTest(DERIVE_TRAJECTORIES_TEST_EXP experimental/trajectory/derive_trajectories_test.cu) diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu new file mode 100644 index 000000000..ef8a0a25e --- /dev/null +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +using namespace cuspatial; + +template +struct PairwisePointInPolygonTest : public ::testing::Test { + public: + rmm::device_vector> make_device_points(std::initializer_list> pts) + { + return rmm::device_vector>(pts.begin(), pts.end()); + } + + rmm::device_vector make_device_offsets(std::initializer_list pts) + { + return rmm::device_vector(pts.begin(), pts.end()); + } +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = ::testing::Types; +TYPED_TEST_CASE(PairwisePointInPolygonTest, TestTypes); + +TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) +{ + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + auto poly_point = + this->make_device_points({{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}}); + + auto got = rmm::device_vector(1); + auto expected = std::vector{false, false, false, false, true, true, true, true}; + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + EXPECT_EQ(got, std::vector({expected[i]})); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) +{ + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; + + auto poly_offsets = this->make_device_offsets({0, 1}); + auto poly_ring_offsets = this->make_device_offsets({0, 5}); + auto poly_point = this->make_device_points({{-1.0, -1.0}, + {-1.0, 1.0}, + {1.0, 1.0}, + {1.0, -1.0}, + {-1.0, -1.0}, + {0.0, 1.0}, + {1.0, 0.0}, + {0.0, -1.0}, + {-1.0, 0.0}, + {0.0, 1.0}}); + + auto got = rmm::device_vector(2); + auto expected = std::vector({0b00, 0b00, 0b00, 0b00, 0b11, 0b11, 0b11, 0b11}); + + for (size_t i = 0; i < point_list.size() / 2; i = i + 2) { + auto points = this->make_device_points( + {{point_list[i][0], point_list[i][1]}, {point_list[i + 1][0], point_list[i + 1][1]}}); + auto ret = pairwise_point_in_polygon(points.begin(), + points.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) +{ + auto point_list = + std::vector>{{0.0, 0.0}, {-0.4, 0.0}, {-0.6, 0.0}, {0.0, 0.4}, {0.0, -0.6}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0, 5}); + auto poly_point = this->make_device_points({{-1.0, -1.0}, + {1.0, -1.0}, + {1.0, 1.0}, + {-1.0, 1.0}, + {-1.0, -1.0}, + {-0.5, -0.5}, + {-0.5, 0.5}, + {0.5, 0.5}, + {0.5, -0.5}, + {-0.5, -0.5}}); + + auto got = rmm::device_vector(1); + auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, std::vector{expected[i]}); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, EdgesOfSquare) +{ + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0, 1, 2, 3}); + auto poly_ring_offsets = this->make_device_offsets({0, 5, 10, 15}); + + // 0: rect on min x side + // 1: rect on max x side + // 2: rect on min y side + // 3: rect on max y side + auto poly_point = this->make_device_points( + {{-1.0, -1.0}, {0.0, -1.0}, {0.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}, {0.0, -1.0}, {1.0, -1.0}, + {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, + {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); + + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +TYPED_TEST(PairwisePointInPolygonTest, CornersOfSquare) +{ + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0, 1, 2, 3}); + auto poly_ring_offsets = this->make_device_offsets({0, 5, 10, 15}); + + // 0: min x min y corner + // 1: min x max y corner + // 2: max x min y corner + // 3: max x max y corner + auto poly_point = this->make_device_points( + {{-1.0, -1.0}, {-1.0, 0.0}, {0.0, 0.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {-1.0, 1.0}, + {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, + {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); + + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +struct OffsetIteratorFunctor { + std::size_t __device__ operator()(std::size_t idx) { return idx * 5; } +}; + +template +struct PolyPointIteratorFunctorA { + T __device__ operator()(std::size_t idx) + { + switch (idx % 5) { + case 0: + case 1: return -1.0; + case 2: + case 3: return 1.0; + case 4: + default: return -1.0; + } + } +}; + +template +struct PolyPointIteratorFunctorB { + T __device__ operator()(std::size_t idx) + { + switch (idx % 5) { + case 0: return -1.0; + case 1: + case 2: return 1.0; + case 3: + case 4: + default: return -1.0; + } + } +}; + +TYPED_TEST(PairwisePointInPolygonTest, 32PolygonSupport) +{ + using T = TypeParam; + + auto constexpr num_polys = 32; + auto constexpr num_poly_points = num_polys * 5; + + auto test_point = this->make_device_points( + {{0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, + {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, + {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, + {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, + {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}); + auto offsets_iter = thrust::make_counting_iterator(0); + auto poly_ring_offsets_iter = + thrust::make_transform_iterator(offsets_iter, OffsetIteratorFunctor{}); + auto poly_point_xs_iter = + thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorA{}); + auto poly_point_ys_iter = + thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorB{}); + auto poly_point_iter = make_vec_2d_iterator(poly_point_xs_iter, poly_point_ys_iter); + + auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + offsets_iter, + offsets_iter + num_polys, + poly_ring_offsets_iter, + poly_ring_offsets_iter + num_polys, + poly_point_iter, + poly_point_iter + num_poly_points, + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +struct PairwisePointInPolygonErrorTest : public PairwisePointInPolygonTest { +}; + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointXYLength) +{ + using T = double; + + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + auto poly_point = this->make_device_points({{0.0, 1.0}, {1.0, 0.0}, {0.0, -1.0}}); + auto got = rmm::device_vector(test_point.size()); + + EXPECT_THROW(pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()), + cuspatial::logic_error); +} + +TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) +{ + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + // "left" edge missing + auto poly_point = this->make_device_points({{-1, 1}, {1, 1}, {1, -1}, {-1, -1}}); + auto expected = std::vector{0b0, 0b1, 0b0}; + auto got = rmm::device_vector(1); + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(got.end(), ret); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) +{ + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + // "right" edge missing + auto poly_point = this->make_device_points({{1, -1}, {-1, -1}, {-1, 1}, {1, 1}}); + auto expected = std::vector{0b0, 0b1, 0b0}; + auto got = rmm::device_vector(1); + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(got.end(), ret); + } +} diff --git a/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp b/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp new file mode 100644 index 000000000..8e27e1042 --- /dev/null +++ b/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace cudf::test; + +template +using wrapper = fixed_width_column_wrapper; + +template +struct PairwisePointInPolygonTest : public BaseFixture { +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = FloatingPointTypes; +TYPED_TEST_CASE(PairwisePointInPolygonTest, TestTypes); + +constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; + +TYPED_TEST(PairwisePointInPolygonTest, Empty) +{ + using T = TypeParam; + + auto test_point_xs = wrapper({}); + auto test_point_ys = wrapper({}); + auto poly_offsets = wrapper({}); + auto poly_ring_offsets = wrapper({}); + auto poly_point_xs = wrapper({}); + auto poly_point_ys = wrapper({}); + + auto expected = wrapper({}); + + auto actual = cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys); + + expect_columns_equal(expected, actual->view(), verbosity); +} + +template +struct PairwisePointInPolygonUnsupportedTypesTest : public BaseFixture { +}; + +using UnsupportedTestTypes = RemoveIf, NumericTypes>; +TYPED_TEST_CASE(PairwisePointInPolygonUnsupportedTypesTest, UnsupportedTestTypes); + +TYPED_TEST(PairwisePointInPolygonUnsupportedTypesTest, UnsupportedPointType) +{ + using T = TypeParam; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +template +struct PairwisePointInPolygonUnsupportedChronoTypesTest : public BaseFixture { +}; + +TYPED_TEST_CASE(PairwisePointInPolygonUnsupportedChronoTypesTest, ChronoTypes); + +TYPED_TEST(PairwisePointInPolygonUnsupportedChronoTypesTest, UnsupportedPointChronoType) +{ + using T = TypeParam; + using R = typename T::rep; + + auto test_point_xs = wrapper({R{0}, R{0}}); + auto test_point_ys = wrapper({R{0}}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({R{0}, R{1}, R{0}, R{-1}}); + auto poly_point_ys = wrapper({R{1}, R{0}, R{-1}, R{0}}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +struct PairwisePointInPolygonErrorTest : public BaseFixture { +}; + +TEST_F(PairwisePointInPolygonErrorTest, MismatchTestPointXYLength) +{ + using T = double; + + auto test_point_xs = wrapper({0.0, 0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchTestPointType) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointXYLength) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointType) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPointTypes) +{ + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MorePointsThanPolygons) +{ + auto test_point_xs = wrapper({0.0, 0.0}); + auto test_point_ys = wrapper({0.0, 0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MorePolygonsThanPoints) +{ + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0, 4}); + auto poly_ring_offsets = wrapper({0, 4}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} From 121e14652f9c1440f7479ae25a248efd59190e3d Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 12:34:53 -0500 Subject: [PATCH 18/90] Create new branch with pairwise_point_in_polygon cpp changes. --- cpp/CMakeLists.txt | 1 + .../detail/is_point_in_polygon_kernel.cuh | 102 +++++ .../detail/pairwise_point_in_polygon.cuh | 137 +++++++ .../experimental/detail/point_in_polygon.cuh | 64 +-- .../pairwise_point_in_polygon.cuh | 112 +++++ .../cuspatial/pairwise_point_in_polygon.hpp | 87 ++++ cpp/src/spatial/pairwise_point_in_polygon.cu | 161 ++++++++ cpp/tests/CMakeLists.txt | 6 + .../spatial/pairwise_point_in_polygon_test.cu | 382 ++++++++++++++++++ .../spatial/point_in_polygon_test.cu | 8 +- .../pairwise_point_in_polygon_test.cpp | 224 ++++++++++ .../spatial/join/test_point_in_polygon.py | 2 +- 12 files changed, 1216 insertions(+), 70 deletions(-) create mode 100644 cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh create mode 100644 cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh create mode 100644 cpp/include/cuspatial/pairwise_point_in_polygon.hpp create mode 100644 cpp/src/spatial/pairwise_point_in_polygon.cu create mode 100644 cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu create mode 100644 cpp/tests/spatial/pairwise_point_in_polygon_test.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d2f50d69f..34c6c6fd3 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -126,6 +126,7 @@ add_library(cuspatial src/spatial/polygon_bounding_box.cu src/spatial/polyline_bounding_box.cu src/spatial/point_in_polygon.cu + src/spatial/pairwise_point_in_polygon.cu src/spatial_window/spatial_window.cu src/spatial/haversine.cu src/spatial/hausdorff.cu diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh new file mode 100644 index 000000000..4f7e31bce --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace cuspatial { +namespace detail { + +/** + * @brief Kernel to test if a point is inside a polygon. + * + * Implemented based on Eric Haines's crossings-multiply algorithm: + * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ + * The improvement in addenda is also addopted to remove divisions in this kernel. + * + * TODO: the ultimate goal of refactoring this as independent function is to remove + * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't + * possible today without further work to refactor quadtree_point_in_polygon into header only + * API. + */ +template ::difference_type, + class Cart2dItDiffType = typename std::iterator_traits::difference_type> +__device__ inline bool is_point_in_polygon(Cart2d const& test_point, + OffsetType poly_begin, + OffsetType poly_end, + OffsetIterator ring_offsets_first, + OffsetItDiffType const& num_rings, + Cart2dIt poly_points_first, + Cart2dItDiffType const& num_poly_points) +{ + using T = iterator_vec_base_type; + + bool point_is_within = false; + bool is_colinear = false; + // for each ring + for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { + int32_t ring_idx_next = ring_idx + 1; + int32_t ring_begin = ring_offsets_first[ring_idx]; + int32_t ring_end = + (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; + + Cart2d b = poly_points_first[ring_end - 1]; + bool y0_flag = b.y > test_point.y; + bool y1_flag; + // for each line segment, including the segment between the last and first vertex + for (auto point_idx = ring_begin; point_idx < ring_end; point_idx++) { + Cart2d const a = poly_points_first[point_idx]; + T run = b.x - a.x; + T rise = b.y - a.y; + + // Points on the line segment are the same, so intersection is impossible. + // This is possible because we allow closed or unclosed polygons. + if (run == 0.0 && rise == 0.0) continue; + + T rise_to_point = test_point.y - a.y; + + // colinearity test + T run_to_point = test_point.x - a.x; + is_colinear = (run * rise_to_point - run_to_point * rise) == 0; + if (is_colinear) { break; } + + y1_flag = a.y > test_point.y; + if (y1_flag != y0_flag) { + // Transform the following inequality to avoid division + // test_point.x < (run / rise) * rise_to_point + a.x + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) + point_is_within = not point_is_within; + } + b = a; + y0_flag = y1_flag; + } + if (is_colinear) { + point_is_within = false; + break; + } + } + + return point_is_within; +} +} // namespace detail +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh new file mode 100644 index 000000000..47f92d2ec --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +namespace cuspatial { +namespace detail { + +template ::difference_type, + class Cart2dItBDiffType = typename std::iterator_traits::difference_type, + class OffsetItADiffType = typename std::iterator_traits::difference_type, + class OffsetItBDiffType = typename std::iterator_traits::difference_type> +__global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, + Cart2dItADiffType const num_test_points, + OffsetIteratorA poly_offsets_first, + OffsetItADiffType const num_polys, + OffsetIteratorB ring_offsets_first, + OffsetItBDiffType const num_rings, + Cart2dItB poly_points_first, + Cart2dItBDiffType const num_poly_points, + OutputIt result) +{ + using Cart2d = iterator_value_type; + using OffsetType = iterator_value_type; + auto idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_test_points) { return; } + + Cart2d const test_point = test_points_first[idx]; + // for the matching polygon + OffsetType poly_begin = poly_offsets_first[idx]; + OffsetType poly_end = (idx + 1 < num_polys) ? poly_offsets_first[idx + 1] : num_rings; + bool const point_is_within = is_point_in_polygon(test_point, + poly_begin, + poly_end, + ring_offsets_first, + num_rings, + poly_points_first, + num_poly_points); + result[idx] = point_is_within; +} + +} // namespace detail + +template +OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream) +{ + using T = iterator_vec_base_type; + + auto const num_test_points = std::distance(test_points_first, test_points_last); + auto const num_polys = std::distance(polygon_offsets_first, polygon_offsets_last); + auto const num_rings = std::distance(poly_ring_offsets_first, poly_ring_offsets_last); + auto const num_poly_points = std::distance(polygon_points_first, polygon_points_last); + + static_assert(is_same_floating_point>(), + "Underlying type of Cart2dItA and Cart2dItB must be the same floating point type"); + static_assert( + is_same, iterator_value_type, iterator_value_type>(), + "Inputs must be cuspatial::vec_2d"); + + static_assert(cuspatial::is_integral, + iterator_value_type>(), + "OffsetIterators must point to integral type."); + + static_assert(std::is_same_v, int32_t>, + "OutputIt must point to 32 bit integer type."); + + CUSPATIAL_EXPECTS(num_rings >= num_polys, "Each polygon must have at least one ring"); + CUSPATIAL_EXPECTS(num_poly_points >= num_polys * 4, "Each ring must have at least four vertices"); + + CUSPATIAL_EXPECTS(num_test_points == num_polys, + "Must pass in an equal number of points and polygons"); + + // TODO: introduce a validation function that checks the rings of the polygon are + // actually closed. (i.e. the first and last vertices are the same) + + auto constexpr block_size = 256; + auto const num_blocks = (num_test_points + block_size - 1) / block_size; + + detail::pairwise_point_in_polygon_kernel<<>>( + test_points_first, + num_test_points, + polygon_offsets_first, + num_polys, + poly_ring_offsets_first, + num_rings, + polygon_points_first, + num_poly_points, + output); + CUSPATIAL_CUDA_TRY(cudaGetLastError()); + + return output + num_test_points; +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index e9e3412f8..e4e16263f 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -30,69 +31,6 @@ namespace cuspatial { namespace detail { -/** - * @brief Kernel to test if a point is inside a polygon. - * - * Implemented based on Eric Haines's crossings-multiply algorithm: - * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ - * The improvement in addenda is also addopted to remove divisions in this kernel. - * - * TODO: the ultimate goal of refactoring this as independent function is to remove - * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't - * possible today without further work to refactor quadtree_point_in_polygon into header only - * API. - */ -template ::difference_type, - class Cart2dItDiffType = typename std::iterator_traits::difference_type> -__device__ inline bool is_point_in_polygon(Cart2d const& test_point, - OffsetType poly_begin, - OffsetType poly_end, - OffsetIterator ring_offsets_first, - OffsetItDiffType const& num_rings, - Cart2dIt poly_points_first, - Cart2dItDiffType const& num_poly_points) -{ - using T = iterator_vec_base_type; - - bool point_is_within = false; - // for each ring - for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { - int32_t ring_idx_next = ring_idx + 1; - int32_t ring_begin = ring_offsets_first[ring_idx]; - int32_t ring_end = - (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - - Cart2d b = poly_points_first[ring_end - 1]; - bool y0_flag = b.y > test_point.y; - bool y1_flag; - // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - y1_flag = a.y > test_point.y; - if (y1_flag != y0_flag) { - T run = b.x - a.x; - T rise = b.y - a.y; - T rise_to_point = test_point.y - a.y; - - // Transform the following inequality to avoid division - // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) - point_is_within = not point_is_within; - } - b = a; - y0_flag = y1_flag; - } - } - - return point_is_within; -} - template + +namespace cuspatial { + +/** + * @ingroup spatial_relationship + * + * @brief Tests whether the specified points are inside their corresponding polygon. + * + * Tests whether points are inside a corresponding polygon. Polygons are a collection of one or + * more rings. Rings are a collection of three or more vertices. + * + * Each input point will map to one `int32_t` element in the output. Each bit (except the sign bit) + * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. + * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * + * + * @tparam Cart2dItA iterator type for point array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam Cart2dItB iterator type for point array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OffsetIteratorA iterator type for offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OffsetIteratorB iterator type for offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OutputIt iterator type for output array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI], be device-accessible, mutable and + * iterate on `int32_t` type. + * + * @param test_points_first begin of range of test points + * @param test_points_last end of range of test points + * @param polygon_offsets_first begin of range of indices to the first ring in each polygon + * @param polygon_offsets_last end of range of indices to the first ring in each polygon + * @param ring_offsets_first begin of range of indices to the first point in each ring + * @param ring_offsets_last end of range of indices to the first point in each ring + * @param polygon_points_first begin of range of polygon points + * @param polygon_points_last end of range of polygon points + * @param output begin iterator to the output buffer + * @param stream The CUDA stream to use for kernel launches. + * @return iterator to one past the last element in the output buffer + * + * @note Direction of rings does not matter. + * @note This algorithm supports the ESRI shapefile format, but assumes all polygons are "clean" (as + * defined by the format), and does _not_ verify whether the input adheres to the shapefile format. + * @note The points of the rings must be explicitly closed. + * @note Overlapping rings negate each other. This behavior is not limited to a single negation, + * allowing for "islands" within the same polygon. + * @note `poly_ring_offsets` must contain only the rings that make up the polygons indexed by + * `poly_offsets`. If there are rings in `poly_ring_offsets` that are not part of the polygons in + * `poly_offsets`, results are likely to be incorrect and behavior is undefined. + * + * ``` + * poly w/two rings poly w/four rings + * +-----------+ +------------------------+ + * :███████████: :████████████████████████: + * :███████████: :██+------------------+██: + * :██████+----:------+ :██: +----+ +----+ :██: + * :██████: :██████: :██: :████: :████: :██: + * +------;----+██████: :██: :----: :----: :██: + * :███████████: :██+------------------+██: + * :███████████: :████████████████████████: + * +-----------+ +------------------------+ + * ``` + * + * @pre All point iterators must have the same `vec_2d` value type, with the same underlying + * floating-point coordinate type (e.g. `cuspatial::vec_2d`). + * @pre All offset iterators must have the same integral value type. + * @pre Output iterator must be mutable and iterate on int32_t type. + * + * @throw cuspatial::logic_error polygon has less than 1 ring. + * @throw cuspatial::logic_error polygon has less than 4 vertices. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, + Cart2dItA test_points_last, + OffsetIteratorA polygon_offsets_first, + OffsetIteratorA polygon_offsets_last, + OffsetIteratorB poly_ring_offsets_first, + OffsetIteratorB poly_ring_offsets_last, + Cart2dItB polygon_points_first, + Cart2dItB polygon_points_last, + OutputIt output, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); + +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp new file mode 100644 index 000000000..ccc628ed7 --- /dev/null +++ b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace cuspatial { + +/** + * @addtogroup spatial_relationship + * @{ + */ + +/** + * @brief Tests whether the specified points are inside any of the specified polygons. + * + * Tests whether points are inside at most 31 polygons. Polygons are a collection of one or more + * rings. Rings are a collection of three or more vertices. + * + * @param[in] test_points_x: x-coordinates of points to test + * @param[in] test_points_y: y-coordinates of points to test + * @param[in] poly_offsets: beginning index of the first ring in each polygon + * @param[in] poly_ring_offsets: beginning index of the first point in each ring + * @param[in] poly_points_x: x-coordinates of polygon points + * @param[in] poly_points_y: y-coordinates of polygon points + * + * @returns A column of INT32 containing one element per input point. Each bit (except the sign bit) + * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. + * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * + * @note Limit 31 polygons per call. Polygons may contain multiple rings. + * @note Direction of rings does not matter. + * @note This algorithm supports the ESRI shapefile format, but assumes all polygons are "clean" (as + * defined by the format), and does _not_ verify whether the input adheres to the shapefile format. + * @note Overlapping rings negate each other. This behavior is not limited to a single negation, + * allowing for "islands" within the same polygon. + * @note `poly_ring_offsets` must contain only the rings that make up the polygons indexed by + * `poly_offsets`. If there are rings in `poly_ring_offsets` that are not part of the polygons in + * `poly_offsets`, results are likely to be incorrect and behavior is undefined. + * + * ``` + * poly w/two rings poly w/four rings + * +-----------+ +------------------------+ + * :███████████: :████████████████████████: + * :███████████: :██+------------------+██: + * :██████+----:------+ :██: +----+ +----+ :██: + * :██████: :██████: :██: :████: :████: :██: + * +------;----+██████: :██: :----: :----: :██: + * :███████████: :██+------------------+██: + * :███████████: :████████████████████████: + * +-----------+ +------------------------+ + * ``` + */ +std::unique_ptr pairwise_point_in_polygon( + cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +/** + * @} // end of doxygen group + */ + +} // namespace cuspatial diff --git a/cpp/src/spatial/pairwise_point_in_polygon.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu new file mode 100644 index 000000000..a8fef0548 --- /dev/null +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace { + +struct pairwise_point_in_polygon_functor { + template + static constexpr bool is_supported() + { + return std::is_floating_point::value; + } + + template ()>* = nullptr, typename... Args> + std::unique_ptr operator()(Args&&...) + { + CUSPATIAL_FAIL("Non-floating point operation is not supported"); + } + + template ()>* = nullptr> + std::unique_ptr operator()(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto size = test_points_x.size(); + auto tid = cudf::type_to_id(); + auto type = cudf::data_type{tid}; + auto results = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); + + if (results->size() == 0) { return results; } + + auto points_begin = + cuspatial::make_vec_2d_iterator(test_points_x.begin(), test_points_y.begin()); + auto polygon_offsets_begin = poly_offsets.begin(); + auto ring_offsets_begin = poly_ring_offsets.begin(); + auto polygon_points_begin = + cuspatial::make_vec_2d_iterator(poly_points_x.begin(), poly_points_y.begin()); + auto results_begin = results->mutable_view().begin(); + + cuspatial::pairwise_point_in_polygon(points_begin, + points_begin + test_points_x.size(), + polygon_offsets_begin, + polygon_offsets_begin + poly_offsets.size(), + ring_offsets_begin, + ring_offsets_begin + poly_ring_offsets.size(), + polygon_points_begin, + polygon_points_begin + poly_points_x.size(), + results_begin, + stream); + + return results; + } +}; +} // anonymous namespace + +namespace cuspatial { + +namespace detail { + +std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + return cudf::type_dispatcher(test_points_x.type(), + pairwise_point_in_polygon_functor(), + test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + stream, + mr); +} + +} // namespace detail + +std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& test_points_x, + cudf::column_view const& test_points_y, + cudf::column_view const& poly_offsets, + cudf::column_view const& poly_ring_offsets, + cudf::column_view const& poly_points_x, + cudf::column_view const& poly_points_y, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS( + test_points_x.size() == test_points_y.size() and poly_points_x.size() == poly_points_y.size(), + "All points must have both x and y values"); + + CUSPATIAL_EXPECTS(test_points_x.type() == test_points_y.type() and + test_points_x.type() == poly_points_x.type() and + test_points_x.type() == poly_points_y.type(), + "All points much have the same type for both x and y"); + + CUSPATIAL_EXPECTS(not test_points_x.has_nulls() && not test_points_y.has_nulls(), + "Test points must not contain nulls"); + + CUSPATIAL_EXPECTS(not poly_points_x.has_nulls() && not poly_points_y.has_nulls(), + "Polygon points must not contain nulls"); + + CUSPATIAL_EXPECTS(poly_ring_offsets.size() >= poly_offsets.size(), + "Each polygon must have at least one ring"); + + CUSPATIAL_EXPECTS(poly_points_x.size() >= poly_offsets.size() * 4, + "Each ring must have at least four vertices"); + + CUSPATIAL_EXPECTS(test_points_x.size() == poly_offsets.size(), + "Must pass in the same number of points as polygons."); + std::cout << "Test points size: " << test_points_x.size() << std::endl; + std::cout << "Poly offsets size: " << poly_offsets.size() << std::endl; + + return cuspatial::detail::pairwise_point_in_polygon(test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + rmm::cuda_stream_default, + mr); +} + +} // namespace cuspatial diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index e94138a4f..b61af45ce 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -68,6 +68,9 @@ ConfigureTest(JOIN_POINT_TO_POLYLINE_SMALL_TEST ConfigureTest(POINT_IN_POLYGON_TEST spatial/point_in_polygon_test.cpp) +ConfigureTest(PAIRWISE_POINT_IN_POLYGON_TEST + spatial/pairwise_point_in_polygon_test.cpp) + ConfigureTest(POINT_QUADTREE_TEST indexing/point_quadtree_test.cu) @@ -137,6 +140,9 @@ ConfigureTest(POINTS_IN_RANGE_TEST_EXP ConfigureTest(POINT_IN_POLYGON_TEST_EXP experimental/spatial/point_in_polygon_test.cu) +ConfigureTest(PAIRWISE_POINT_IN_POLYGON_TEST_EXP + experimental/spatial/pairwise_point_in_polygon_test.cu) + ConfigureTest(DERIVE_TRAJECTORIES_TEST_EXP experimental/trajectory/derive_trajectories_test.cu) diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu new file mode 100644 index 000000000..ef8a0a25e --- /dev/null +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +using namespace cuspatial; + +template +struct PairwisePointInPolygonTest : public ::testing::Test { + public: + rmm::device_vector> make_device_points(std::initializer_list> pts) + { + return rmm::device_vector>(pts.begin(), pts.end()); + } + + rmm::device_vector make_device_offsets(std::initializer_list pts) + { + return rmm::device_vector(pts.begin(), pts.end()); + } +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = ::testing::Types; +TYPED_TEST_CASE(PairwisePointInPolygonTest, TestTypes); + +TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) +{ + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + auto poly_point = + this->make_device_points({{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}}); + + auto got = rmm::device_vector(1); + auto expected = std::vector{false, false, false, false, true, true, true, true}; + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + EXPECT_EQ(got, std::vector({expected[i]})); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) +{ + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; + + auto poly_offsets = this->make_device_offsets({0, 1}); + auto poly_ring_offsets = this->make_device_offsets({0, 5}); + auto poly_point = this->make_device_points({{-1.0, -1.0}, + {-1.0, 1.0}, + {1.0, 1.0}, + {1.0, -1.0}, + {-1.0, -1.0}, + {0.0, 1.0}, + {1.0, 0.0}, + {0.0, -1.0}, + {-1.0, 0.0}, + {0.0, 1.0}}); + + auto got = rmm::device_vector(2); + auto expected = std::vector({0b00, 0b00, 0b00, 0b00, 0b11, 0b11, 0b11, 0b11}); + + for (size_t i = 0; i < point_list.size() / 2; i = i + 2) { + auto points = this->make_device_points( + {{point_list[i][0], point_list[i][1]}, {point_list[i + 1][0], point_list[i + 1][1]}}); + auto ret = pairwise_point_in_polygon(points.begin(), + points.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) +{ + auto point_list = + std::vector>{{0.0, 0.0}, {-0.4, 0.0}, {-0.6, 0.0}, {0.0, 0.4}, {0.0, -0.6}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0, 5}); + auto poly_point = this->make_device_points({{-1.0, -1.0}, + {1.0, -1.0}, + {1.0, 1.0}, + {-1.0, 1.0}, + {-1.0, -1.0}, + {-0.5, -0.5}, + {-0.5, 0.5}, + {0.5, 0.5}, + {0.5, -0.5}, + {-0.5, -0.5}}); + + auto got = rmm::device_vector(1); + auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, std::vector{expected[i]}); + EXPECT_EQ(ret, got.end()); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, EdgesOfSquare) +{ + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0, 1, 2, 3}); + auto poly_ring_offsets = this->make_device_offsets({0, 5, 10, 15}); + + // 0: rect on min x side + // 1: rect on max x side + // 2: rect on min y side + // 3: rect on max y side + auto poly_point = this->make_device_points( + {{-1.0, -1.0}, {0.0, -1.0}, {0.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}, {0.0, -1.0}, {1.0, -1.0}, + {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, + {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); + + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +TYPED_TEST(PairwisePointInPolygonTest, CornersOfSquare) +{ + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0, 1, 2, 3}); + auto poly_ring_offsets = this->make_device_offsets({0, 5, 10, 15}); + + // 0: min x min y corner + // 1: min x max y corner + // 2: max x min y corner + // 3: max x max y corner + auto poly_point = this->make_device_points( + {{-1.0, -1.0}, {-1.0, 0.0}, {0.0, 0.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {-1.0, 1.0}, + {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, + {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); + + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +struct OffsetIteratorFunctor { + std::size_t __device__ operator()(std::size_t idx) { return idx * 5; } +}; + +template +struct PolyPointIteratorFunctorA { + T __device__ operator()(std::size_t idx) + { + switch (idx % 5) { + case 0: + case 1: return -1.0; + case 2: + case 3: return 1.0; + case 4: + default: return -1.0; + } + } +}; + +template +struct PolyPointIteratorFunctorB { + T __device__ operator()(std::size_t idx) + { + switch (idx % 5) { + case 0: return -1.0; + case 1: + case 2: return 1.0; + case 3: + case 4: + default: return -1.0; + } + } +}; + +TYPED_TEST(PairwisePointInPolygonTest, 32PolygonSupport) +{ + using T = TypeParam; + + auto constexpr num_polys = 32; + auto constexpr num_poly_points = num_polys * 5; + + auto test_point = this->make_device_points( + {{0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, + {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, + {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, + {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}, + {0.0, 0.0}, {2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}); + auto offsets_iter = thrust::make_counting_iterator(0); + auto poly_ring_offsets_iter = + thrust::make_transform_iterator(offsets_iter, OffsetIteratorFunctor{}); + auto poly_point_xs_iter = + thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorA{}); + auto poly_point_ys_iter = + thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorB{}); + auto poly_point_iter = make_vec_2d_iterator(poly_point_xs_iter, poly_point_ys_iter); + + auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); + auto got = rmm::device_vector(test_point.size()); + + auto ret = pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + offsets_iter, + offsets_iter + num_polys, + poly_ring_offsets_iter, + poly_ring_offsets_iter + num_polys, + poly_point_iter, + poly_point_iter + num_poly_points, + got.begin()); + + EXPECT_EQ(got, expected); + EXPECT_EQ(ret, got.end()); +} + +struct PairwisePointInPolygonErrorTest : public PairwisePointInPolygonTest { +}; + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointXYLength) +{ + using T = double; + + auto test_point = this->make_device_points({{0.0, 0.0}, {0.0, 0.0}}); + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + auto poly_point = this->make_device_points({{0.0, 1.0}, {1.0, 0.0}, {0.0, -1.0}}); + auto got = rmm::device_vector(test_point.size()); + + EXPECT_THROW(pairwise_point_in_polygon(test_point.begin(), + test_point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()), + cuspatial::logic_error); +} + +TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) +{ + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + // "left" edge missing + auto poly_point = this->make_device_points({{-1, 1}, {1, 1}, {1, -1}, {-1, -1}}); + auto expected = std::vector{0b0, 0b1, 0b0}; + auto got = rmm::device_vector(1); + + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(got.end(), ret); + } +} + +TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) +{ + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto poly_offsets = this->make_device_offsets({0}); + auto poly_ring_offsets = this->make_device_offsets({0}); + // "right" edge missing + auto poly_point = this->make_device_points({{1, -1}, {-1, -1}, {-1, 1}, {1, 1}}); + auto expected = std::vector{0b0, 0b1, 0b0}; + auto got = rmm::device_vector(1); + for (size_t i = 0; i < point_list.size(); ++i) { + auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); + auto ret = pairwise_point_in_polygon(point.begin(), + point.end(), + poly_offsets.begin(), + poly_offsets.end(), + poly_ring_offsets.begin(), + poly_ring_offsets.end(), + poly_point.begin(), + poly_point.end(), + got.begin()); + + EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(got.end(), ret); + } +} diff --git a/cpp/tests/experimental/spatial/point_in_polygon_test.cu b/cpp/tests/experimental/spatial/point_in_polygon_test.cu index 1a769c24a..cf9971b82 100644 --- a/cpp/tests/experimental/spatial/point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/point_in_polygon_test.cu @@ -169,9 +169,7 @@ TYPED_TEST(PointInPolygonTest, EdgesOfSquare) {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); - // point is included in rects on min x and y sides, but not on max x or y sides. - // this behavior is inconsistent, and not necessarily intentional. - auto expected = std::vector{0b1010}; + auto expected = std::vector{0b0000}; auto got = rmm::device_vector(test_point.size()); auto ret = point_in_polygon(test_point.begin(), @@ -203,9 +201,7 @@ TYPED_TEST(PointInPolygonTest, CornersOfSquare) {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); - // point is only included on the max x max y corner. - // this behavior is inconsistent, and not necessarily intentional. - auto expected = std::vector{0b1000}; + auto expected = std::vector{0b0000}; auto got = rmm::device_vector(test_point.size()); auto ret = point_in_polygon(test_point.begin(), diff --git a/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp b/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp new file mode 100644 index 000000000..8e27e1042 --- /dev/null +++ b/cpp/tests/spatial/pairwise_point_in_polygon_test.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace cudf::test; + +template +using wrapper = fixed_width_column_wrapper; + +template +struct PairwisePointInPolygonTest : public BaseFixture { +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = FloatingPointTypes; +TYPED_TEST_CASE(PairwisePointInPolygonTest, TestTypes); + +constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; + +TYPED_TEST(PairwisePointInPolygonTest, Empty) +{ + using T = TypeParam; + + auto test_point_xs = wrapper({}); + auto test_point_ys = wrapper({}); + auto poly_offsets = wrapper({}); + auto poly_ring_offsets = wrapper({}); + auto poly_point_xs = wrapper({}); + auto poly_point_ys = wrapper({}); + + auto expected = wrapper({}); + + auto actual = cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys); + + expect_columns_equal(expected, actual->view(), verbosity); +} + +template +struct PairwisePointInPolygonUnsupportedTypesTest : public BaseFixture { +}; + +using UnsupportedTestTypes = RemoveIf, NumericTypes>; +TYPED_TEST_CASE(PairwisePointInPolygonUnsupportedTypesTest, UnsupportedTestTypes); + +TYPED_TEST(PairwisePointInPolygonUnsupportedTypesTest, UnsupportedPointType) +{ + using T = TypeParam; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +template +struct PairwisePointInPolygonUnsupportedChronoTypesTest : public BaseFixture { +}; + +TYPED_TEST_CASE(PairwisePointInPolygonUnsupportedChronoTypesTest, ChronoTypes); + +TYPED_TEST(PairwisePointInPolygonUnsupportedChronoTypesTest, UnsupportedPointChronoType) +{ + using T = TypeParam; + using R = typename T::rep; + + auto test_point_xs = wrapper({R{0}, R{0}}); + auto test_point_ys = wrapper({R{0}}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({R{0}, R{1}, R{0}, R{-1}}); + auto poly_point_ys = wrapper({R{1}, R{0}, R{-1}, R{0}}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +struct PairwisePointInPolygonErrorTest : public BaseFixture { +}; + +TEST_F(PairwisePointInPolygonErrorTest, MismatchTestPointXYLength) +{ + using T = double; + + auto test_point_xs = wrapper({0.0, 0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchTestPointType) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointXYLength) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointType) +{ + using T = double; + + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MismatchPointTypes) +{ + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MorePointsThanPolygons) +{ + auto test_point_xs = wrapper({0.0, 0.0}); + auto test_point_ys = wrapper({0.0, 0.0}); + auto poly_offsets = wrapper({0}); + auto poly_ring_offsets = wrapper({0}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} + +TEST_F(PairwisePointInPolygonErrorTest, MorePolygonsThanPoints) +{ + auto test_point_xs = wrapper({0.0}); + auto test_point_ys = wrapper({0.0}); + auto poly_offsets = wrapper({0, 4}); + auto poly_ring_offsets = wrapper({0, 4}); + auto poly_point_xs = wrapper({0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0}); + auto poly_point_ys = wrapper({1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0, 0.0}); + + EXPECT_THROW( + cuspatial::pairwise_point_in_polygon( + test_point_xs, test_point_ys, poly_offsets, poly_ring_offsets, poly_point_xs, poly_point_ys), + cuspatial::logic_error); +} diff --git a/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py b/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py index 7c7010728..c01f37e05 100644 --- a/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py +++ b/python/cuspatial/cuspatial/tests/spatial/join/test_point_in_polygon.py @@ -236,7 +236,7 @@ def test_three_points_two_features(): ) expected = cudf.DataFrame() expected[0] = [True, True, False] - expected[1] = [True, False, True] + expected[1] = [False, False, True] cudf.testing.assert_frame_equal(expected, result) From 756282d341fe51b2d53e2db46f6190b4f6ff660c Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 12:43:31 -0500 Subject: [PATCH 19/90] Clean up docs on cursory review. --- .../experimental/pairwise_point_in_polygon.cuh | 2 +- cpp/include/cuspatial/pairwise_point_in_polygon.hpp | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index cfe0f5d8f..d96ba13f7 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -25,7 +25,7 @@ namespace cuspatial { * * @brief Tests whether the specified points are inside their corresponding polygon. * - * Tests whether points are inside a corresponding polygon. Polygons are a collection of one or + * Tests whether each point is inside a corresponding polygon. Polygons are a collection of one or * more rings. Rings are a collection of three or more vertices. * * Each input point will map to one `int32_t` element in the output. Each bit (except the sign bit) diff --git a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp index ccc628ed7..07213816b 100644 --- a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp +++ b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp @@ -32,10 +32,11 @@ namespace cuspatial { */ /** - * @brief Tests whether the specified points are inside any of the specified polygons. + * @brief Tests whether the specified points are inside each of their corresponding polygons. * - * Tests whether points are inside at most 31 polygons. Polygons are a collection of one or more - * rings. Rings are a collection of three or more vertices. + * Tests that each point is or is not inside of the polygon in the corresponding index. + * Polygons are a collection of one or more * rings. Rings are a collection of three or more + * vertices. * * @param[in] test_points_x: x-coordinates of points to test * @param[in] test_points_y: y-coordinates of points to test @@ -44,12 +45,10 @@ namespace cuspatial { * @param[in] poly_points_x: x-coordinates of polygon points * @param[in] poly_points_y: y-coordinates of polygon points * - * @returns A column of INT32 containing one element per input point. Each bit (except the sign bit) - * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. - * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * @returns A column of booleans for each point/polygon pair. * - * @note Limit 31 polygons per call. Polygons may contain multiple rings. * @note Direction of rings does not matter. + * @note Supports open or closed polygon formats. * @note This algorithm supports the ESRI shapefile format, but assumes all polygons are "clean" (as * defined by the format), and does _not_ verify whether the input adheres to the shapefile format. * @note Overlapping rings negate each other. This behavior is not limited to a single negation, From f5f7777c4fcc2ac35126a8de9c2b5354a403f7a1 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 12:48:47 -0500 Subject: [PATCH 20/90] Use T as TypeParam --- .../spatial/pairwise_point_in_polygon_test.cu | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu index ef8a0a25e..d5a3d347e 100644 --- a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -49,14 +49,15 @@ TYPED_TEST_CASE(PairwisePointInPolygonTest, TestTypes); TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) { - auto point_list = std::vector>{{-2.0, 0.0}, - {2.0, 0.0}, - {0.0, -2.0}, - {0.0, 2.0}, - {-0.5, 0.0}, - {0.5, 0.0}, - {0.0, -0.5}, - {0.0, 0.5}}; + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; auto poly_offsets = this->make_device_offsets({0}); auto poly_ring_offsets = this->make_device_offsets({0}); auto poly_point = @@ -83,14 +84,15 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) { - auto point_list = std::vector>{{-2.0, 0.0}, - {2.0, 0.0}, - {0.0, -2.0}, - {0.0, 2.0}, - {-0.5, 0.0}, - {0.5, 0.0}, - {0.0, -0.5}, - {0.0, 0.5}}; + using T = TypeParam; + auto point_list = std::vector>{{-2.0, 0.0}, + {2.0, 0.0}, + {0.0, -2.0}, + {0.0, 2.0}, + {-0.5, 0.0}, + {0.5, 0.0}, + {0.0, -0.5}, + {0.0, 0.5}}; auto poly_offsets = this->make_device_offsets({0, 1}); auto poly_ring_offsets = this->make_device_offsets({0, 5}); @@ -128,8 +130,9 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) { + using T = TypeParam; auto point_list = - std::vector>{{0.0, 0.0}, {-0.4, 0.0}, {-0.6, 0.0}, {0.0, 0.4}, {0.0, -0.6}}; + std::vector>{{0.0, 0.0}, {-0.4, 0.0}, {-0.6, 0.0}, {0.0, 0.4}, {0.0, -0.6}}; auto poly_offsets = this->make_device_offsets({0}); auto poly_ring_offsets = this->make_device_offsets({0, 5}); auto poly_point = this->make_device_points({{-1.0, -1.0}, @@ -329,7 +332,7 @@ TEST_F(PairwisePointInPolygonErrorTest, MismatchPolyPointXYLength) TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) { using T = TypeParam; - auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; auto poly_offsets = this->make_device_offsets({0}); auto poly_ring_offsets = this->make_device_offsets({0}); // "left" edge missing @@ -357,7 +360,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) { using T = TypeParam; - auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; + auto point_list = std::vector>{{-2.0, 0.0}, {0.0, 0.0}, {2.0, 0.0}}; auto poly_offsets = this->make_device_offsets({0}); auto poly_ring_offsets = this->make_device_offsets({0}); // "right" edge missing From e784307f6a13584a05f6dbbd530864b6e40e251a Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 15:08:47 -0500 Subject: [PATCH 21/90] Writing tests and implementation for polygon/point contains. --- .../cuspatial/cuspatial/_lib/CMakeLists.txt | 2 +- python/cuspatial/cuspatial/_lib/contains.pyx | 42 -------------- .../cuspatial/cuspatial/_lib/cpp/contains.pxd | 17 ------ python/cuspatial/cuspatial/core/geoseries.py | 15 +---- .../cuspatial/core/spatial/binops.py | 36 ++++++++---- .../cuspatial/tests/test_gpd_contains.py | 58 ++++++++++++++----- 6 files changed, 72 insertions(+), 98 deletions(-) delete mode 100644 python/cuspatial/cuspatial/_lib/contains.pyx delete mode 100644 python/cuspatial/cuspatial/_lib/cpp/contains.pxd diff --git a/python/cuspatial/cuspatial/_lib/CMakeLists.txt b/python/cuspatial/cuspatial/_lib/CMakeLists.txt index 1e331e22c..e7a0f7fa1 100644 --- a/python/cuspatial/cuspatial/_lib/CMakeLists.txt +++ b/python/cuspatial/cuspatial/_lib/CMakeLists.txt @@ -17,7 +17,7 @@ set(cython_sources interpolate.pyx nearest_points.pyx point_in_polygon.pyx - contains.pyx + pairwise_point_in_polygon.pyx polygon_bounding_boxes.pyx polyline_bounding_boxes.pyx linestring_distance.pyx diff --git a/python/cuspatial/cuspatial/_lib/contains.pyx b/python/cuspatial/cuspatial/_lib/contains.pyx deleted file mode 100644 index a5c768a51..000000000 --- a/python/cuspatial/cuspatial/_lib/contains.pyx +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. - -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from cudf._lib.column cimport Column, column, column_view - -from cuspatial._lib.cpp.contains cimport ( - pairwise_point_in_polygon as cpp_pairwise_point_in_polygon, -) - - -def contains( - Column test_points_x, - Column test_points_y, - Column poly_offsets, - Column poly_ring_offsets, - Column poly_points_x, - Column poly_points_y -): - cdef column_view c_test_points_x = test_points_x.view() - cdef column_view c_test_points_y = test_points_y.view() - cdef column_view c_poly_offsets = poly_offsets.view() - cdef column_view c_poly_ring_offsets = poly_ring_offsets.view() - cdef column_view c_poly_points_x = poly_points_x.view() - cdef column_view c_poly_points_y = poly_points_y.view() - - cdef unique_ptr[column] result - - with nogil: - result = move( - cpp_pairwise_point_in_polygon( - c_test_points_x, - c_test_points_y, - c_poly_offsets, - c_poly_ring_offsets, - c_poly_points_x, - c_poly_points_y - ) - ) - - return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/_lib/cpp/contains.pxd b/python/cuspatial/cuspatial/_lib/cpp/contains.pxd deleted file mode 100644 index 2d246453b..000000000 --- a/python/cuspatial/cuspatial/_lib/cpp/contains.pxd +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. - -from libcpp.memory cimport unique_ptr - -from cudf._lib.column cimport column, column_view - - -cdef extern from "cuspatial/pairwise_point_in_polygon.hpp" \ - namespace "cuspatial" nogil: - cdef unique_ptr[column] pairwise_point_in_polygon( - const column_view & test_points_x, - const column_view & test_points_y, - const column_view & poly_offsets, - const column_view & poly_ring_offsets, - const column_view & poly_points_x, - const column_view & poly_points_y - ) except + diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 1a338c542..e670fc820 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -21,10 +21,10 @@ import cudf +import cuspatial.core.spatial.binops as binops import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.spatial.binops import contains from cuspatial.utils.column_utils import contains_only_polygons T = TypeVar("T", bound="GeoSeries") @@ -529,14 +529,7 @@ def contains(self, other, align=True): # linestring in polygon # polygon in polygon - if len(self) != len(other) and len(other) != 1: - raise ValueError( - """.contains method is either one to one or many to - one. Number of polygons must equal number of rhs rows, or - rhs must have length 1.""" - ) - # call pip on the three subtypes on the right: - point_result = contains( + return binops.contains( other.points.x, other.points.y, self.polygons.part_offset[:-1], @@ -544,10 +537,6 @@ def contains(self, other, align=True): self.polygons.x, self.polygons.y, ) - # flatten our "all to all" into its diagonal - - # Apply diag mask to point_result data - return point_result """ # Apply binpreds rules on results: # point in polygon = true for row diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/spatial/binops.py index d005e85fd..8b13f4cb6 100644 --- a/python/cuspatial/cuspatial/core/spatial/binops.py +++ b/python/cuspatial/cuspatial/core/spatial/binops.py @@ -3,7 +3,12 @@ from cudf import Series from cudf.core.column import as_column -from cuspatial._lib.contains import contains as cpp_contains +from cuspatial._lib.pairwise_point_in_polygon import ( + pairwise_point_in_polygon as cpp_pairwise_point_in_polygon, +) +from cuspatial._lib.point_in_polygon import ( + point_in_polygon as cpp_point_in_polygon, +) from cuspatial.utils.column_utils import normalize_point_columns @@ -61,7 +66,6 @@ def contains( if len(poly_offsets) == 0: return Series() - ( test_points_x, test_points_y, @@ -74,14 +78,24 @@ def contains( as_column(poly_points_y), ) - contains_bitmap = cpp_contains( - test_points_x, - test_points_y, - as_column(poly_offsets, dtype="int32"), - as_column(poly_ring_offsets, dtype="int32"), - poly_points_x, - poly_points_y, - ) + if len(test_points_x) == 1 or len(poly_offsets) == 1: + pip_result = cpp_point_in_polygon( + test_points_x, + test_points_y, + as_column(poly_offsets, dtype="int32"), + as_column(poly_ring_offsets, dtype="int32"), + poly_points_x, + poly_points_y, + ) + elif len(test_points_x) == len(poly_offsets): + pip_result = cpp_pairwise_point_in_polygon( + test_points_x, + test_points_y, + as_column(poly_offsets, dtype="int32"), + as_column(poly_ring_offsets, dtype="int32"), + poly_points_x, + poly_points_y, + ) - result = Series(contains_bitmap, dtype="bool") + result = Series(pip_result, dtype="bool") return result diff --git a/python/cuspatial/cuspatial/tests/test_gpd_contains.py b/python/cuspatial/cuspatial/tests/test_gpd_contains.py index f20b14947..82e02fb00 100644 --- a/python/cuspatial/cuspatial/tests/test_gpd_contains.py +++ b/python/cuspatial/cuspatial/tests/test_gpd_contains.py @@ -44,18 +44,48 @@ ], ) def test_point_in_polygon(point, polygon, expects): - point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) - polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) - result = cuspatial.point_in_polygon( - point_series.points.x, - point_series.points.y, - polygon_series.polygons.part_offset[:-1], - polygon_series.polygons.ring_offset[:-1], - polygon_series.polygons.x, - polygon_series.polygons.y, + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + result = polygon.contains(point) + assert gpdpolygon.contains(gpdpoint).values == result.values_host + assert result.values_host[0] == expects + + +def test_two_points_one_polygon(): + gpdpoint = gpd.GeoSeries([Point(0, 0), Point(0, 0)]) + gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdpoint).values + == polygon.contains(point).values_host + ).all() + + +def test_one_point_two_polygons(): + gpdpoint = gpd.GeoSeries([Point(0, 0)]) + gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + ] ) - result[0].name = None - gpdpoint = point_series.to_pandas() - gpdpolygon = polygon_series.to_pandas() - assert gpdpolygon.contains(gpdpoint).values == result[0].values_host - assert result[0].values_host[0] == expects + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdpoint).values + == polygon.contains(point).values_host + ).all() + + +def test_ten_pairs(): + gpdpoints = gpd.GeoSeries([*point_generator(10)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(10)]) + points = cuspatial.from_geopandas(gpdpoints) + polygons = cuspatial.from_geopandas(gpdpolygons) + assert ( + gpdpolygons.contains(gpdpoints).values + == polygons.contains(points).values_host + ).all() From f53acec92a3e23ecfa41a9b49d0f3139a66bcb61 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Mon, 24 Oct 2022 15:27:00 -0500 Subject: [PATCH 22/90] Create polygon and multipolygon generator. --- python/cuspatial/cuspatial/tests/conftest.py | 32 +++++++++++++++++++ .../cuspatial/tests/test_gpd_contains.py | 4 +-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 4c58b9a91..f36f47bd5 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import pytest +from shapely.affinity import rotate from shapely.geometry import ( LineString, MultiLineString, @@ -204,3 +205,34 @@ def generator(n, max_num_geometries, max_num_segments): ) return generator + + +@pytest.fixture +def polygon_generator(): + rstate = np.random.RandomState(0) + + def generator(n, distance_from_origin): + for _ in range(n): + outer = Point(distance_from_origin * 2, 0).buffer(1) + inners = [] + for i in range(rstate.randint(1, 4)): + inner = Point(distance_from_origin + i * 0.1, 0).buffer(0.01) + inners.append(inner) + together = Polygon(outer, inners) + yield rotate( + together, rstate.random() * 2 * np.pi, use_radians=True + ) + + return generator + + +@pytest.fixture +def multipolygon_generator(): + rstate = np.random.RandomState(0) + + def generator(n, max_per_multi): + for _ in range(n): + num_polygons = rstate.randint(1, max_per_multi) + yield MultiPolygon([*polygon_generator(0, num_polygons)]) + + return generator diff --git a/python/cuspatial/cuspatial/tests/test_gpd_contains.py b/python/cuspatial/cuspatial/tests/test_gpd_contains.py index 82e02fb00..6f486770f 100644 --- a/python/cuspatial/cuspatial/tests/test_gpd_contains.py +++ b/python/cuspatial/cuspatial/tests/test_gpd_contains.py @@ -80,9 +80,9 @@ def test_one_point_two_polygons(): ).all() -def test_ten_pairs(): +def test_ten_pairs(point_generator, polygon_generator): gpdpoints = gpd.GeoSeries([*point_generator(10)]) - gpdpolygons = gpd.GeoSeries([*polygon_generator(10)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) points = cuspatial.from_geopandas(gpdpoints) polygons = cuspatial.from_geopandas(gpdpolygons) assert ( From 390890d2e1d73d70ee533098b53b437823fd87cf Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Tue, 25 Oct 2022 08:16:36 -0500 Subject: [PATCH 23/90] Refactor and create exhaustive polygon contains tests. --- .../_lib/cpp/pairwise_point_in_polygon.pxd | 17 ++ .../_lib/pairwise_point_in_polygon.pyx | 42 +++++ python/cuspatial/cuspatial/core/geoseries.py | 24 ++- .../cuspatial/core/spatial/binops.py | 2 +- .../cuspatial/tests/test_contains.py | 145 +++++++++++++----- .../cuspatial/tests/test_gpd_contains.py | 91 ----------- 6 files changed, 190 insertions(+), 131 deletions(-) create mode 100644 python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd create mode 100644 python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx delete mode 100644 python/cuspatial/cuspatial/tests/test_gpd_contains.py diff --git a/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd b/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd new file mode 100644 index 000000000..2d246453b --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd @@ -0,0 +1,17 @@ +# Copyright (c) 2020, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.column cimport column, column_view + + +cdef extern from "cuspatial/pairwise_point_in_polygon.hpp" \ + namespace "cuspatial" nogil: + cdef unique_ptr[column] pairwise_point_in_polygon( + const column_view & test_points_x, + const column_view & test_points_y, + const column_view & poly_offsets, + const column_view & poly_ring_offsets, + const column_view & poly_points_x, + const column_view & poly_points_y + ) except + diff --git a/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx b/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx new file mode 100644 index 000000000..d303e1229 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx @@ -0,0 +1,42 @@ +# Copyright (c) 2020, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move + +from cudf._lib.column cimport Column, column, column_view + +from cuspatial._lib.cpp.pairwise_point_in_polygon cimport ( + pairwise_point_in_polygon as cpp_pairwise_point_in_polygon, +) + + +def pairwise_point_in_polygon( + Column test_points_x, + Column test_points_y, + Column poly_offsets, + Column poly_ring_offsets, + Column poly_points_x, + Column poly_points_y +): + cdef column_view c_test_points_x = test_points_x.view() + cdef column_view c_test_points_y = test_points_y.view() + cdef column_view c_poly_offsets = poly_offsets.view() + cdef column_view c_poly_ring_offsets = poly_ring_offsets.view() + cdef column_view c_poly_points_x = poly_points_x.view() + cdef column_view c_poly_points_y = poly_points_y.view() + + cdef unique_ptr[column] result + + with nogil: + result = move( + cpp_pairwise_point_in_polygon( + c_test_points_x, + c_test_points_y, + c_poly_offsets, + c_poly_ring_offsets, + c_poly_points_x, + c_poly_points_y + ) + ) + + return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index e670fc820..2204f5dfc 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -21,11 +21,15 @@ import cudf -import cuspatial.core.spatial.binops as binops import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.utils.column_utils import contains_only_polygons +from cuspatial.core.spatial.binops import contains +from cuspatial.utils.column_utils import ( + contains_only_linestrings, + contains_only_points, + contains_only_polygons, +) T = TypeVar("T", bound="GeoSeries") @@ -523,13 +527,22 @@ def to_arrow(self): def contains(self, other, align=True): if contains_only_polygons(self) is False: raise TypeError("left series contains non-polygons.") + # RHS conditioning: + mode = "POINTS" # point in polygon + if contains_only_points(other) is True: + # no conditioning is required + pass # mpoint in polygon # linestring in polygon + if contains_only_linestrings(other) is True: + # condition for linestrings + mode = "LINESTRINGS" # polygon in polygon - return binops.contains( + # call pip on the three subtypes on the right: + point_result = contains( other.points.x, other.points.y, self.polygons.part_offset[:-1], @@ -537,6 +550,7 @@ def contains(self, other, align=True): self.polygons.x, self.polygons.y, ) + """ # Apply binpreds rules on results: # point in polygon = true for row @@ -546,3 +560,7 @@ def contains(self, other, align=True): # linestring in polygon for all points = true # polygon in polygon for all points = true """ + if mode == "LINESTRINGS": + # process for completed linestrings + pass + return point_result diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/spatial/binops.py index 8b13f4cb6..e850bbe22 100644 --- a/python/cuspatial/cuspatial/core/spatial/binops.py +++ b/python/cuspatial/cuspatial/core/spatial/binops.py @@ -87,7 +87,7 @@ def contains( poly_points_x, poly_points_y, ) - elif len(test_points_x) == len(poly_offsets): + else: pip_result = cpp_pairwise_point_in_polygon( test_points_x, test_points_y, diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index afb2ce89f..2c148fa50 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -1,45 +1,118 @@ import geopandas as gpd -import pandas as pd +import pytest from shapely.geometry import Point, Polygon import cuspatial +clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) +clockwiseSquare = Polygon( + [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], [-0.5, -0.5]] +) -def test_point_shared_with_polygon(): - point_series = cuspatial.from_geopandas( - gpd.GeoSeries( - [ - Point([0.25, 0.5]), - Point([1, 1]), - Point([0.5, 0.25]), - ] - ) - ) - polygon_series = cuspatial.from_geopandas( - gpd.GeoSeries( - [ - Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - ] - ) - ) - gpdpoint = point_series.to_pandas() - gpdpolygon = polygon_series.to_pandas() - pd.testing.assert_series_equal( - gpdpolygon.contains(gpdpoint), - polygon_series.contains(point_series).to_pandas(), - ) + +@pytest.mark.parametrize( + "point, polygon, expects", + [ + [Point([-0.5, -0.5]), clockwiseSquare, False], + [Point([-0.5, 0.5]), clockwiseSquare, False], + [Point([0.5, 0.5]), clockwiseSquare, False], + [Point([0.5, -0.5]), clockwiseSquare, False], + # clockwise square, should be true + [Point([-0.5, 0.0]), clockwiseSquare, False], + [Point([0.0, 0.5]), clockwiseSquare, False], + [Point([0.5, 0.0]), clockwiseSquare, False], + [Point([0.0, -0.5]), clockwiseSquare, False], + # wound clockwise, should be false + [Point([0, 0]), clockwiseTriangle, False], + [Point([0.0, 1.0]), clockwiseTriangle, False], + [Point([1.0, 1.0]), clockwiseTriangle, False], + [Point([0.0, 0.5]), clockwiseTriangle, False], + [Point([0.5, 0.5]), clockwiseTriangle, False], + [Point([0.5, 1]), clockwiseTriangle, False], + # wound clockwise, should be true + [Point([0.25, 0.5]), clockwiseTriangle, True], + [Point([0.75, 0.9]), clockwiseTriangle, True], + # wound counter clockwise, should be false + [Point([0.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([1.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([1.0, 1.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([0.5, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([0.5, 0.5]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + # wound counter clockwise, should be true + [Point([0.5, 0.25]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], + [Point([0.9, 0.75]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], + ], +) +def test_point_in_polygon(point, polygon, expects): + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + result = polygon.contains(point) + assert gpdpolygon.contains(gpdpoint).values == result.values_host + assert result.values_host[0] == expects + + +def test_two_points_one_polygon(): + gpdpoint = gpd.GeoSeries([Point(0, 0), Point(0, 0)]) + gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdpoint).values + == polygon.contains(point).values_host + ).all() -def test_point_collinear_with_polygon(): - point = Point([0.5, 0.0]) - polygon = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) - point_series = cuspatial.from_geopandas(gpd.GeoSeries(point)) - polygon_series = cuspatial.from_geopandas(gpd.GeoSeries(polygon)) - gpdpoint = point_series.to_pandas() - gpdpolygon = polygon_series.to_pandas() - pd.testing.assert_series_equal( - gpdpolygon.contains(gpdpoint), - polygon_series.contains(point_series).to_pandas(), +def test_one_point_two_polygons(): + gpdpoint = gpd.GeoSeries([Point(0, 0)]) + gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + ] ) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdpoint).values + == polygon.contains(point).values_host + ).all() + + +def test_ten_pair_points(point_generator, polygon_generator): + gpdpoints = gpd.GeoSeries([*point_generator(10)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) + points = cuspatial.from_geopandas(gpdpoints) + polygons = cuspatial.from_geopandas(gpdpolygons) + assert ( + gpdpolygons.contains(gpdpoints).values + == polygons.contains(points).values_host + ).all() + + +def test_one_polygon_one_linestring(linestring_generator): + gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 3)]) + gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [0, 1], [1, 1], [0, 0]])) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdlinestring).values + == polygons.contains(linestring).values_host + ).all() + + +""" +def test_onehundred_polygons_one_linestring( + linestring_generator, + polygon_generator +): + gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 3)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(100, 0)]) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygons) + assert ( + gpdpolygons.contains(gpdlinestring).values + == polygons.contains(linestring).values_host + ).all() +""" diff --git a/python/cuspatial/cuspatial/tests/test_gpd_contains.py b/python/cuspatial/cuspatial/tests/test_gpd_contains.py deleted file mode 100644 index 6f486770f..000000000 --- a/python/cuspatial/cuspatial/tests/test_gpd_contains.py +++ /dev/null @@ -1,91 +0,0 @@ -import geopandas as gpd -import pytest -from shapely.geometry import Point, Polygon - -import cuspatial - -clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) -clockwiseSquare = Polygon( - [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], [-0.5, -0.5]] -) - - -@pytest.mark.parametrize( - "point, polygon, expects", - [ - [Point([-0.5, -0.5]), clockwiseSquare, False], - [Point([-0.5, 0.5]), clockwiseSquare, False], - [Point([0.5, 0.5]), clockwiseSquare, False], - [Point([0.5, -0.5]), clockwiseSquare, False], - # clockwise square, should be true - [Point([-0.5, 0.0]), clockwiseSquare, False], - [Point([0.0, 0.5]), clockwiseSquare, False], - [Point([0.5, 0.0]), clockwiseSquare, False], - [Point([0.0, -0.5]), clockwiseSquare, False], - # wound clockwise, should be false - [Point([0, 0]), clockwiseTriangle, False], - [Point([0.0, 1.0]), clockwiseTriangle, False], - [Point([1.0, 1.0]), clockwiseTriangle, False], - [Point([0.0, 0.5]), clockwiseTriangle, False], - [Point([0.5, 0.5]), clockwiseTriangle, False], - [Point([0.5, 1]), clockwiseTriangle, False], - # wound clockwise, should be true - [Point([0.25, 0.5]), clockwiseTriangle, True], - [Point([0.75, 0.9]), clockwiseTriangle, True], - # wound counter clockwise, should be false - [Point([0.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([1.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([1.0, 1.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([0.5, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([0.5, 0.5]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - # wound counter clockwise, should be true - [Point([0.5, 0.25]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], - [Point([0.9, 0.75]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], - ], -) -def test_point_in_polygon(point, polygon, expects): - gpdpoint = gpd.GeoSeries(point) - gpdpolygon = gpd.GeoSeries(polygon) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - result = polygon.contains(point) - assert gpdpolygon.contains(gpdpoint).values == result.values_host - assert result.values_host[0] == expects - - -def test_two_points_one_polygon(): - gpdpoint = gpd.GeoSeries([Point(0, 0), Point(0, 0)]) - gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdpoint).values - == polygon.contains(point).values_host - ).all() - - -def test_one_point_two_polygons(): - gpdpoint = gpd.GeoSeries([Point(0, 0)]) - gpdpolygon = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), - ] - ) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdpoint).values - == polygon.contains(point).values_host - ).all() - - -def test_ten_pairs(point_generator, polygon_generator): - gpdpoints = gpd.GeoSeries([*point_generator(10)]) - gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) - points = cuspatial.from_geopandas(gpdpoints) - polygons = cuspatial.from_geopandas(gpdpolygons) - assert ( - gpdpolygons.contains(gpdpoints).values - == polygons.contains(points).values_host - ).all() From a6f8f696d1303864f596af3432bef9d68ff94484 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Tue, 25 Oct 2022 08:37:49 -0500 Subject: [PATCH 24/90] Add epsilon to colinearity test. --- .../experimental/detail/is_point_in_polygon_kernel.cuh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 4f7e31bce..616c9289d 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -74,8 +74,9 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T rise_to_point = test_point.y - a.y; // colinearity test - T run_to_point = test_point.x - a.x; - is_colinear = (run * rise_to_point - run_to_point * rise) == 0; + const T EPSILON = 0.000000001; + T run_to_point = test_point.x - a.x; + is_colinear = (run * rise_to_point - run_to_point * rise) < EPSILON; if (is_colinear) { break; } y1_flag = a.y > test_point.y; From 87020fa92b34215abfa1cd358d4c62807dd88ccf Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 25 Oct 2022 08:41:04 -0500 Subject: [PATCH 25/90] Formatting. --- .../cuspatial/tests/test_contains.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 2c148fa50..bc649edab 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -4,6 +4,29 @@ import cuspatial + +@pytest.mark.parametrize( + "point, polygon, expects", + [ + # unique failure cases identified by @mharris + [ + Point([0.6, 0.06]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), False], + ], +) +def test_float_precision_limits(point, polygon, expects): + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + result = polygon.contains(point) + assert gpdpolygon.contains(gpdpoint).values == result.values_host + assert result.values_host[0] == expects + + clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) clockwiseSquare = Polygon( [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], [-0.5, -0.5]] From 9b2a69ba933939765de271fb43a437b25e8d8994 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 25 Oct 2022 10:08:32 -0500 Subject: [PATCH 26/90] Boundary cases that are inconsistent. --- .../cuspatial/cuspatial/tests/test_contains.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index bc649edab..a169f85b9 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -14,7 +14,23 @@ Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), False, ], - [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), False], + [ + Point([0.66, 0.006]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [ + Point([0.666, 0.0006]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], + [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], + [ + Point([3.333, 1.111]), + Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), + True, + ], ], ) def test_float_precision_limits(point, polygon, expects): From 3a34016d2d67f8406e4534169e1631aa7c85ccc2 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Tue, 25 Oct 2022 10:28:37 -0500 Subject: [PATCH 27/90] Add EPSILON and refactor some tests. --- .../detail/is_point_in_polygon_kernel.cuh | 20 +++++++++---- .../spatial/pairwise_point_in_polygon_test.cu | 28 +++++++++---------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 616c9289d..bf61b4f48 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -74,19 +74,29 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T rise_to_point = test_point.y - a.y; // colinearity test - const T EPSILON = 0.000000001; + const T EPSILON = 0.0000000000000000000000000000001; T run_to_point = test_point.x - a.x; - is_colinear = (run * rise_to_point - run_to_point * rise) < EPSILON; + T colinearity = (run * rise_to_point - run_to_point * rise); + is_colinear = colinearity * colinearity < EPSILON; + printf("Colinearity: %.60lf\n", colinearity * colinearity); + printf("Is colinear? : %s\n", is_colinear ? "True" : "False"); if (is_colinear) { break; } y1_flag = a.y > test_point.y; + printf("Can intersect? : %s\n", y1_flag != y0_flag ? "True" : "False"); if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + const T INTERSECT_EPSILON = 0.00000000000001; + printf("lhs - rhs: %.60lf\n", lhs - rhs); + if (lhs - INTERSECT_EPSILON < rhs != y1_flag) { + printf("lhs: %.60lf\n", lhs - INTERSECT_EPSILON); + printf("rhs: %.60lf\n", rhs); + printf("Does intersect\n"); point_is_within = not point_is_within; + } } b = a; y0_flag = y1_flag; diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu index d5a3d347e..970014edb 100644 --- a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -64,7 +64,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) this->make_device_points({{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}}); auto got = rmm::device_vector(1); - auto expected = std::vector{false, false, false, false, true, true, true, true}; + auto expected = std::vector{false, false, false, false, true, true, true, true}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -77,7 +77,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) poly_point.begin(), poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i]})); + EXPECT_EQ(got, std::vector({expected[i]})); EXPECT_EQ(ret, got.end()); } } @@ -108,7 +108,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) {0.0, 1.0}}); auto got = rmm::device_vector(2); - auto expected = std::vector({0b00, 0b00, 0b00, 0b00, 0b11, 0b11, 0b11, 0b11}); + auto expected = std::vector({false, false, false, false, true, true, true, true}); for (size_t i = 0; i < point_list.size() / 2; i = i + 2) { auto points = this->make_device_points( @@ -123,7 +123,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); + EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); EXPECT_EQ(ret, got.end()); } } @@ -147,7 +147,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) {-0.5, -0.5}}); auto got = rmm::device_vector(1); - auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; + auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -161,7 +161,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector{expected[i]}); + EXPECT_EQ(got, std::vector{expected[i]}); EXPECT_EQ(ret, got.end()); } } @@ -181,7 +181,7 @@ TYPED_TEST(PairwisePointInPolygonTest, EdgesOfSquare) {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -213,7 +213,7 @@ TYPED_TEST(PairwisePointInPolygonTest, CornersOfSquare) {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -286,8 +286,8 @@ TYPED_TEST(PairwisePointInPolygonTest, 32PolygonSupport) thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorB{}); auto poly_point_iter = make_vec_2d_iterator(poly_point_xs_iter, poly_point_ys_iter); - auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); + auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -337,7 +337,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "left" edge missing auto poly_point = this->make_device_points({{-1, 1}, {1, 1}, {1, -1}, {-1, -1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { @@ -352,7 +352,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } @@ -365,7 +365,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "right" edge missing auto poly_point = this->make_device_points({{1, -1}, {-1, -1}, {-1, 1}, {1, 1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -379,7 +379,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } From e8af4346228ab882751c25a12067ccc3ea79d4bb Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 25 Oct 2022 10:53:16 -0500 Subject: [PATCH 28/90] Remove stale comment. --- cpp/src/spatial/pairwise_point_in_polygon.cu | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/src/spatial/pairwise_point_in_polygon.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu index a8fef0548..2e5bf5e4b 100644 --- a/cpp/src/spatial/pairwise_point_in_polygon.cu +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -145,8 +145,6 @@ std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& CUSPATIAL_EXPECTS(test_points_x.size() == poly_offsets.size(), "Must pass in the same number of points as polygons."); - std::cout << "Test points size: " << test_points_x.size() << std::endl; - std::cout << "Poly offsets size: " << poly_offsets.size() << std::endl; return cuspatial::detail::pairwise_point_in_polygon(test_points_x, test_points_y, From 70437daf98eb8308d239df5963b279b38d68bfe9 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 25 Oct 2022 10:56:21 -0500 Subject: [PATCH 29/90] Correct comments --- .../experimental/pairwise_point_in_polygon.cuh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index d96ba13f7..18dd74151 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -25,12 +25,12 @@ namespace cuspatial { * * @brief Tests whether the specified points are inside their corresponding polygon. * - * Tests whether each point is inside a corresponding polygon. Polygons are a collection of one or - * more rings. Rings are a collection of three or more vertices. + * Tests whether each point is inside a corresponding polygon. Points on the edges of the + * polygon are not considered to be inside. Floating point precision limitations mean that + * some arithmetically excluded boundary points will not be excluded, and vice-versa. + * Polygons are a collection of one or more rings. Rings are a collection of three or more vertices. * - * Each input point will map to one `int32_t` element in the output. Each bit (except the sign bit) - * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. - * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. + * Each input point will map to one `int32_t` element in the output. * * * @tparam Cart2dItA iterator type for point array. Must meet From 832167f37dc7ed6c8ddfafeba85c6482f0238d00 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 26 Oct 2022 09:41:58 -0500 Subject: [PATCH 30/90] Add polygon.contains(linestring) --- python/cuspatial/cuspatial/core/geoseries.py | 30 ++++++-- .../cuspatial/core/spatial/binops.py | 6 +- python/cuspatial/cuspatial/tests/conftest.py | 2 +- .../cuspatial/tests/test_contains.py | 70 +++++++++++++------ 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 2204f5dfc..a940867cc 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -175,6 +175,14 @@ def geometry_offset(self): def part_offset(self): return cudf.Series(self._col.elements.offsets.values) + def as_points(self): + return GeoSeries(self._col.elements.elements.elements) + + def point_indices(self): + offsets = cp.array(self.part_offset) + linestring_sizes = offsets[1:] - offsets[:-1] + return cp.repeat(self._series.index, linestring_sizes) + class PolygonGeoColumnAccessor(GeoColumnAccessor): def __init__(self, list_series, meta): super().__init__(list_series, meta) @@ -533,18 +541,23 @@ def contains(self, other, align=True): # point in polygon if contains_only_points(other) is True: # no conditioning is required - pass + points = other.points # mpoint in polygon # linestring in polygon if contains_only_linestrings(other) is True: # condition for linestrings mode = "LINESTRINGS" + linestring_points = other.lines.xy + point_indices = other.lines.point_indices() + points = GeoSeries( + GeoColumn._from_points_xy(linestring_points._column) + ).points # polygon in polygon # call pip on the three subtypes on the right: point_result = contains( - other.points.x, - other.points.y, + points.x, + points.y, self.polygons.part_offset[:-1], self.polygons.ring_offset[:-1], self.polygons.x, @@ -562,5 +575,14 @@ def contains(self, other, align=True): """ if mode == "LINESTRINGS": # process for completed linestrings - pass + result = cudf.DataFrame( + {"idx": point_indices, "pip": point_result} + ) + df_result = ( + result.groupby("idx").sum() == result.groupby("idx").count() + ).sort_index() + point_result = cudf.Series( + df_result["pip"], index=cudf.RangeIndex(0, len(df_result)) + ) + point_result.name = None return point_result diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/spatial/binops.py index e850bbe22..e9f4396fc 100644 --- a/python/cuspatial/cuspatial/core/spatial/binops.py +++ b/python/cuspatial/cuspatial/core/spatial/binops.py @@ -78,8 +78,8 @@ def contains( as_column(poly_points_y), ) - if len(test_points_x) == 1 or len(poly_offsets) == 1: - pip_result = cpp_point_in_polygon( + if len(test_points_x) == len(poly_offsets): + pip_result = cpp_pairwise_point_in_polygon( test_points_x, test_points_y, as_column(poly_offsets, dtype="int32"), @@ -88,7 +88,7 @@ def contains( poly_points_y, ) else: - pip_result = cpp_pairwise_point_in_polygon( + pip_result = cpp_point_in_polygon( test_points_x, test_points_y, as_column(poly_offsets, dtype="int32"), diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index f36f47bd5..cffbeea64 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -183,7 +183,7 @@ def generator(n, max_num_geometries): @pytest.fixture def linestring_generator(point_generator): - rstate = np.random.RandomState(0) + rstate = np.random.RandomState(1) def generator(n, max_num_segments): for _ in range(n): diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index a169f85b9..18a2038aa 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -1,19 +1,27 @@ import geopandas as gpd import pytest -from shapely.geometry import Point, Polygon +from shapely.geometry import LineString, Point, Polygon import cuspatial +""" +[ + Point([0.6, 0.06]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, +], +[ + Point([3.333, 1.111]), + Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), + True, +], +""" + @pytest.mark.parametrize( "point, polygon, expects", [ # unique failure cases identified by @mharris - [ - Point([0.6, 0.06]), - Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), - False, - ], [ Point([0.66, 0.006]), Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), @@ -26,11 +34,6 @@ ], [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], - [ - Point([3.333, 1.111]), - Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), - True, - ], ], ) def test_float_precision_limits(point, polygon, expects): @@ -131,8 +134,10 @@ def test_ten_pair_points(point_generator, polygon_generator): def test_one_polygon_one_linestring(linestring_generator): - gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 3)]) - gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [0, 1], [1, 1], [0, 0]])) + gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 4)]) + gpdpolygon = gpd.GeoSeries( + Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) assert ( @@ -141,17 +146,36 @@ def test_one_polygon_one_linestring(linestring_generator): ).all() -""" -def test_onehundred_polygons_one_linestring( - linestring_generator, - polygon_generator -): - gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 3)]) - gpdpolygons = gpd.GeoSeries([*polygon_generator(100, 0)]) +def test_four_polygons_four_linestrings(linestring_generator): + gpdlinestring = gpd.GeoSeries( + [ + LineString([[0.35, 0.35], [0.35, 0.65]]), + LineString([[0.25, 0.25], [0.25, 0.75]]), + LineString([[0.15, 0.15], [0.15, 0.85]]), + LineString([[0.05, 0.05], [0.05, 0.95]]), + ] + ) + gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + ] + ) linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygons) + polygons = cuspatial.from_geopandas(gpdpolygon) assert ( - gpdpolygons.contains(gpdlinestring).values + gpdpolygon.contains(gpdlinestring).values == polygons.contains(linestring).values_host ).all() -""" + + +def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): + gpdlinestring = gpd.GeoSeries([*linestring_generator(31, 3)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(31, 0)]) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygons) + gpdresult = gpdpolygons.contains(gpdlinestring) + result = polygons.contains(linestring) + assert (gpdresult.values == result.values_host).all() From cc87c86fb4348b99aa45f470ee22faec472058a6 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Wed, 26 Oct 2022 11:42:35 -0500 Subject: [PATCH 31/90] Test points in polygons. --- python/cuspatial/cuspatial/core/geoseries.py | 31 ++++++++++++------- python/cuspatial/cuspatial/tests/conftest.py | 8 +++-- .../cuspatial/tests/test_contains.py | 26 ++++++++++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index a940867cc..ade7e84ee 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -180,8 +180,8 @@ def as_points(self): def point_indices(self): offsets = cp.array(self.part_offset) - linestring_sizes = offsets[1:] - offsets[:-1] - return cp.repeat(self._series.index, linestring_sizes) + sizes = offsets[1:] - offsets[:-1] + return cp.repeat(self._series.index, sizes) class PolygonGeoColumnAccessor(GeoColumnAccessor): def __init__(self, list_series, meta): @@ -200,6 +200,11 @@ def part_offset(self): def ring_offset(self): return cudf.Series(self._col.elements.elements.offsets.values) + def point_indices(self): + offsets = cp.array(self.ring_offset) + sizes = offsets[1:] - offsets[:-1] + return cp.repeat(self._series.index, sizes) + @property def points(self): """ @@ -544,15 +549,20 @@ def contains(self, other, align=True): points = other.points # mpoint in polygon # linestring in polygon - if contains_only_linestrings(other) is True: - # condition for linestrings - mode = "LINESTRINGS" - linestring_points = other.lines.xy - point_indices = other.lines.point_indices() + else: + if contains_only_linestrings(other) is True: + # condition for linestrings + mode = "LINESTRINGS" + xy = other.lines + elif contains_only_polygons(other) is True: + # polygon in polygon + mode = "POLYGONS" + xy = other.polygons + xy_points = xy.xy + point_indices = xy.point_indices() points = GeoSeries( - GeoColumn._from_points_xy(linestring_points._column) + GeoColumn._from_points_xy(xy_points._column) ).points - # polygon in polygon # call pip on the three subtypes on the right: point_result = contains( @@ -563,7 +573,6 @@ def contains(self, other, align=True): self.polygons.x, self.polygons.y, ) - """ # Apply binpreds rules on results: # point in polygon = true for row @@ -573,7 +582,7 @@ def contains(self, other, align=True): # linestring in polygon for all points = true # polygon in polygon for all points = true """ - if mode == "LINESTRINGS": + if mode == "LINESTRINGS" or mode == "POLYGONS": # process for completed linestrings result = cudf.DataFrame( {"idx": point_indices, "pip": point_result} diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index cffbeea64..737c041ac 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -211,12 +211,14 @@ def generator(n, max_num_geometries, max_num_segments): def polygon_generator(): rstate = np.random.RandomState(0) - def generator(n, distance_from_origin): + def generator(n, distance_from_origin, radius=1.0): for _ in range(n): - outer = Point(distance_from_origin * 2, 0).buffer(1) + outer = Point(distance_from_origin * 2, 0).buffer(radius) inners = [] for i in range(rstate.randint(1, 4)): - inner = Point(distance_from_origin + i * 0.1, 0).buffer(0.01) + inner = Point(distance_from_origin + i * 0.1, 0).buffer( + 0.01 * radius + ) inners.append(inner) together = Polygon(outer, inners) yield rotate( diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 18a2038aa..ceccc0516 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -179,3 +179,29 @@ def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): gpdresult = gpdpolygons.contains(gpdlinestring) result = polygons.contains(linestring) assert (gpdresult.values == result.values_host).all() + + +def test_one_polygon_one_polygon(polygon_generator): + gpdlhs = gpd.GeoSeries(Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])) + gpdrhs = gpd.GeoSeries([*polygon_generator(1, 0)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + assert ( + gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + ).all() + assert ( + gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host + ).all() + + +def test_max_polygons_max_polygons(polygon_generator): + gpdlhs = gpd.GeoSeries([*polygon_generator(31, 0, 10)]) + gpdrhs = gpd.GeoSeries([*polygon_generator(31, 0, 1)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + assert ( + gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + ).all() + assert ( + gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host + ).all() From 1fdd9211ad983ec22360ff474f2c59ae4e2a3332 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Oct 2022 14:33:06 -0500 Subject: [PATCH 32/90] Revert "Merge branch 'feature/pairwise_point_in_polygon' into feature/GeoSeries.contains" This reverts commit 1028860d10b115e7ed4f0860234eb73a418db2aa, reversing changes made to 9b2a69ba933939765de271fb43a437b25e8d8994. --- .../detail/is_point_in_polygon_kernel.cuh | 20 ++++--------- .../pairwise_point_in_polygon.cuh | 10 +++---- cpp/src/spatial/pairwise_point_in_polygon.cu | 2 ++ .../spatial/pairwise_point_in_polygon_test.cu | 28 +++++++++---------- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index bf61b4f48..616c9289d 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -74,29 +74,19 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T rise_to_point = test_point.y - a.y; // colinearity test - const T EPSILON = 0.0000000000000000000000000000001; + const T EPSILON = 0.000000001; T run_to_point = test_point.x - a.x; - T colinearity = (run * rise_to_point - run_to_point * rise); - is_colinear = colinearity * colinearity < EPSILON; - printf("Colinearity: %.60lf\n", colinearity * colinearity); - printf("Is colinear? : %s\n", is_colinear ? "True" : "False"); + is_colinear = (run * rise_to_point - run_to_point * rise) < EPSILON; if (is_colinear) { break; } y1_flag = a.y > test_point.y; - printf("Can intersect? : %s\n", y1_flag != y0_flag ? "True" : "False"); if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - const T INTERSECT_EPSILON = 0.00000000000001; - printf("lhs - rhs: %.60lf\n", lhs - rhs); - if (lhs - INTERSECT_EPSILON < rhs != y1_flag) { - printf("lhs: %.60lf\n", lhs - INTERSECT_EPSILON); - printf("rhs: %.60lf\n", rhs); - printf("Does intersect\n"); + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + if ((rise > 0 && lhs < rhs) || (rise < 0 && lhs > rhs)) point_is_within = not point_is_within; - } } b = a; y0_flag = y1_flag; diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index 18dd74151..d96ba13f7 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -25,12 +25,12 @@ namespace cuspatial { * * @brief Tests whether the specified points are inside their corresponding polygon. * - * Tests whether each point is inside a corresponding polygon. Points on the edges of the - * polygon are not considered to be inside. Floating point precision limitations mean that - * some arithmetically excluded boundary points will not be excluded, and vice-versa. - * Polygons are a collection of one or more rings. Rings are a collection of three or more vertices. + * Tests whether each point is inside a corresponding polygon. Polygons are a collection of one or + * more rings. Rings are a collection of three or more vertices. * - * Each input point will map to one `int32_t` element in the output. + * Each input point will map to one `int32_t` element in the output. Each bit (except the sign bit) + * represents a hit or miss for each of the input polygons in least-significant-bit order. i.e. + * `output[3] & 0b0010` indicates a hit or miss for the 3rd point against the 2nd polygon. * * * @tparam Cart2dItA iterator type for point array. Must meet diff --git a/cpp/src/spatial/pairwise_point_in_polygon.cu b/cpp/src/spatial/pairwise_point_in_polygon.cu index 2e5bf5e4b..a8fef0548 100644 --- a/cpp/src/spatial/pairwise_point_in_polygon.cu +++ b/cpp/src/spatial/pairwise_point_in_polygon.cu @@ -145,6 +145,8 @@ std::unique_ptr pairwise_point_in_polygon(cudf::column_view const& CUSPATIAL_EXPECTS(test_points_x.size() == poly_offsets.size(), "Must pass in the same number of points as polygons."); + std::cout << "Test points size: " << test_points_x.size() << std::endl; + std::cout << "Poly offsets size: " << poly_offsets.size() << std::endl; return cuspatial::detail::pairwise_point_in_polygon(test_points_x, test_points_y, diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu index 970014edb..d5a3d347e 100644 --- a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -64,7 +64,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) this->make_device_points({{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}}); auto got = rmm::device_vector(1); - auto expected = std::vector{false, false, false, false, true, true, true, true}; + auto expected = std::vector{false, false, false, false, true, true, true, true}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -77,7 +77,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) poly_point.begin(), poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i]})); + EXPECT_EQ(got, std::vector({expected[i]})); EXPECT_EQ(ret, got.end()); } } @@ -108,7 +108,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) {0.0, 1.0}}); auto got = rmm::device_vector(2); - auto expected = std::vector({false, false, false, false, true, true, true, true}); + auto expected = std::vector({0b00, 0b00, 0b00, 0b00, 0b11, 0b11, 0b11, 0b11}); for (size_t i = 0; i < point_list.size() / 2; i = i + 2) { auto points = this->make_device_points( @@ -123,7 +123,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); + EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); EXPECT_EQ(ret, got.end()); } } @@ -147,7 +147,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) {-0.5, -0.5}}); auto got = rmm::device_vector(1); - auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; + auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -161,7 +161,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector{expected[i]}); + EXPECT_EQ(got, std::vector{expected[i]}); EXPECT_EQ(ret, got.end()); } } @@ -181,7 +181,7 @@ TYPED_TEST(PairwisePointInPolygonTest, EdgesOfSquare) {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -213,7 +213,7 @@ TYPED_TEST(PairwisePointInPolygonTest, CornersOfSquare) {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -286,8 +286,8 @@ TYPED_TEST(PairwisePointInPolygonTest, 32PolygonSupport) thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorB{}); auto poly_point_iter = make_vec_2d_iterator(poly_point_xs_iter, poly_point_ys_iter); - auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); + auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -337,7 +337,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "left" edge missing auto poly_point = this->make_device_points({{-1, 1}, {1, 1}, {1, -1}, {-1, -1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { @@ -352,7 +352,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } @@ -365,7 +365,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "right" edge missing auto poly_point = this->make_device_points({{1, -1}, {-1, -1}, {-1, 1}, {1, 1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -379,7 +379,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } From 8e8bc81c7de964398592fa993e95bb1466f8f4cf Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Thu, 27 Oct 2022 14:38:45 -0500 Subject: [PATCH 33/90] Fixed --- .../experimental/detail/is_point_in_polygon_kernel.cuh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 616c9289d..3d33b2b72 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -74,9 +74,8 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T rise_to_point = test_point.y - a.y; // colinearity test - const T EPSILON = 0.000000001; - T run_to_point = test_point.x - a.x; - is_colinear = (run * rise_to_point - run_to_point * rise) < EPSILON; + T run_to_point = test_point.x - a.x; + is_colinear = run * rise_to_point == run_to_point * rise; if (is_colinear) { break; } y1_flag = a.y > test_point.y; From 0efb650cd8ca486e9e53ec34eb4944706452ad60 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Thu, 27 Oct 2022 14:54:03 -0500 Subject: [PATCH 34/90] Add the polygon with a hole with a linestring crossing it case. --- .../cuspatial/tests/test_contains.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index ceccc0516..5d6aafcec 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -133,6 +133,71 @@ def test_ten_pair_points(point_generator, polygon_generator): ).all() +@pytest.mark.xfail( + reason="We don't support intersection yet to check for crossings." +) +def test_one_polygon_with_hole_one_linestring_crossing_it( + linestring_generator, +): + gpdlinestring = gpd.GeoSeries(LineString([[0.5, 2.0], [3.5, 2.0]])) + gpdpolygon = gpd.GeoSeries( + Polygon( + ( + [0, 0], + [0, 4], + [4, 4], + [4, 0], + [0, 0], + ), + [ + ( + [1, 1], + [1, 3], + [3, 3], + [3, 1], + [1, 1], + ) + ], + ) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdlinestring).values + == polygons.contains(linestring).values_host + ).all() + + +def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): + gpdlinestring = gpd.GeoSeries(LineString([[1.5, 2.0], [2.5, 2.0]])) + gpdpolygon = gpd.GeoSeries( + Polygon( + ( + [0, 0], + [0, 4], + [4, 4], + [4, 0], + [0, 0], + ), + [ + ( + [1, 1], + [1, 3], + [3, 3], + [3, 1], + [1, 1], + ) + ], + ) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdlinestring).values + == polygons.contains(linestring).values_host + ).all() + + def test_one_polygon_one_linestring(linestring_generator): gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 4)]) gpdpolygon = gpd.GeoSeries( From a092a6e2571f50f0ca779b01e3d07ad3e1c32ab4 Mon Sep 17 00:00:00 2001 From: Thomson Comer Date: Fri, 28 Oct 2022 14:09:12 -0500 Subject: [PATCH 35/90] Add polygons and multilinestrings and some known failure cases. --- python/cuspatial/cuspatial/core/geoseries.py | 59 ++++++++++------- python/cuspatial/cuspatial/tests/conftest.py | 14 +++++ .../cuspatial/tests/test_contains.py | 63 ++++++++++++++++++- .../cuspatial/cuspatial/utils/column_utils.py | 8 +++ 4 files changed, 118 insertions(+), 26 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index ade7e84ee..f371f95e6 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -27,7 +27,7 @@ from cuspatial.core.spatial.binops import contains from cuspatial.utils.column_utils import ( contains_only_linestrings, - contains_only_points, + contains_only_multipoints, contains_only_polygons, ) @@ -153,6 +153,12 @@ def xy(self): result = self._col.take(indices._column).leaves().values return cudf.Series(result) + def point_indices(self): + # Points only case + offsets = cp.arange(0, len(self.x) * 2 + 1, 2) + sizes = offsets[1:] - offsets[:-1] + return cp.repeat(self._series.index, sizes) + class MultiPointGeoColumnAccessor(GeoColumnAccessor): def __init__(self, list_series, meta): super().__init__(list_series, meta) @@ -162,6 +168,11 @@ def __init__(self, list_series, meta): def geometry_offset(self): return cudf.Series(self._col.offsets.values) + def point_indices(self): + offsets = cp.array(self.geometry_offset) + sizes = offsets[1:] - offsets[:-1] + return cp.repeat(self._series.index, sizes) + class LineStringGeoColumnAccessor(GeoColumnAccessor): def __init__(self, list_series, meta): super().__init__(list_series, meta) @@ -175,9 +186,6 @@ def geometry_offset(self): def part_offset(self): return cudf.Series(self._col.elements.offsets.values) - def as_points(self): - return GeoSeries(self._col.elements.elements.elements) - def point_indices(self): offsets = cp.array(self.part_offset) sizes = offsets[1:] - offsets[:-1] @@ -544,25 +552,26 @@ def contains(self, other, align=True): # RHS conditioning: mode = "POINTS" # point in polygon - if contains_only_points(other) is True: - # no conditioning is required - points = other.points - # mpoint in polygon - # linestring in polygon + if contains_only_linestrings(other) is True: + # condition for linestrings + mode = "LINESTRINGS" + xy = other.lines + elif contains_only_polygons(other) is True: + # polygon in polygon + mode = "POLYGONS" + xy = other.polygons + elif contains_only_multipoints(other) is True: + # mpoint in polygon + mode = "MULTIPOINTS" + xy = other.multipoints else: - if contains_only_linestrings(other) is True: - # condition for linestrings - mode = "LINESTRINGS" - xy = other.lines - elif contains_only_polygons(other) is True: - # polygon in polygon - mode = "POLYGONS" - xy = other.polygons - xy_points = xy.xy - point_indices = xy.point_indices() - points = GeoSeries( - GeoColumn._from_points_xy(xy_points._column) - ).points + # no conditioning is required + xy = other.points + # mpoint in polygon + # linestring in polygon + xy_points = xy.xy + point_indices = xy.point_indices() + points = GeoSeries(GeoColumn._from_points_xy(xy_points._column)).points # call pip on the three subtypes on the right: point_result = contains( @@ -582,7 +591,11 @@ def contains(self, other, align=True): # linestring in polygon for all points = true # polygon in polygon for all points = true """ - if mode == "LINESTRINGS" or mode == "POLYGONS": + if ( + mode == "LINESTRINGS" + or mode == "POLYGONS" + or mode == "MULTIPOINTS" + ): # process for completed linestrings result = cudf.DataFrame( {"idx": point_indices, "pip": point_result} diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 737c041ac..b9d5dd404 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -207,6 +207,20 @@ def generator(n, max_num_geometries, max_num_segments): return generator +@pytest.fixture +def simple_polygon_generator(): + np.random.seed(0) + rstate = np.random.RandomState(0) + + def generator(n, distance_from_origin, radius=1.0): + for _ in range(n): + outer = Point(distance_from_origin * 2, 0).buffer(radius) + circle = Polygon(outer) + yield rotate(circle, rstate.random() * 2 * np.pi, use_radians=True) + + return generator + + @pytest.fixture def polygon_generator(): rstate = np.random.RandomState(0) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 5d6aafcec..791e43388 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -211,6 +211,20 @@ def test_one_polygon_one_linestring(linestring_generator): ).all() +@pytest.mark.xfail(reason="The boundaries share points, so pip is impossible.") +def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): + gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) + gpdpolygon = gpd.GeoSeries( + Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + assert ( + gpdpolygon.contains(gpdlinestring).values + == polygons.contains(linestring).values_host + ).all() + + def test_four_polygons_four_linestrings(linestring_generator): gpdlinestring = gpd.GeoSeries( [ @@ -259,9 +273,21 @@ def test_one_polygon_one_polygon(polygon_generator): ).all() -def test_max_polygons_max_polygons(polygon_generator): - gpdlhs = gpd.GeoSeries([*polygon_generator(31, 0, 10)]) - gpdrhs = gpd.GeoSeries([*polygon_generator(31, 0, 1)]) +@pytest.mark.xfail( + reason="The polygons share numerous boundaries, so pip is impossible." +) +def test_manual_polygons(): + gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) + gpdrhs = gpd.GeoSeries( + [ + Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), + Polygon(((-2, -2), (-2, 2), (2, 2), (2, -2))), + Polygon(((-10, -2), (-10, 2), (-6, 2), (-6, -2))), + Polygon(((-2, 8), (-2, 12), (2, 12), (2, 8))), + Polygon(((6, 0), (8, 2), (10, 0), (8, -2))), + Polygon(((-2, -8), (-2, -4), (2, -4), (2, -8))), + ] + ) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) assert ( @@ -270,3 +296,34 @@ def test_max_polygons_max_polygons(polygon_generator): assert ( gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host ).all() + + +def test_max_polygons_max_polygons(simple_polygon_generator): + gpdlhs = gpd.GeoSeries([*simple_polygon_generator(31, 1, 3)]) + gpdrhs = gpd.GeoSeries([*simple_polygon_generator(31, 1.49, 2)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + assert ( + gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + ).all() + assert ( + gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host + ).all() + + +def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): + gpdlhs = gpd.GeoSeries([*polygon_generator(1, 0)]) + gpdrhs = gpd.GeoSeries([*multipoint_generator(1, 5)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + assert gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + + +def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): + gpdlhs = gpd.GeoSeries([*polygon_generator(31, 0, 1)]) + gpdrhs = gpd.GeoSeries([*multipoint_generator(31, 10)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + assert ( + gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + ).all() diff --git a/python/cuspatial/cuspatial/utils/column_utils.py b/python/cuspatial/cuspatial/utils/column_utils.py index 3e5ac322f..fab7ff0e4 100644 --- a/python/cuspatial/cuspatial/utils/column_utils.py +++ b/python/cuspatial/cuspatial/utils/column_utils.py @@ -75,6 +75,14 @@ def contains_only_points(gs: GeoSeries): ) +def contains_only_multipoints(gs: GeoSeries): + """ + Returns true if `gs` contains only multipoints + """ + + return contain_single_type_geometry(gs) and (len(gs.multipoints.xy) > 0) + + def contains_only_linestrings(gs: GeoSeries): """ Returns true if `gs` contains only linestrings From c55ac729b97d113b9d9a31ee15f48fe140cfb94b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 1 Nov 2022 17:00:00 -0500 Subject: [PATCH 36/90] Rename is_point_in_polygon.cuh --- .../{is_point_in_polygon_kernel.cuh => is_point_in_polygon.cuh} | 0 .../cuspatial/experimental/detail/pairwise_point_in_polygon.cuh | 2 +- cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename cpp/include/cuspatial/experimental/detail/{is_point_in_polygon_kernel.cuh => is_point_in_polygon.cuh} (100%) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh similarity index 100% rename from cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh rename to cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index 47f92d2ec..b4fcb9562 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -17,7 +17,7 @@ #pragma once #include -#include +#include #include #include diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index e4e16263f..5232cae1f 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -17,7 +17,7 @@ #pragma once #include -#include +#include #include #include From 56240687a8c72c8c5c7b734a4573260451ea0c53 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 1 Nov 2022 17:04:11 -0500 Subject: [PATCH 37/90] Keep only colinearity epsilon. --- .../detail/is_point_in_polygon.cuh | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh index bf61b4f48..b174708fd 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh @@ -74,29 +74,18 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, T rise_to_point = test_point.y - a.y; // colinearity test - const T EPSILON = 0.0000000000000000000000000000001; - T run_to_point = test_point.x - a.x; - T colinearity = (run * rise_to_point - run_to_point * rise); - is_colinear = colinearity * colinearity < EPSILON; - printf("Colinearity: %.60lf\n", colinearity * colinearity); - printf("Is colinear? : %s\n", is_colinear ? "True" : "False"); + T run_to_point = test_point.x - a.x; + T colinearity = (run * rise_to_point - run_to_point * rise); + is_colinear = abs(colinearity) < numeric_limits::epsilon(); if (is_colinear) { break; } y1_flag = a.y > test_point.y; - printf("Can intersect? : %s\n", y1_flag != y0_flag ? "True" : "False"); if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - const T INTERSECT_EPSILON = 0.00000000000001; - printf("lhs - rhs: %.60lf\n", lhs - rhs); - if (lhs - INTERSECT_EPSILON < rhs != y1_flag) { - printf("lhs: %.60lf\n", lhs - INTERSECT_EPSILON); - printf("rhs: %.60lf\n", rhs); - printf("Does intersect\n"); - point_is_within = not point_is_within; - } + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + if (lhs < rhs != y1_flag) { point_is_within = not point_is_within; } } b = a; y0_flag = y1_flag; From 9bb615b57da58d9cf9cf729149da5301fc300f37 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 2 Nov 2022 11:19:39 -0500 Subject: [PATCH 38/90] Revert "Merge branch 'feature/pairwise_point_in_polygon' into feature/GeoSeries.contains" This reverts commit 08e845bb7b2c350f254cac4773059470cb38bec4, reversing changes made to 87020fa92b34215abfa1cd358d4c62807dd88ccf. --- .../experimental/detail/is_point_in_polygon_kernel.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh index 3d33b2b72..4f7e31bce 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon_kernel.cuh @@ -75,7 +75,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, // colinearity test T run_to_point = test_point.x - a.x; - is_colinear = run * rise_to_point == run_to_point * rise; + is_colinear = (run * rise_to_point - run_to_point * rise) == 0; if (is_colinear) { break; } y1_flag = a.y > test_point.y; From 755039973e83f176941172b725ffcc22982844e1 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 4 Nov 2022 14:47:06 -0500 Subject: [PATCH 39/90] Change to grid/stride loop. --- .../detail/is_point_in_polygon.cuh | 2 +- .../detail/pairwise_point_in_polygon.cuh | 31 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh index b174708fd..0b031f04c 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh @@ -76,7 +76,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, // colinearity test T run_to_point = test_point.x - a.x; T colinearity = (run * rise_to_point - run_to_point * rise); - is_colinear = abs(colinearity) < numeric_limits::epsilon(); + is_colinear = abs(colinearity) < std::numeric_limits::epsilon(); if (is_colinear) { break; } y1_flag = a.y > test_point.y; diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index b4fcb9562..28d9cf0a7 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -52,21 +52,22 @@ __global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, { using Cart2d = iterator_value_type; using OffsetType = iterator_value_type; - auto idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= num_test_points) { return; } - - Cart2d const test_point = test_points_first[idx]; - // for the matching polygon - OffsetType poly_begin = poly_offsets_first[idx]; - OffsetType poly_end = (idx + 1 < num_polys) ? poly_offsets_first[idx + 1] : num_rings; - bool const point_is_within = is_point_in_polygon(test_point, - poly_begin, - poly_end, - ring_offsets_first, - num_rings, - poly_points_first, - num_poly_points); - result[idx] = point_is_within; + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; + idx < std::distance(test_points_first, thrust::prev(test_points_first + num_test_points)); + idx += gridDim.x * blockDim.x) { + Cart2d const test_point = test_points_first[idx]; + // for the matching polygon + OffsetType poly_begin = poly_offsets_first[idx]; + OffsetType poly_end = (idx + 1 < num_polys) ? poly_offsets_first[idx + 1] : num_rings; + bool const point_is_within = is_point_in_polygon(test_point, + poly_begin, + poly_end, + ring_offsets_first, + num_rings, + poly_points_first, + num_poly_points); + result[idx] = point_is_within; + } } } // namespace detail From be5a5c00ff69a1f93cacaf26c6e3a7f63f46c05b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 28 Nov 2022 13:06:46 -0600 Subject: [PATCH 40/90] Fix bug with grid-stride loop and switch to for comparison. --- .../cuspatial/experimental/detail/is_point_in_polygon.cuh | 8 +++++--- .../experimental/detail/pairwise_point_in_polygon.cuh | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh index 0b031f04c..b05fd934a 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh @@ -18,6 +18,8 @@ #include +#include + namespace cuspatial { namespace detail { @@ -69,14 +71,14 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, // Points on the line segment are the same, so intersection is impossible. // This is possible because we allow closed or unclosed polygons. - if (run == 0.0 && rise == 0.0) continue; + T zero = 0.0; + if (float_equal(run, zero) && float_equal(rise, zero)) continue; T rise_to_point = test_point.y - a.y; // colinearity test T run_to_point = test_point.x - a.x; - T colinearity = (run * rise_to_point - run_to_point * rise); - is_colinear = abs(colinearity) < std::numeric_limits::epsilon(); + is_colinear = float_equal(run * rise_to_point, run_to_point * rise); if (is_colinear) { break; } y1_flag = a.y > test_point.y; diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index 28d9cf0a7..a4a993e34 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -53,7 +53,8 @@ __global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, using Cart2d = iterator_value_type; using OffsetType = iterator_value_type; for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; - idx < std::distance(test_points_first, thrust::prev(test_points_first + num_test_points)); + idx < + std::distance(test_points_first, thrust::prev(test_points_first + num_test_points + 1)); idx += gridDim.x * blockDim.x) { Cart2d const test_point = test_points_first[idx]; // for the matching polygon From f7bbf1010bda226f2497df5c9b5e02f650df2546 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 28 Nov 2022 13:25:59 -0600 Subject: [PATCH 41/90] Update cpp/include/cuspatial/pairwise_point_in_polygon.hpp Co-authored-by: Mark Harris --- cpp/include/cuspatial/pairwise_point_in_polygon.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp index 07213816b..fbdb7f1ae 100644 --- a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp +++ b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp @@ -32,7 +32,7 @@ namespace cuspatial { */ /** - * @brief Tests whether the specified points are inside each of their corresponding polygons. + * @brief Given (point, polygon pairs), tests whether the point of each pair is inside the polygon of the pair. * * Tests that each point is or is not inside of the polygon in the corresponding index. * Polygons are a collection of one or more * rings. Rings are a collection of three or more From b6d55b84f0aabdfbba8b0cc23eee82f9024256dd Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 28 Nov 2022 13:26:17 -0600 Subject: [PATCH 42/90] Update cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh Co-authored-by: Mark Harris --- .../cuspatial/experimental/pairwise_point_in_polygon.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index 18dd74151..9eb17d3c0 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -23,7 +23,7 @@ namespace cuspatial { /** * @ingroup spatial_relationship * - * @brief Tests whether the specified points are inside their corresponding polygon. + * @brief Given (point, polygon) pairs, tests whether the point of each pair is inside the polygon of the pair. * * Tests whether each point is inside a corresponding polygon. Points on the edges of the * polygon are not considered to be inside. Floating point precision limitations mean that From d3d753327de572a1c87427b33c6f807a5c767be5 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 28 Nov 2022 13:26:34 -0600 Subject: [PATCH 43/90] Update cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh Co-authored-by: Mark Harris --- .../cuspatial/experimental/pairwise_point_in_polygon.cuh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index 9eb17d3c0..0d9a3d78b 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -26,8 +26,7 @@ namespace cuspatial { * @brief Given (point, polygon) pairs, tests whether the point of each pair is inside the polygon of the pair. * * Tests whether each point is inside a corresponding polygon. Points on the edges of the - * polygon are not considered to be inside. Floating point precision limitations mean that - * some arithmetically excluded boundary points will not be excluded, and vice-versa. + * polygon are not considered to be inside. * Polygons are a collection of one or more rings. Rings are a collection of three or more vertices. * * Each input point will map to one `int32_t` element in the output. From 0edd2dfa81156f477436fde4123fc85c66a57a91 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 28 Nov 2022 13:47:39 -0600 Subject: [PATCH 44/90] run-clang-format.py --- .../pairwise_point_in_polygon.cuh | 3 +- .../cuspatial/pairwise_point_in_polygon.hpp | 3 +- .../spatial/pairwise_point_in_polygon_test.cu | 28 +++++++++---------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh index 0d9a3d78b..129c79324 100644 --- a/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_point_in_polygon.cuh @@ -23,7 +23,8 @@ namespace cuspatial { /** * @ingroup spatial_relationship * - * @brief Given (point, polygon) pairs, tests whether the point of each pair is inside the polygon of the pair. + * @brief Given (point, polygon) pairs, tests whether the point of each pair is inside the polygon + * of the pair. * * Tests whether each point is inside a corresponding polygon. Points on the edges of the * polygon are not considered to be inside. diff --git a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp index fbdb7f1ae..76563d2fc 100644 --- a/cpp/include/cuspatial/pairwise_point_in_polygon.hpp +++ b/cpp/include/cuspatial/pairwise_point_in_polygon.hpp @@ -32,7 +32,8 @@ namespace cuspatial { */ /** - * @brief Given (point, polygon pairs), tests whether the point of each pair is inside the polygon of the pair. + * @brief Given (point, polygon pairs), tests whether the point of each pair is inside the polygon + * of the pair. * * Tests that each point is or is not inside of the polygon in the corresponding index. * Polygons are a collection of one or more * rings. Rings are a collection of three or more diff --git a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu index 970014edb..5a5fe62e3 100644 --- a/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu +++ b/cpp/tests/experimental/spatial/pairwise_point_in_polygon_test.cu @@ -64,7 +64,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) this->make_device_points({{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {-1.0, -1.0}}); auto got = rmm::device_vector(1); - auto expected = std::vector{false, false, false, false, true, true, true, true}; + auto expected = std::vector{false, false, false, false, true, true, true, true}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -77,7 +77,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonOneRing) poly_point.begin(), poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i]})); + EXPECT_EQ(got, std::vector({expected[i]})); EXPECT_EQ(ret, got.end()); } } @@ -108,7 +108,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) {0.0, 1.0}}); auto got = rmm::device_vector(2); - auto expected = std::vector({false, false, false, false, true, true, true, true}); + auto expected = std::vector({false, false, false, false, true, true, true, true}); for (size_t i = 0; i < point_list.size() / 2; i = i + 2) { auto points = this->make_device_points( @@ -123,7 +123,7 @@ TYPED_TEST(PairwisePointInPolygonTest, TwoPolygonsOneRingEach) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); + EXPECT_EQ(got, std::vector({expected[i], expected[i + 1]})); EXPECT_EQ(ret, got.end()); } } @@ -147,7 +147,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) {-0.5, -0.5}}); auto got = rmm::device_vector(1); - auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; + auto expected = std::vector{0b0, 0b0, 0b1, 0b0, 0b1}; for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -161,7 +161,7 @@ TYPED_TEST(PairwisePointInPolygonTest, OnePolygonTwoRings) poly_point.end(), got.begin()); - EXPECT_EQ(got, std::vector{expected[i]}); + EXPECT_EQ(got, std::vector{expected[i]}); EXPECT_EQ(ret, got.end()); } } @@ -181,7 +181,7 @@ TYPED_TEST(PairwisePointInPolygonTest, EdgesOfSquare) {1.0, 1.0}, {0.0, 1.0}, {0.0, -1.0}, {-1.0, -1.0}, {-1.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {-1.0, 1.0}, {-1.0, 0.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {-1.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -213,7 +213,7 @@ TYPED_TEST(PairwisePointInPolygonTest, CornersOfSquare) {0.0, 1.0}, {-1.0, 0.0}, {-1.0, 0.0}, {0.0, -1.0}, {0.0, 0.0}, {1.0, 0.0}, {1.0, -1.0}, {0.0, -1.0}, {0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}}); - auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; + auto expected = std::vector{0b0, 0b0, 0b0, 0b0}; auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -286,8 +286,8 @@ TYPED_TEST(PairwisePointInPolygonTest, 32PolygonSupport) thrust::make_transform_iterator(offsets_iter, PolyPointIteratorFunctorB{}); auto poly_point_iter = make_vec_2d_iterator(poly_point_xs_iter, poly_point_ys_iter); - auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); + auto expected = std::vector({1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}); auto got = rmm::device_vector(test_point.size()); auto ret = pairwise_point_in_polygon(test_point.begin(), @@ -337,7 +337,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "left" edge missing auto poly_point = this->make_device_points({{-1, 1}, {1, 1}, {1, -1}, {-1, -1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { @@ -352,7 +352,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopLeftEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } @@ -365,7 +365,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) auto poly_ring_offsets = this->make_device_offsets({0}); // "right" edge missing auto poly_point = this->make_device_points({{1, -1}, {-1, -1}, {-1, 1}, {1, 1}}); - auto expected = std::vector{0b0, 0b1, 0b0}; + auto expected = std::vector{0b0, 0b1, 0b0}; auto got = rmm::device_vector(1); for (size_t i = 0; i < point_list.size(); ++i) { auto point = this->make_device_points({{point_list[i][0], point_list[i][1]}}); @@ -379,7 +379,7 @@ TYPED_TEST(PairwisePointInPolygonTest, SelfClosingLoopRightEdgeMissing) poly_point.end(), got.begin()); - EXPECT_EQ(std::vector{expected[i]}, got); + EXPECT_EQ(std::vector{expected[i]}, got); EXPECT_EQ(got.end(), ret); } } From 1c02f3ca0df6163c40891777e2318b1499e69dd1 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 08:24:33 -0600 Subject: [PATCH 45/90] Improve grid stride loop computations. --- .../experimental/detail/pairwise_point_in_polygon.cuh | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index a4a993e34..8753367f6 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -16,6 +16,7 @@ #pragma once +#include #include #include #include @@ -52,9 +53,7 @@ __global__ void pairwise_point_in_polygon_kernel(Cart2dItA test_points_first, { using Cart2d = iterator_value_type; using OffsetType = iterator_value_type; - for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; - idx < - std::distance(test_points_first, thrust::prev(test_points_first + num_test_points + 1)); + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < num_test_points; idx += gridDim.x * blockDim.x) { Cart2d const test_point = test_points_first[idx]; // for the matching polygon @@ -118,10 +117,8 @@ OutputIt pairwise_point_in_polygon(Cart2dItA test_points_first, // TODO: introduce a validation function that checks the rings of the polygon are // actually closed. (i.e. the first and last vertices are the same) - auto constexpr block_size = 256; - auto const num_blocks = (num_test_points + block_size - 1) / block_size; - - detail::pairwise_point_in_polygon_kernel<<>>( + auto [threads_per_block, num_blocks] = grid_1d(num_test_points); + detail::pairwise_point_in_polygon_kernel<<>>( test_points_first, num_test_points, polygon_offsets_first, From 89169ee56714e93bbe7875a7544c8e2028ff1c73 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 08:25:32 -0600 Subject: [PATCH 46/90] Update cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh Co-authored-by: Michael Wang --- .../cuspatial/experimental/detail/is_point_in_polygon.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh index b05fd934a..b9c7d5ac6 100644 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh @@ -71,7 +71,7 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, // Points on the line segment are the same, so intersection is impossible. // This is possible because we allow closed or unclosed polygons. - T zero = 0.0; + T constexpr zero = 0.0; if (float_equal(run, zero) && float_equal(rise, zero)) continue; T rise_to_point = test_point.y - a.y; From 0cf4602f4635503a6ac0248f461f5704c5449f44 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 09:05:36 -0600 Subject: [PATCH 47/90] Resolve circular import issue. --- .../core/binops/binops_with_quadtree.py | 126 ++++++++++++++++++ .../{spatial/binops.py => binops/contains.py} | 0 python/cuspatial/cuspatial/core/geoseries.py | 2 +- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py rename python/cuspatial/cuspatial/core/{spatial/binops.py => binops/contains.py} (100%) diff --git a/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py b/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py new file mode 100644 index 000000000..ed5861158 --- /dev/null +++ b/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. + +from cudf import Series +from cudf.core.column import as_column + +import cuspatial +from cuspatial._lib.point_in_polygon import ( + point_in_polygon as cpp_point_in_polygon, +) +from cuspatial.utils.column_utils import normalize_point_columns + + +def contains( + test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, +): + """Compute from a set of points and a set of polygons which points fall + within each polygon. Note that `polygons_(x,y)` must be specified as + closed polygons: the first and last coordinate of each polygon must be + the same. + + Parameters + ---------- + test_points_x + x-coordinate of test points + test_points_y + y-coordinate of test points + poly_offsets + beginning index of the first ring in each polygon + poly_ring_offsets + beginning index of the first point in each ring + poly_points_x + x closed-coordinate of polygon points + poly_points_y + y closed-coordinate of polygon points + + Examples + -------- + + Test whether 3 points fall within either of two polygons + + # TODO: Examples + + note + input Series x and y will not be index aligned, but computed as + sequential arrays. + + note + poly_ring_offsets must contain only the rings that make up the polygons + indexed by poly_offsets. If there are rings in poly_ring_offsets that + are not part of the polygons in poly_offsets, results are likely to be + incorrect and behavior is undefined. + + Returns + ------- + result : cudf.DataFrame + A DataFrame of boolean values indicating whether each point falls + within each polygon. + """ + + if len(poly_offsets) == 0: + return Series() + ( + test_points_x, + test_points_y, + poly_points_x, + poly_points_y, + ) = normalize_point_columns( + as_column(test_points_x), + as_column(test_points_y), + as_column(poly_points_x), + as_column(poly_points_y), + ) + + QUADTREE = False + if QUADTREE: + scale = 5 + max_depth = 7 + min_size = 125 + x_max = poly_points_x.max() + x_min = poly_points_x.min() + y_max = poly_points_y.max() + y_min = poly_points_y.min() + point_indices, quadtree = cuspatial.quadtree_on_points( + test_points_x, + test_points_y, + x_min, + x_max, + y_min, + y_max, + scale, + max_depth, + min_size, + ) + poly_bboxes = cuspatial.polygon_bounding_boxes( + poly_offsets, poly_ring_offsets, poly_points_x, poly_points_y + ) + intersections = cuspatial.join_quadtree_and_bounding_boxes( + quadtree, poly_bboxes, x_min, x_max, y_min, y_max, scale, max_depth + ) + polygons_and_points = cuspatial.quadtree_point_in_polygon( + intersections, + quadtree, + point_indices, + test_points_x, + test_points_y, + poly_offsets, + poly_ring_offsets, + poly_points_x, + poly_points_y, + ) + return polygons_and_points + else: + pip_result = cpp_point_in_polygon( + test_points_x, + test_points_y, + as_column(poly_offsets, dtype="int32"), + as_column(poly_ring_offsets, dtype="int32"), + poly_points_x, + poly_points_y, + ) + return Series(pip_result, dtype="bool") diff --git a/python/cuspatial/cuspatial/core/spatial/binops.py b/python/cuspatial/cuspatial/core/binops/contains.py similarity index 100% rename from python/cuspatial/cuspatial/core/spatial/binops.py rename to python/cuspatial/cuspatial/core/binops/contains.py diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 6ae932e1a..7408ddd4b 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -24,7 +24,7 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.spatial.binops import contains +from cuspatial.core.binops.contains import contains from cuspatial.utils.column_utils import ( contains_only_linestrings, contains_only_multipoints, From 66000c3fa6d0d9b7c945b2b9b77702f1b243e5ae Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 10:06:48 -0600 Subject: [PATCH 48/90] Pass pytests and refactor to got/expected pattern. --- python/cuspatial/cuspatial/tests/conftest.py | 11 + .../cuspatial/tests/test_contains.py | 285 +++++++++--------- 2 files changed, 160 insertions(+), 136 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index b9d5dd404..ac2945dcc 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -252,3 +252,14 @@ def generator(n, max_per_multi): yield MultiPolygon([*polygon_generator(0, num_polygons)]) return generator + + +@pytest.fixture +def slice_twenty(): + return [ + slice(0, 4), + slice(4, 8), + slice(8, 12), + slice(12, 16), + slice(16, 20), + ] diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 791e43388..1027dc6f0 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -4,18 +4,108 @@ import cuspatial -""" -[ - Point([0.6, 0.06]), - Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), - False, -], -[ - Point([3.333, 1.111]), - Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), - True, -], -""" + +@pytest.mark.xfail( + reason="The polygons share numerous boundaries, so pip is impossible." +) +def test_manual_polygons(): + gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) + gpdrhs = gpd.GeoSeries( + [ + Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), + Polygon(((-2, -2), (-2, 2), (2, 2), (2, -2))), + Polygon(((-10, -2), (-10, 2), (-6, 2), (-6, -2))), + Polygon(((-2, 8), (-2, 12), (2, 12), (2, 8))), + Polygon(((6, 0), (8, 2), (10, 0), (8, -2))), + Polygon(((-2, -8), (-2, -4), (2, -4), (2, -8))), + ] + ) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + expected = gpdlhs.contains(gpdrhs).values + got = lhs.contains(rhs).values_host + assert (got == expected).all() + expected = gpdrhs.contains(gpdlhs).values + got = rhs.contains(lhs).values_host + assert (got == expected).all() + + +@pytest.mark.xfail(reason="The boundaries share points, so pip is impossible.") +def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): + gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) + gpdpolygon = gpd.GeoSeries( + Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + expected = gpdpolygon.contains(gpdlinestring).values + got = polygons.contains(linestring).values_host + assert (got == expected).all() + + +@pytest.mark.xfail( + reason="We don't support intersection yet to check for crossings." +) +def test_one_polygon_with_hole_one_linestring_crossing_it( + linestring_generator, +): + gpdlinestring = gpd.GeoSeries(LineString([[0.5, 2.0], [3.5, 2.0]])) + gpdpolygon = gpd.GeoSeries( + Polygon( + ( + [0, 0], + [0, 4], + [4, 4], + [4, 0], + [0, 0], + ), + [ + ( + [1, 1], + [1, 3], + [3, 3], + [3, 1], + [1, 1], + ) + ], + ) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + expected = gpdpolygon.contains(gpdlinestring).values + got = polygons.contains(linestring).values_host + assert (got == expected).all() + + +@pytest.mark.xfail( + reason="""These boundary cases conflict with Pandas results because they + implement `contains` and we implement `contains_properly`.""" +) +@pytest.mark.parametrize( + "point, polygon, expects", + [ + [ + Point([0.6, 0.06]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [ + Point([3.333, 1.111]), + Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), + True, + ], + [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], + ], +) +def test_float_precision_limits_failures(point, polygon, expects): + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + expected = gpdpolygon.contains(gpdpoint).values + got = polygon.contains(point).values_host + assert got == expected + assert got.values_host[0] == expects @pytest.mark.parametrize( @@ -33,7 +123,6 @@ False, ], [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], - [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], ], ) def test_float_precision_limits(point, polygon, expects): @@ -41,9 +130,10 @@ def test_float_precision_limits(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - result = polygon.contains(point) - assert gpdpolygon.contains(gpdpoint).values == result.values_host - assert result.values_host[0] == expects + got = polygon.contains(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert got == expected + assert got[0] == expects clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) @@ -90,9 +180,10 @@ def test_point_in_polygon(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - result = polygon.contains(point) - assert gpdpolygon.contains(gpdpoint).values == result.values_host - assert result.values_host[0] == expects + got = polygon.contains(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert got == expected + assert got[0] == expects def test_two_points_one_polygon(): @@ -100,10 +191,9 @@ def test_two_points_one_polygon(): gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdpoint).values - == polygon.contains(point).values_host - ).all() + got = polygon.contains(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert (got == expected).all() def test_one_point_two_polygons(): @@ -116,10 +206,9 @@ def test_one_point_two_polygons(): ) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdpoint).values - == polygon.contains(point).values_host - ).all() + got = polygon.contains(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert (got == expected).all() def test_ten_pair_points(point_generator, polygon_generator): @@ -127,45 +216,9 @@ def test_ten_pair_points(point_generator, polygon_generator): gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) points = cuspatial.from_geopandas(gpdpoints) polygons = cuspatial.from_geopandas(gpdpolygons) - assert ( - gpdpolygons.contains(gpdpoints).values - == polygons.contains(points).values_host - ).all() - - -@pytest.mark.xfail( - reason="We don't support intersection yet to check for crossings." -) -def test_one_polygon_with_hole_one_linestring_crossing_it( - linestring_generator, -): - gpdlinestring = gpd.GeoSeries(LineString([[0.5, 2.0], [3.5, 2.0]])) - gpdpolygon = gpd.GeoSeries( - Polygon( - ( - [0, 0], - [0, 4], - [4, 4], - [4, 0], - [0, 0], - ), - [ - ( - [1, 1], - [1, 3], - [3, 3], - [3, 1], - [1, 1], - ) - ], - ) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdlinestring).values - == polygons.contains(linestring).values_host - ).all() + got = polygons.contains(points).values_host + expected = gpdpolygons.contains(gpdpoints).values + assert (got == expected).all() def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): @@ -192,10 +245,9 @@ def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdlinestring).values - == polygons.contains(linestring).values_host - ).all() + got = polygons.contains(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() def test_one_polygon_one_linestring(linestring_generator): @@ -205,24 +257,9 @@ def test_one_polygon_one_linestring(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdlinestring).values - == polygons.contains(linestring).values_host - ).all() - - -@pytest.mark.xfail(reason="The boundaries share points, so pip is impossible.") -def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): - gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdlinestring).values - == polygons.contains(linestring).values_host - ).all() + got = polygons.contains(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() def test_four_polygons_four_linestrings(linestring_generator): @@ -244,10 +281,9 @@ def test_four_polygons_four_linestrings(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - assert ( - gpdpolygon.contains(gpdlinestring).values - == polygons.contains(linestring).values_host - ).all() + got = polygons.contains(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): @@ -255,9 +291,9 @@ def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): gpdpolygons = gpd.GeoSeries([*polygon_generator(31, 0)]) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygons) - gpdresult = gpdpolygons.contains(gpdlinestring) - result = polygons.contains(linestring) - assert (gpdresult.values == result.values_host).all() + got = polygons.contains(linestring).values_host + expected = gpdpolygons.contains(gpdlinestring).values + assert (got == expected).all() def test_one_polygon_one_polygon(polygon_generator): @@ -265,37 +301,12 @@ def test_one_polygon_one_polygon(polygon_generator): gpdrhs = gpd.GeoSeries([*polygon_generator(1, 0)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - assert ( - gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host - ).all() - assert ( - gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host - ).all() - - -@pytest.mark.xfail( - reason="The polygons share numerous boundaries, so pip is impossible." -) -def test_manual_polygons(): - gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) - gpdrhs = gpd.GeoSeries( - [ - Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), - Polygon(((-2, -2), (-2, 2), (2, 2), (2, -2))), - Polygon(((-10, -2), (-10, 2), (-6, 2), (-6, -2))), - Polygon(((-2, 8), (-2, 12), (2, 12), (2, 8))), - Polygon(((6, 0), (8, 2), (10, 0), (8, -2))), - Polygon(((-2, -8), (-2, -4), (2, -4), (2, -8))), - ] - ) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - assert ( - gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host - ).all() - assert ( - gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host - ).all() + expected = gpdlhs.contains(gpdrhs).values + got = lhs.contains(rhs).values_host + assert (expected == got).all() + expected = gpdrhs.contains(gpdlhs).values + got = rhs.contains(lhs).values_host + assert (got == expected).all() def test_max_polygons_max_polygons(simple_polygon_generator): @@ -303,12 +314,12 @@ def test_max_polygons_max_polygons(simple_polygon_generator): gpdrhs = gpd.GeoSeries([*simple_polygon_generator(31, 1.49, 2)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - assert ( - gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host - ).all() - assert ( - gpdrhs.contains(gpdlhs).values == rhs.contains(lhs).values_host - ).all() + expected = gpdlhs.contains(gpdrhs).values + got = lhs.contains(rhs).values_host + assert (expected == got).all() + expected = gpdrhs.contains(gpdlhs).values + got = rhs.contains(lhs).values_host + assert (got == expected).all() def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): @@ -316,7 +327,9 @@ def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): gpdrhs = gpd.GeoSeries([*multipoint_generator(1, 5)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - assert gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + got = lhs.contains(rhs).values_host + assert (got == expected).all() def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): @@ -324,6 +337,6 @@ def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): gpdrhs = gpd.GeoSeries([*multipoint_generator(31, 10)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - assert ( - gpdlhs.contains(gpdrhs).values == lhs.contains(rhs).values_host - ).all() + expected = gpdlhs.contains(gpdrhs).values + got = lhs.contains(rhs).values_host + assert (got == expected).all() From 88312c91a35f18c13e3c1c06a6437ba4864ccf50 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 10:50:24 -0600 Subject: [PATCH 49/90] Add one more test case. --- python/cuspatial/cuspatial/tests/test_contains.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 1027dc6f0..f96f1eef2 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -262,13 +262,15 @@ def test_one_polygon_one_linestring(linestring_generator): assert (got == expected).all() -def test_four_polygons_four_linestrings(linestring_generator): +def test_six_polygons_six_linestrings(linestring_generator): gpdlinestring = gpd.GeoSeries( [ + LineString([[1.35, 0.35], [0.35, 0.65]]), LineString([[0.35, 0.35], [0.35, 0.65]]), LineString([[0.25, 0.25], [0.25, 0.75]]), LineString([[0.15, 0.15], [0.15, 0.85]]), LineString([[0.05, 0.05], [0.05, 0.95]]), + LineString([[0.05, 0.05], [1.05, 0.95]]), ] ) gpdpolygon = gpd.GeoSeries( @@ -277,6 +279,8 @@ def test_four_polygons_four_linestrings(linestring_generator): Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), ] ) linestring = cuspatial.from_geopandas(gpdlinestring) From 2c893d34862dd8044eafaacf7cd57904717f29b0 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:01:39 -0600 Subject: [PATCH 50/90] Write detailed xfail descriptions. --- .../cuspatial/tests/test_contains.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index f96f1eef2..5355c4c77 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -6,7 +6,9 @@ @pytest.mark.xfail( - reason="The polygons share numerous boundaries, so pip is impossible." + reason="""The polygons share numerous boundaries. See + https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# + for examples of each case.""" ) def test_manual_polygons(): gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) @@ -30,7 +32,12 @@ def test_manual_polygons(): assert (got == expected).all() -@pytest.mark.xfail(reason="The boundaries share points, so pip is impossible.") +@pytest.mark.xfail( + reason="""The linestring is colinear with one of the polygon + edges. It is excluded from `.contains_properly`. See + https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# + for detailed examples.""" +) def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) gpdpolygon = gpd.GeoSeries( @@ -44,7 +51,10 @@ def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): @pytest.mark.xfail( - reason="We don't support intersection yet to check for crossings." + reason="""The linestring has boundaries inside of the polygon, and crosses + over a single inner ring. See + https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# + for detailed examples.""" ) def test_one_polygon_with_hole_one_linestring_crossing_it( linestring_generator, @@ -78,8 +88,15 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( @pytest.mark.xfail( - reason="""These boundary cases conflict with Pandas results because they - implement `contains` and we implement `contains_properly`.""" + reason="""These points are on edges of their corresponding polygons. + Because of floating point error, they can have inconsistent results with + GeoPandas. In this case, GeoPandas results are incorrect due to their + geometry engine. It is possible that implementing `.contains_properly` + would correct this error. These boundary cases conflict with GeoPandas + results because they implement `contains` and we implement + `contains_properly`. Detailed documentation of this in + https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# + hasn't been completed yet.""" ) @pytest.mark.parametrize( "point, polygon, expects", From f81ac930b7ab857846cde11068c95417ee6e7e61 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:07:03 -0600 Subject: [PATCH 51/90] Update python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd --- .../cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd b/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd index 2d246453b..90149e590 100644 --- a/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd +++ b/python/cuspatial/cuspatial/_lib/cpp/pairwise_point_in_polygon.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2022, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr From 104657f4999dfa56920426f5941a4e52c6a1d042 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:08:08 -0600 Subject: [PATCH 52/90] Update python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx --- python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx b/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx index d303e1229..8ce2c952e 100644 --- a/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx +++ b/python/cuspatial/cuspatial/_lib/pairwise_point_in_polygon.pyx @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2022, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr from libcpp.utility cimport move From 4bd3f493a76b125e1f680f6b5142458bf3130cd5 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:17:24 -0600 Subject: [PATCH 53/90] Update python/cuspatial/cuspatial/core/geoseries.py --- python/cuspatial/cuspatial/core/geoseries.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 7408ddd4b..11ec8735a 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -586,8 +586,6 @@ def contains(self, other, align=True): else: # no conditioning is required xy = other.points - # mpoint in polygon - # linestring in polygon xy_points = xy.xy point_indices = xy.point_indices() points = GeoSeries(GeoColumn._from_points_xy(xy_points._column)).points From 0cdb9f33fe4b220330310b0e7380b390fbe91fb6 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:22:24 -0600 Subject: [PATCH 54/90] Update python/cuspatial/cuspatial/core/geoseries.py --- python/cuspatial/cuspatial/core/geoseries.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 11ec8735a..665ac7a5e 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -599,15 +599,6 @@ def contains(self, other, align=True): self.polygons.x, self.polygons.y, ) - """ - # Apply binpreds rules on results: - # point in polygon = true for row - # reverse index, points indices refer back to row - # indices - # mpoint in polygon for all points = true - # linestring in polygon for all points = true - # polygon in polygon for all points = true - """ if ( mode == "LINESTRINGS" or mode == "POLYGONS" From 437eea9b23269f946811f65415eb0d8ee0779a5e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:24:02 -0600 Subject: [PATCH 55/90] Tweak a comment. --- python/cuspatial/cuspatial/core/geoseries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 665ac7a5e..7881f0cb0 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -604,7 +604,8 @@ def contains(self, other, align=True): or mode == "POLYGONS" or mode == "MULTIPOINTS" ): - # process for completed linestrings + # process for completed linestrings, polygons, and multipoints. + # Not necessary for points. result = cudf.DataFrame( {"idx": point_indices, "pip": point_result} ) From ec545ac65cd105241fb3e4d9c303445ba17fe28b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:29:38 -0600 Subject: [PATCH 56/90] Add comments back to generators. --- python/cuspatial/cuspatial/tests/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index ac2945dcc..27ecfefdd 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -171,6 +171,7 @@ def generator(n): @pytest.fixture def multipoint_generator(point_generator): + """Generator for n multipoints. Usage: mp=generator(n, max_num_points)""" rstate = np.random.RandomState(0) def generator(n, max_num_geometries): @@ -183,7 +184,8 @@ def generator(n, max_num_geometries): @pytest.fixture def linestring_generator(point_generator): - rstate = np.random.RandomState(1) + """Generator for n linestrings. Usage: ls=generator(n, max_num_segments)""" + rstate = np.random.RandomState(0) def generator(n, max_num_segments): for _ in range(n): @@ -195,6 +197,9 @@ def generator(n, max_num_segments): @pytest.fixture def multilinestring_generator(linestring_generator): + """Generator for n multilinestrings. + Usage: mls=generator(n, max_num_lines, max_num_segments) + """ rstate = np.random.RandomState(0) def generator(n, max_num_geometries, max_num_segments): From 4aa40646812a502fa02ef9e09cfc26bf7661c73f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:33:26 -0600 Subject: [PATCH 57/90] Write more fixture docs. --- python/cuspatial/cuspatial/tests/conftest.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 27ecfefdd..aa35cd1ac 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -160,6 +160,7 @@ def gs_sorted(gs): @pytest.fixture def point_generator(): + """Generator for n points. Usage: p=generator(n)""" rstate = np.random.RandomState(0) def generator(n): @@ -214,7 +215,9 @@ def generator(n, max_num_geometries, max_num_segments): @pytest.fixture def simple_polygon_generator(): - np.random.seed(0) + """Generator for polygons with no interior ring. + Usage: poly=generator(n, distance_from_origin, radius) + """ rstate = np.random.RandomState(0) def generator(n, distance_from_origin, radius=1.0): @@ -228,6 +231,12 @@ def generator(n, distance_from_origin, radius=1.0): @pytest.fixture def polygon_generator(): + """Generator for complex polygons. Each polygon will + have 1-4 randomly rotated interior polygons. Each polygon + is a circle, with very small inner ring polygons located in + a spiral around its center. + Usage: poly=generator(n, distance_from_origin, radius) + """ rstate = np.random.RandomState(0) def generator(n, distance_from_origin, radius=1.0): @@ -249,6 +258,9 @@ def generator(n, distance_from_origin, radius=1.0): @pytest.fixture def multipolygon_generator(): + """Generator for multi complex polygons. + Usage: mpoly=generator(n, max_per_multi) + """ rstate = np.random.RandomState(0) def generator(n, max_per_multi): From 8691e5eac46517bb5879f1eba71d818bcf3be8ce Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:35:53 -0600 Subject: [PATCH 58/90] Reorder got/expected. --- python/cuspatial/cuspatial/tests/test_contains.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 5355c4c77..e3486b1ab 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -24,11 +24,11 @@ def test_manual_polygons(): ) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - expected = gpdlhs.contains(gpdrhs).values got = lhs.contains(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values assert (got == expected).all() - expected = gpdrhs.contains(gpdlhs).values got = rhs.contains(lhs).values_host + expected = gpdrhs.contains(gpdlhs).values assert (got == expected).all() @@ -45,8 +45,8 @@ def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - expected = gpdpolygon.contains(gpdlinestring).values got = polygons.contains(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -82,8 +82,8 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - expected = gpdpolygon.contains(gpdlinestring).values got = polygons.contains(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -119,8 +119,8 @@ def test_float_precision_limits_failures(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - expected = gpdpolygon.contains(gpdpoint).values got = polygon.contains(point).values_host + expected = gpdpolygon.contains(gpdpoint).values assert got == expected assert got.values_host[0] == expects From 11393167a2367f40c205d812c64bb515dc1ba21b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:39:21 -0600 Subject: [PATCH 59/90] Update one more test description. --- python/cuspatial/cuspatial/tests/test_contains.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index e3486b1ab..e7dc466a0 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -96,7 +96,11 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( results because they implement `contains` and we implement `contains_properly`. Detailed documentation of this in https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# - hasn't been completed yet.""" + hasn't been completed yet. + + The below test_float_precision_limits pairs with this test and shows + the inconsistency. + """ ) @pytest.mark.parametrize( "point, polygon, expects", @@ -128,8 +132,11 @@ def test_float_precision_limits_failures(point, polygon, expects): @pytest.mark.parametrize( "point, polygon, expects", [ - # unique failure cases identified by @mharris - [ + """Unique success cases identified by @mharris. These go in a pair + with test_float_precision_limits_failures because these are + inconsistent results, where 0.6 fails above (as True, within the + polygon) and 0.66 below succeeds, though they are colinear. + """[ Point([0.66, 0.006]), Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), False, From e62aa9f6ce9e08835c0be31cf54c464bc2432187 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 12:59:11 -0600 Subject: [PATCH 60/90] Create .contains examples. --- .../cuspatial/core/binops/contains.py | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index e9f4396fc..79326cb0e 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -25,6 +25,13 @@ def contains( closed polygons: the first and last coordinate of each polygon must be the same. + This function is pairwise, or many-to-one. + + This implements `.contains_properly`, which shares a large + space of correct cases with `GeoPandas.contains` but they do not produce + identical results. In the future we will use intersection testing to + match .contains behavior. + Parameters ---------- test_points_x @@ -43,9 +50,53 @@ def contains( Examples -------- + Test if a polygon is inside another polygon: + + >>> gpdpoint = gpd.GeoSeries( + [Point(0.5, 0.5)], + ) + >>> gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + ] + ) + >>> point = cuspatial.from_geopandas(gpdpoint) + >>> polygon = cuspatial.from_geopandas(gpdpolygon) + >>> print(polygon.contains(point)) + 0 False + dtype: bool + + Test whether 3 points fall within either of two polygons - # TODO: Examples + >>> gpdpoint = gpd.GeoSeries( + [Point(0, 0)], + [Point(0, 0)], + [Point(0, 0)], + [Point(-1, 1)], + [Point(-1, 1)], + [Point(-1, 1)], + ) + >>> gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + ] + ) + >>> point = cuspatial.from_geopandas(gpdpoint) + >>> polygon = cuspatial.from_geopandas(gpdpolygon) + >>> print(polygon.contains(point)) + 0 False + 1 False + 2 False + 3 True + 4 True + 5 True + dtype: bool note input Series x and y will not be index aligned, but computed as From 22b9241f63377eb9f4c0dcdc4a3fb60d33aaab27 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 13:16:27 -0600 Subject: [PATCH 61/90] Move .contains docs to geoseries.py --- .../cuspatial/core/binops/contains.py | 95 ------------------- python/cuspatial/cuspatial/core/geoseries.py | 92 ++++++++++++++++++ 2 files changed, 92 insertions(+), 95 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 79326cb0e..aa19e107a 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -20,101 +20,6 @@ def contains( poly_points_x, poly_points_y, ): - """Compute from a set of points and a set of polygons which points fall - within each polygon. Note that `polygons_(x,y)` must be specified as - closed polygons: the first and last coordinate of each polygon must be - the same. - - This function is pairwise, or many-to-one. - - This implements `.contains_properly`, which shares a large - space of correct cases with `GeoPandas.contains` but they do not produce - identical results. In the future we will use intersection testing to - match .contains behavior. - - Parameters - ---------- - test_points_x - x-coordinate of test points - test_points_y - y-coordinate of test points - poly_offsets - beginning index of the first ring in each polygon - poly_ring_offsets - beginning index of the first point in each ring - poly_points_x - x closed-coordinate of polygon points - poly_points_y - y closed-coordinate of polygon points - - Examples - -------- - - Test if a polygon is inside another polygon: - - >>> gpdpoint = gpd.GeoSeries( - [Point(0.5, 0.5)], - ) - >>> gpdpolygon = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - ] - ) - >>> point = cuspatial.from_geopandas(gpdpoint) - >>> polygon = cuspatial.from_geopandas(gpdpolygon) - >>> print(polygon.contains(point)) - 0 False - dtype: bool - - - Test whether 3 points fall within either of two polygons - - >>> gpdpoint = gpd.GeoSeries( - [Point(0, 0)], - [Point(0, 0)], - [Point(0, 0)], - [Point(-1, 1)], - [Point(-1, 1)], - [Point(-1, 1)], - ) - >>> gpdpolygon = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), - Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), - Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), - ] - ) - >>> point = cuspatial.from_geopandas(gpdpoint) - >>> polygon = cuspatial.from_geopandas(gpdpolygon) - >>> print(polygon.contains(point)) - 0 False - 1 False - 2 False - 3 True - 4 True - 5 True - dtype: bool - - note - input Series x and y will not be index aligned, but computed as - sequential arrays. - - note - poly_ring_offsets must contain only the rings that make up the polygons - indexed by poly_offsets. If there are rings in poly_ring_offsets that - are not part of the polygons in poly_offsets, results are likely to be - incorrect and behavior is undefined. - - Returns - ------- - result : cudf.DataFrame - A DataFrame of boolean values indicating whether each point falls - within each polygon. - """ - if len(poly_offsets) == 0: return Series() ( diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 7881f0cb0..c4dc448c3 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -565,6 +565,98 @@ def to_arrow(self): ) def contains(self, other, align=True): + """Compute from a set of points and a set of polygons which points fall + within each polygon. Note that `polygons_(x,y)` must be specified as + closed polygons: the first and last coordinate of each polygon must be + the same. + + This implements `.contains_properly`, which shares a large + space of correct cases with `GeoPandas.contains` but they do not + produce identical results. In the future we will use intersection + testing to match .contains behavior. + + Parameters + ---------- + test_points_x + x-coordinate of test points + test_points_y + y-coordinate of test points + poly_offsets + beginning index of the first ring in each polygon + poly_ring_offsets + beginning index of the first point in each ring + poly_points_x + x closed-coordinate of polygon points + poly_points_y + y closed-coordinate of polygon points + + Examples + -------- + + Test if a polygon is inside another polygon: + >>> gpdpoint = gpd.GeoSeries( + [Point(0.5, 0.5)], + ) + >>> gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + ] + ) + >>> point = cuspatial.from_geopandas(gpdpoint) + >>> polygon = cuspatial.from_geopandas(gpdpolygon) + >>> print(polygon.contains(point)) + 0 False + dtype: bool + + + Test whether 3 points fall within either of two polygons + >>> gpdpoint = gpd.GeoSeries( + [Point(0, 0)], + [Point(0, 0)], + [Point(0, 0)], + [Point(-1, 1)], + [Point(-1, 1)], + [Point(-1, 1)], + ) + >>> gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + ] + ) + >>> point = cuspatial.from_geopandas(gpdpoint) + >>> polygon = cuspatial.from_geopandas(gpdpolygon) + >>> print(polygon.contains(point)) + 0 False + 1 False + 2 False + 3 True + 4 True + 5 True + dtype: bool + + Note + ---- + input Series x and y will not be index aligned, but computed as + sequential arrays. + + Note + ---- + poly_ring_offsets must contain only the rings that make up the polygons + indexed by poly_offsets. If there are rings in poly_ring_offsets that + are not part of the polygons in poly_offsets, results are likely to be + incorrect and behavior is undefined. + + Returns + ------- + result : cudf.DataFrame + A DataFrame of boolean values indicating whether each point falls + within each polygon. + """ if contains_only_polygons(self) is False: raise TypeError("left series contains non-polygons.") From 597d932074aeeaeac19177f9b8b67d43ed3acce3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 13:17:57 -0600 Subject: [PATCH 62/90] Remove binops with quadtree. --- .../core/binops/binops_with_quadtree.py | 126 ------------------ 1 file changed, 126 deletions(-) delete mode 100644 python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py diff --git a/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py b/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py deleted file mode 100644 index ed5861158..000000000 --- a/python/cuspatial/cuspatial/core/binops/binops_with_quadtree.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. - -from cudf import Series -from cudf.core.column import as_column - -import cuspatial -from cuspatial._lib.point_in_polygon import ( - point_in_polygon as cpp_point_in_polygon, -) -from cuspatial.utils.column_utils import normalize_point_columns - - -def contains( - test_points_x, - test_points_y, - poly_offsets, - poly_ring_offsets, - poly_points_x, - poly_points_y, -): - """Compute from a set of points and a set of polygons which points fall - within each polygon. Note that `polygons_(x,y)` must be specified as - closed polygons: the first and last coordinate of each polygon must be - the same. - - Parameters - ---------- - test_points_x - x-coordinate of test points - test_points_y - y-coordinate of test points - poly_offsets - beginning index of the first ring in each polygon - poly_ring_offsets - beginning index of the first point in each ring - poly_points_x - x closed-coordinate of polygon points - poly_points_y - y closed-coordinate of polygon points - - Examples - -------- - - Test whether 3 points fall within either of two polygons - - # TODO: Examples - - note - input Series x and y will not be index aligned, but computed as - sequential arrays. - - note - poly_ring_offsets must contain only the rings that make up the polygons - indexed by poly_offsets. If there are rings in poly_ring_offsets that - are not part of the polygons in poly_offsets, results are likely to be - incorrect and behavior is undefined. - - Returns - ------- - result : cudf.DataFrame - A DataFrame of boolean values indicating whether each point falls - within each polygon. - """ - - if len(poly_offsets) == 0: - return Series() - ( - test_points_x, - test_points_y, - poly_points_x, - poly_points_y, - ) = normalize_point_columns( - as_column(test_points_x), - as_column(test_points_y), - as_column(poly_points_x), - as_column(poly_points_y), - ) - - QUADTREE = False - if QUADTREE: - scale = 5 - max_depth = 7 - min_size = 125 - x_max = poly_points_x.max() - x_min = poly_points_x.min() - y_max = poly_points_y.max() - y_min = poly_points_y.min() - point_indices, quadtree = cuspatial.quadtree_on_points( - test_points_x, - test_points_y, - x_min, - x_max, - y_min, - y_max, - scale, - max_depth, - min_size, - ) - poly_bboxes = cuspatial.polygon_bounding_boxes( - poly_offsets, poly_ring_offsets, poly_points_x, poly_points_y - ) - intersections = cuspatial.join_quadtree_and_bounding_boxes( - quadtree, poly_bboxes, x_min, x_max, y_min, y_max, scale, max_depth - ) - polygons_and_points = cuspatial.quadtree_point_in_polygon( - intersections, - quadtree, - point_indices, - test_points_x, - test_points_y, - poly_offsets, - poly_ring_offsets, - poly_points_x, - poly_points_y, - ) - return polygons_and_points - else: - pip_result = cpp_point_in_polygon( - test_points_x, - test_points_y, - as_column(poly_offsets, dtype="int32"), - as_column(poly_ring_offsets, dtype="int32"), - poly_points_x, - poly_points_y, - ) - return Series(pip_result, dtype="bool") From 70a4991a39621f62ebdea2a4a52041be9e18224b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 13:47:10 -0600 Subject: [PATCH 63/90] Fix test issue with sporadic groupby reordering. --- python/cuspatial/cuspatial/core/geoseries.py | 3 ++- python/cuspatial/cuspatial/tests/test_contains.py | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index c4dc448c3..fa6d32e6f 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -702,7 +702,8 @@ def contains(self, other, align=True): {"idx": point_indices, "pip": point_result} ) df_result = ( - result.groupby("idx").sum() == result.groupby("idx").count() + result.groupby("idx").sum().sort_index() + == result.groupby("idx").count().sort_index() ).sort_index() point_result = cudf.Series( df_result["pip"], index=cudf.RangeIndex(0, len(df_result)) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index e7dc466a0..30366bd29 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -132,11 +132,7 @@ def test_float_precision_limits_failures(point, polygon, expects): @pytest.mark.parametrize( "point, polygon, expects", [ - """Unique success cases identified by @mharris. These go in a pair - with test_float_precision_limits_failures because these are - inconsistent results, where 0.6 fails above (as True, within the - polygon) and 0.66 below succeeds, though they are colinear. - """[ + [ Point([0.66, 0.006]), Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), False, @@ -150,6 +146,11 @@ def test_float_precision_limits_failures(point, polygon, expects): ], ) def test_float_precision_limits(point, polygon, expects): + """Unique success cases identified by @mharris. These go in a pair + with test_float_precision_limits_failures because these are + inconsistent results, where 0.6 fails above (as True, within the + polygon) and 0.66 below succeeds, though they are colinear. + """ gpdpoint = gpd.GeoSeries(point) gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) From 104912d4b6202d2589ff5bd178c8c25d7a1cf50c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 13:48:59 -0600 Subject: [PATCH 64/90] Fix benchmark regression from 22.12 --- python/cuspatial/benchmarks/api/bench_api.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/cuspatial/benchmarks/api/bench_api.py b/python/cuspatial/benchmarks/api/bench_api.py index 126e9c249..d254e3333 100644 --- a/python/cuspatial/benchmarks/api/bench_api.py +++ b/python/cuspatial/benchmarks/api/bench_api.py @@ -118,12 +118,8 @@ def bench_pairwise_linestring_distance(benchmark, gpu_dataframe): geometry = gpu_dataframe["geometry"] benchmark( cuspatial.pairwise_linestring_distance, - geometry.polygons.ring_offset, - geometry.polygons.x, - geometry.polygons.y, - geometry.polygons.ring_offset, - geometry.polygons.x, - geometry.polygons.y, + geometry, + geometry, ) From fee62b9d84b3ff941a146c56f0bc30c129d2032f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 14:05:40 -0600 Subject: [PATCH 65/90] Fix bug with benchmark point_in_polygon --- python/cuspatial/benchmarks/api/bench_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/benchmarks/api/bench_api.py b/python/cuspatial/benchmarks/api/bench_api.py index d254e3333..b7945a383 100644 --- a/python/cuspatial/benchmarks/api/bench_api.py +++ b/python/cuspatial/benchmarks/api/bench_api.py @@ -263,11 +263,12 @@ def bench_point_in_polygon(benchmark, gpu_dataframe): y_points = (cupy.random.random(50000000) - 0.5) * 180 short_dataframe = gpu_dataframe.iloc[0:32] geometry = short_dataframe["geometry"] + polygon_offset = cudf.Series(geometry.polygons.geometry_offset[0:31]) benchmark( cuspatial.point_in_polygon, x_points, y_points, - geometry.polygons.geometry_offset[0:31], + polygon_offset, geometry.polygons.ring_offset, geometry.polygons.x, geometry.polygons.y, From 0b1dc7956cab43a07d06cef4e898fe7a8ac291f3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:08:07 -0600 Subject: [PATCH 66/90] Poly offsets is n+1 the size of points. Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/core/binops/contains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index aa19e107a..27db8bfe2 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -34,7 +34,7 @@ def contains( as_column(poly_points_y), ) - if len(test_points_x) == len(poly_offsets): + if len(test_points_x) == len(poly_offsets)-1: pip_result = cpp_pairwise_point_in_polygon( test_points_x, test_points_y, From 166ecb3a26c3d6c30c201c7d25c3d5651409c6aa Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:16:09 -0600 Subject: [PATCH 67/90] Update python/cuspatial/cuspatial/tests/test_contains.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/tests/test_contains.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 30366bd29..0992cc4e9 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -146,6 +146,7 @@ def test_float_precision_limits_failures(point, polygon, expects): ], ) def test_float_precision_limits(point, polygon, expects): + """Corner case to test point on edges with floating point precision limits.""" """Unique success cases identified by @mharris. These go in a pair with test_float_precision_limits_failures because these are inconsistent results, where 0.6 fails above (as True, within the From 9e8892aa2bfc8cab3d11a394217c85a33f14d3fe Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:20:25 -0600 Subject: [PATCH 68/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index fa6d32e6f..2cb939ad6 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -166,7 +166,7 @@ def _get_current_features(self, type): def point_indices(self): # Points only case - offsets = cp.arange(0, len(self.x) * 2 + 1, 2) + offsets = cp.arange(0, len(self.xy) + 1, 2) sizes = offsets[1:] - offsets[:-1] return cp.repeat(self._series.index, sizes) From 166c5cfda7e9f552fce41949dec5a679b0995b17 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:26:13 -0600 Subject: [PATCH 69/90] Update python/cuspatial/cuspatial/tests/conftest.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index aa35cd1ac..cbeb585b7 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -216,7 +216,7 @@ def generator(n, max_num_geometries, max_num_segments): @pytest.fixture def simple_polygon_generator(): """Generator for polygons with no interior ring. - Usage: poly=generator(n, distance_from_origin, radius) + Usage: polygon_generator(n, distance_from_origin, radius) """ rstate = np.random.RandomState(0) From e92686d01d6e452d534eab06180e7ad930f4a36b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:26:33 -0600 Subject: [PATCH 70/90] Update python/cuspatial/cuspatial/tests/conftest.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index cbeb585b7..77c3f166c 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -259,7 +259,7 @@ def generator(n, distance_from_origin, radius=1.0): @pytest.fixture def multipolygon_generator(): """Generator for multi complex polygons. - Usage: mpoly=generator(n, max_per_multi) + Usage: multipolygon_generator(n, max_per_multi) """ rstate = np.random.RandomState(0) From 88b864d04ea866b9acd475afabdb4f24bced4985 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:32:37 -0600 Subject: [PATCH 71/90] DRY-ify a function. --- python/cuspatial/cuspatial/core/binops/contains.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 27db8bfe2..b1116e432 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -33,13 +33,15 @@ def contains( as_column(poly_points_x), as_column(poly_points_y), ) + poly_offsets_column = (as_column(poly_offsets, dtype="int32"),) + poly_ring_offsets_column = (as_column(poly_ring_offsets, dtype="int32"),) - if len(test_points_x) == len(poly_offsets)-1: + if len(test_points_x) == len(poly_offsets) - 1: pip_result = cpp_pairwise_point_in_polygon( test_points_x, test_points_y, - as_column(poly_offsets, dtype="int32"), - as_column(poly_ring_offsets, dtype="int32"), + poly_offsets_column, + poly_ring_offsets_column, poly_points_x, poly_points_y, ) @@ -47,8 +49,8 @@ def contains( pip_result = cpp_point_in_polygon( test_points_x, test_points_y, - as_column(poly_offsets, dtype="int32"), - as_column(poly_ring_offsets, dtype="int32"), + poly_offsets_column, + poly_ring_offsets_column, poly_points_x, poly_points_y, ) From 61c48e2a613f4f6314815752d6f84620ccf18ad0 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 16:57:07 -0600 Subject: [PATCH 72/90] Handling @isVoid's comments. --- .../cuspatial/core/binops/contains.py | 37 ++++++++++++++++-- python/cuspatial/cuspatial/core/geoseries.py | 38 +++++++++---------- python/cuspatial/cuspatial/tests/conftest.py | 10 ++++- .../cuspatial/tests/test_contains.py | 24 ++++-------- 4 files changed, 68 insertions(+), 41 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index b1116e432..6d3ad6298 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -20,6 +20,37 @@ def contains( poly_points_x, poly_points_y, ): + """Compute from a series of points and a series of polygons which points + fall within each polygon. Note that polygons must be closed: the first and + last coordinate of each polygon must be the same. + + This implements `.contains_properly`, which shares a large + space of correct cases with `GeoPandas.contains` but they do not + produce identical results. In the future we will use intersection + testing to match .contains behavior. + + Parameters + ---------- + test_points_x + x-coordinate of test points + test_points_y + y-coordinate of test points + poly_offsets + beginning index of the first ring in each polygon + poly_ring_offsets + beginning index of the first point in each ring + poly_points_x + x closed-coordinate of polygon points + poly_points_y + y closed-coordinate of polygon points + + Returns + ------- + result : cudf.Series + A Series of boolean values indicating whether each point falls + within each polygon. + """ + if len(poly_offsets) == 0: return Series() ( @@ -33,10 +64,10 @@ def contains( as_column(poly_points_x), as_column(poly_points_y), ) - poly_offsets_column = (as_column(poly_offsets, dtype="int32"),) - poly_ring_offsets_column = (as_column(poly_ring_offsets, dtype="int32"),) + poly_offsets_column = as_column(poly_offsets, dtype="int32") + poly_ring_offsets_column = as_column(poly_ring_offsets, dtype="int32") - if len(test_points_x) == len(poly_offsets) - 1: + if len(test_points_x) == len(poly_offsets): pip_result = cpp_pairwise_point_in_polygon( test_points_x, test_points_y, diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index b73908d9c..0a88bdde9 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -166,7 +166,8 @@ def _get_current_features(self, type): return existing_features def point_indices(self): - # Points only case + # Return a cupy.ndarray containing the index values that each + # point belongs to. offsets = cp.arange(0, len(self.xy) + 1, 2) sizes = offsets[1:] - offsets[:-1] return cp.repeat(self._series.index, sizes) @@ -181,6 +182,8 @@ def geometry_offset(self): return self._get_current_features(self._type).offsets.values def point_indices(self): + # Return a cupy.ndarray containing the index values from the + # MultiPoint GeoSeries that each individual point is member of. offsets = cp.array(self.geometry_offset) sizes = offsets[1:] - offsets[:-1] return cp.repeat(self._series.index, sizes) @@ -201,6 +204,8 @@ def part_offset(self): ).elements.offsets.values def point_indices(self): + # Return a cupy.ndarray containing the index values from the + # LineString GeoSeries that each individual point is member of. offsets = cp.array(self.part_offset) sizes = offsets[1:] - offsets[:-1] return cp.repeat(self._series.index, sizes) @@ -227,6 +232,8 @@ def ring_offset(self): ).elements.elements.offsets.values def point_indices(self): + # Return a cupy.ndarray containing the index values from the + # Polygon GeoSeries that each individual point is member of. offsets = cp.array(self.ring_offset) sizes = offsets[1:] - offsets[:-1] return cp.repeat(self._series.index, sizes) @@ -644,10 +651,9 @@ def _gather( return self.iloc[gather_map] def contains(self, other, align=True): - """Compute from a set of points and a set of polygons which points fall - within each polygon. Note that `polygons_(x,y)` must be specified as - closed polygons: the first and last coordinate of each polygon must be - the same. + """Compute from a series of points and a series of polygons which + points fall within each polygon. Note that polygons must be closed: + the first and last coordinate of each polygon must be the same. This implements `.contains_properly`, which shares a large space of correct cases with `GeoPandas.contains` but they do not @@ -656,18 +662,12 @@ def contains(self, other, align=True): Parameters ---------- - test_points_x - x-coordinate of test points - test_points_y - y-coordinate of test points - poly_offsets - beginning index of the first ring in each polygon - poly_ring_offsets - beginning index of the first point in each ring - poly_points_x - x closed-coordinate of polygon points - poly_points_y - y closed-coordinate of polygon points + other + a cuspatial.GeoSeries + align=True + to align the indices before computing .contains or not. If the + indices are not aligned, they will be compared based on their + implicit row order. Examples -------- @@ -732,8 +732,8 @@ def contains(self, other, align=True): Returns ------- - result : cudf.DataFrame - A DataFrame of boolean values indicating whether each point falls + result : cudf.Series + A Series of boolean values indicating whether each point falls within each polygon. """ if contains_only_polygons(self) is False: diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 77c3f166c..08fd259a2 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -263,10 +263,16 @@ def multipolygon_generator(): """ rstate = np.random.RandomState(0) - def generator(n, max_per_multi): + def generator(n, max_per_multi, distance_from_origin, radius): for _ in range(n): num_polygons = rstate.randint(1, max_per_multi) - yield MultiPolygon([*polygon_generator(0, num_polygons)]) + yield MultiPolygon( + [ + *polygon_generator( + num_polygons, distance_from_origin, radius + ) + ] + ) return generator diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 0992cc4e9..59a8bd127 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -5,11 +5,7 @@ import cuspatial -@pytest.mark.xfail( - reason="""The polygons share numerous boundaries. See - https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# - for examples of each case.""" -) +@pytest.mark.xfail(reason="The polygons share numerous boundaries.") def test_manual_polygons(): gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) gpdrhs = gpd.GeoSeries( @@ -33,10 +29,7 @@ def test_manual_polygons(): @pytest.mark.xfail( - reason="""The linestring is colinear with one of the polygon - edges. It is excluded from `.contains_properly`. See - https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# - for detailed examples.""" + reason="The linestring is colinear with one of the polygon edges." ) def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) @@ -52,9 +45,7 @@ def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): @pytest.mark.xfail( reason="""The linestring has boundaries inside of the polygon, and crosses - over a single inner ring. See - https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# - for detailed examples.""" + over a single inner ring.""" ) def test_one_polygon_with_hole_one_linestring_crossing_it( linestring_generator, @@ -94,9 +85,7 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( geometry engine. It is possible that implementing `.contains_properly` would correct this error. These boundary cases conflict with GeoPandas results because they implement `contains` and we implement - `contains_properly`. Detailed documentation of this in - https://docs.google.com/document/d/1akxcRcUVK-qv5puK-mSTiKDKUS6UlR6Ccnr9_bJ4fi8/edit?pli=1# - hasn't been completed yet. + `contains_properly`. The below test_float_precision_limits pairs with this test and shows the inconsistency. @@ -146,8 +135,9 @@ def test_float_precision_limits_failures(point, polygon, expects): ], ) def test_float_precision_limits(point, polygon, expects): - """Corner case to test point on edges with floating point precision limits.""" - """Unique success cases identified by @mharris. These go in a pair + """Corner case to test point on edges with floating point precision + limits. + Unique success cases identified by @mharris. These go in a pair with test_float_precision_limits_failures because these are inconsistent results, where 0.6 fails above (as True, within the polygon) and 0.66 below succeeds, though they are colinear. From 903e0ae0f074348aac9145e8f63b80451a1c92cc Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:04:29 -0600 Subject: [PATCH 73/90] Rename to contains_properly --- .../cuspatial/core/binops/contains.py | 2 +- python/cuspatial/cuspatial/core/geoseries.py | 6 +-- .../cuspatial/tests/test_contains.py | 40 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 6d3ad6298..af5a19893 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -12,7 +12,7 @@ from cuspatial.utils.column_utils import normalize_point_columns -def contains( +def contains_properly( test_points_x, test_points_y, poly_offsets, diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 0a88bdde9..d736612d4 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -25,7 +25,7 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.binops.contains import contains +from cuspatial.core.binops.contains import contains_properly from cuspatial.utils.column_utils import ( contains_only_linestrings, contains_only_multipoints, @@ -650,7 +650,7 @@ def _gather( ): return self.iloc[gather_map] - def contains(self, other, align=True): + def contains_properly(self, other, align=True): """Compute from a series of points and a series of polygons which points fall within each polygon. Note that polygons must be closed: the first and last coordinate of each polygon must be the same. @@ -762,7 +762,7 @@ def contains(self, other, align=True): points = GeoSeries(GeoColumn._from_points_xy(xy_points._column)).points # call pip on the three subtypes on the right: - point_result = contains( + point_result = contains_properly( points.x, points.y, self.polygons.part_offset[:-1], diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py index 59a8bd127..5da281869 100644 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ b/python/cuspatial/cuspatial/tests/test_contains.py @@ -20,10 +20,10 @@ def test_manual_polygons(): ) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains(rhs).values_host + got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values assert (got == expected).all() - got = rhs.contains(lhs).values_host + got = rhs.contains_properly(lhs).values_host expected = gpdrhs.contains(gpdlhs).values assert (got == expected).all() @@ -38,7 +38,7 @@ def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -73,7 +73,7 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -112,7 +112,7 @@ def test_float_precision_limits_failures(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains(point).values_host + got = polygon.contains_properly(point).values_host expected = gpdpolygon.contains(gpdpoint).values assert got == expected assert got.values_host[0] == expects @@ -146,7 +146,7 @@ def test_float_precision_limits(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains(point).values_host + got = polygon.contains_properly(point).values_host expected = gpdpolygon.contains(gpdpoint).values assert got == expected assert got[0] == expects @@ -196,7 +196,7 @@ def test_point_in_polygon(point, polygon, expects): gpdpolygon = gpd.GeoSeries(polygon) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains(point).values_host + got = polygon.contains_properly(point).values_host expected = gpdpolygon.contains(gpdpoint).values assert got == expected assert got[0] == expects @@ -207,7 +207,7 @@ def test_two_points_one_polygon(): gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains(point).values_host + got = polygon.contains_properly(point).values_host expected = gpdpolygon.contains(gpdpoint).values assert (got == expected).all() @@ -222,7 +222,7 @@ def test_one_point_two_polygons(): ) point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains(point).values_host + got = polygon.contains_properly(point).values_host expected = gpdpolygon.contains(gpdpoint).values assert (got == expected).all() @@ -232,7 +232,7 @@ def test_ten_pair_points(point_generator, polygon_generator): gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) points = cuspatial.from_geopandas(gpdpoints) polygons = cuspatial.from_geopandas(gpdpolygons) - got = polygons.contains(points).values_host + got = polygons.contains_properly(points).values_host expected = gpdpolygons.contains(gpdpoints).values assert (got == expected).all() @@ -261,7 +261,7 @@ def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -273,7 +273,7 @@ def test_one_polygon_one_linestring(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -301,7 +301,7 @@ def test_six_polygons_six_linestrings(linestring_generator): ) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values assert (got == expected).all() @@ -311,7 +311,7 @@ def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): gpdpolygons = gpd.GeoSeries([*polygon_generator(31, 0)]) linestring = cuspatial.from_geopandas(gpdlinestring) polygons = cuspatial.from_geopandas(gpdpolygons) - got = polygons.contains(linestring).values_host + got = polygons.contains_properly(linestring).values_host expected = gpdpolygons.contains(gpdlinestring).values assert (got == expected).all() @@ -321,11 +321,11 @@ def test_one_polygon_one_polygon(polygon_generator): gpdrhs = gpd.GeoSeries([*polygon_generator(1, 0)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values - got = lhs.contains(rhs).values_host assert (expected == got).all() + got = rhs.contains_properly(lhs).values_host expected = gpdrhs.contains(gpdlhs).values - got = rhs.contains(lhs).values_host assert (got == expected).all() @@ -334,11 +334,11 @@ def test_max_polygons_max_polygons(simple_polygon_generator): gpdrhs = gpd.GeoSeries([*simple_polygon_generator(31, 1.49, 2)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values - got = lhs.contains(rhs).values_host assert (expected == got).all() + got = rhs.contains_properly(lhs).values_host expected = gpdrhs.contains(gpdlhs).values - got = rhs.contains(lhs).values_host assert (got == expected).all() @@ -347,8 +347,8 @@ def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): gpdrhs = gpd.GeoSeries([*multipoint_generator(1, 5)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values - got = lhs.contains(rhs).values_host assert (got == expected).all() @@ -357,6 +357,6 @@ def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): gpdrhs = gpd.GeoSeries([*multipoint_generator(31, 10)]) rhs = cuspatial.from_geopandas(gpdrhs) lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values - got = lhs.contains(rhs).values_host assert (got == expected).all() From dea2b969ae4891307e6b3b1a8317543319457d86 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:06:41 -0600 Subject: [PATCH 74/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris --- python/cuspatial/cuspatial/core/geoseries.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index b73908d9c..f36fbf5ec 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -649,10 +649,6 @@ def contains(self, other, align=True): closed polygons: the first and last coordinate of each polygon must be the same. - This implements `.contains_properly`, which shares a large - space of correct cases with `GeoPandas.contains` but they do not - produce identical results. In the future we will use intersection - testing to match .contains behavior. Parameters ---------- From ad308fb2e47fa53eb7218a4241109131d3e11d8b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:07:09 -0600 Subject: [PATCH 75/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index f36fbf5ec..36ac9b297 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -645,7 +645,7 @@ def _gather( def contains(self, other, align=True): """Compute from a set of points and a set of polygons which points fall - within each polygon. Note that `polygons_(x,y)` must be specified as + strictly inside each polygon. Points on polygon edges and vertices are not contained in the polygon. Note that `polygons_(x,y)` must be specified as closed polygons: the first and last coordinate of each polygon must be the same. From 0854a656ef0cc47d5ef429bf6ec5dca85e01eafc Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:08:29 -0600 Subject: [PATCH 76/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 36ac9b297..bb6b6b7c2 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -646,7 +646,7 @@ def _gather( def contains(self, other, align=True): """Compute from a set of points and a set of polygons which points fall strictly inside each polygon. Points on polygon edges and vertices are not contained in the polygon. Note that `polygons_(x,y)` must be specified as - closed polygons: the first and last coordinate of each polygon must be + closed polygons: the first and last vertices of each polygon must be the same. From cd6bd6632972e1ecebdc58fddcfcbcdc9e86722a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:28:42 -0600 Subject: [PATCH 77/90] Handle more review comments. --- .../cuspatial/core/binops/contains.py | 8 +- python/cuspatial/cuspatial/core/geoseries.py | 116 +++++++++--------- python/cuspatial/cuspatial/tests/conftest.py | 4 +- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index af5a19893..7c77388fb 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -32,17 +32,17 @@ def contains_properly( Parameters ---------- test_points_x - x-coordinate of test points + x-coordinate of points to test for containment test_points_y - y-coordinate of test points + y-coordinate of points to test for containment poly_offsets beginning index of the first ring in each polygon poly_ring_offsets beginning index of the first point in each ring poly_points_x - x closed-coordinate of polygon points + x-coordinates of polygon vertices poly_points_y - y closed-coordinate of polygon points + y-coordinates of polygon vertices Returns ------- diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index d736612d4..e2200d440 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -563,6 +563,45 @@ def to_arrow(self): ], ) + def _align_to_index( + self: T, + index: ColumnLike, + how: str = "outer", + sort: bool = True, + allow_non_unique: bool = False, + ) -> T: + """ + The values in the newly aligned columns will not change, + only positions in the union offsets and type codes. + """ + aligned_union_offsets = ( + self._column._meta.union_offsets._align_to_index( + index, how, sort, allow_non_unique + ) + ).astype("int32") + aligned_union_offsets[ + aligned_union_offsets.isna() + ] = Feature_Enum.NONE.value + aligned_input_types = self._column._meta.input_types._align_to_index( + index, how, sort, allow_non_unique + ).astype("int8") + aligned_input_types[ + aligned_input_types.isna() + ] = Feature_Enum.NONE.value + column = GeoColumn( + ( + self._column.points, + self._column.mpoints, + self._column.lines, + self._column.polygons, + ), + { + "input_types": aligned_input_types, + "union_offsets": aligned_union_offsets, + }, + ) + return GeoSeries(column) + def align(self, other): """ Align the rows of two GeoSeries using outer join. @@ -651,7 +690,7 @@ def _gather( return self.iloc[gather_map] def contains_properly(self, other, align=True): - """Compute from a series of points and a series of polygons which + """Compute from a GeoSeries of points and a GeoSeries of polygons which points fall within each polygon. Note that polygons must be closed: the first and last coordinate of each polygon must be the same. @@ -691,11 +730,11 @@ def contains_properly(self, other, align=True): Test whether 3 points fall within either of two polygons >>> gpdpoint = gpd.GeoSeries( [Point(0, 0)], - [Point(0, 0)], - [Point(0, 0)], - [Point(-1, 1)], - [Point(-1, 1)], + [Point(-1, 0)], + [Point(-2, 0)], + [Point(0, 1)], [Point(-1, 1)], + [Point(-2, 1)], ) >>> gpdpolygon = gpd.GeoSeries( [ @@ -715,7 +754,7 @@ def contains_properly(self, other, align=True): 2 False 3 True 4 True - 5 True + 5 False dtype: bool Note @@ -734,29 +773,31 @@ def contains_properly(self, other, align=True): ------- result : cudf.Series A Series of boolean values indicating whether each point falls - within each polygon. + within the corresponding polygon in the input. """ if contains_only_polygons(self) is False: raise TypeError("left series contains non-polygons.") + (lhs, rhs) = self.align(other) if align else (self, other) + # RHS conditioning: mode = "POINTS" # point in polygon - if contains_only_linestrings(other) is True: + if contains_only_linestrings(rhs) is True: # condition for linestrings mode = "LINESTRINGS" - xy = other.lines - elif contains_only_polygons(other) is True: + xy = rhs.lines + elif contains_only_polygons(rhs) is True: # polygon in polygon mode = "POLYGONS" - xy = other.polygons - elif contains_only_multipoints(other) is True: + xy = rhs.polygons + elif contains_only_multipoints(rhs) is True: # mpoint in polygon mode = "MULTIPOINTS" - xy = other.multipoints + xy = rhs.multipoints else: # no conditioning is required - xy = other.points + xy = rhs.points xy_points = xy.xy point_indices = xy.point_indices() points = GeoSeries(GeoColumn._from_points_xy(xy_points._column)).points @@ -765,10 +806,10 @@ def contains_properly(self, other, align=True): point_result = contains_properly( points.x, points.y, - self.polygons.part_offset[:-1], - self.polygons.ring_offset[:-1], - self.polygons.x, - self.polygons.y, + lhs.polygons.part_offset[:-1], + lhs.polygons.ring_offset[:-1], + lhs.polygons.x, + lhs.polygons.y, ) if ( mode == "LINESTRINGS" @@ -789,42 +830,3 @@ def contains_properly(self, other, align=True): ) point_result.name = None return point_result - - def _align_to_index( - self: T, - index: ColumnLike, - how: str = "outer", - sort: bool = True, - allow_non_unique: bool = False, - ) -> T: - """ - The values in the newly aligned columns will not change, - only positions in the union offsets and type codes. - """ - aligned_union_offsets = ( - self._column._meta.union_offsets._align_to_index( - index, how, sort, allow_non_unique - ) - ).astype("int32") - aligned_union_offsets[ - aligned_union_offsets.isna() - ] = Feature_Enum.NONE.value - aligned_input_types = self._column._meta.input_types._align_to_index( - index, how, sort, allow_non_unique - ).astype("int8") - aligned_input_types[ - aligned_input_types.isna() - ] = Feature_Enum.NONE.value - column = GeoColumn( - ( - self._column.points, - self._column.mpoints, - self._column.lines, - self._column.polygons, - ), - { - "input_types": aligned_input_types, - "union_offsets": aligned_union_offsets, - }, - ) - return GeoSeries(column) diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 08fd259a2..571d29091 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -232,8 +232,8 @@ def generator(n, distance_from_origin, radius=1.0): @pytest.fixture def polygon_generator(): """Generator for complex polygons. Each polygon will - have 1-4 randomly rotated interior polygons. Each polygon - is a circle, with very small inner ring polygons located in + have 1-4 randomly rotated interior rings. Each polygon + is a circle, with very small inner rings located in a spiral around its center. Usage: poly=generator(n, distance_from_origin, radius) """ From d72df1c9ac69be253c43aea668a8c17217db6332 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:30:23 -0600 Subject: [PATCH 78/90] Comment --- python/cuspatial/cuspatial/core/binops/contains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 7c77388fb..2df3b9e69 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -48,7 +48,7 @@ def contains_properly( ------- result : cudf.Series A Series of boolean values indicating whether each point falls - within each polygon. + within its corresponding polygon. """ if len(poly_offsets) == 0: From d08ad01c4da01691c6460dafda52e6c8cf0ba689 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 29 Nov 2022 17:42:08 -0600 Subject: [PATCH 79/90] Fix alignment. --- python/cuspatial/cuspatial/core/geoseries.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index d5defa07a..51d728eda 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -668,7 +668,9 @@ def align(self, other): dtype: geometry) """ - index = other.index + index = ( + other.index if len(other.index) >= len(self.index) else self.index + ) aligned_left = self._align_to_index(index) aligned_right = other._align_to_index(index) aligned_right.index = index From cb1b149fe92189bdd1df46ddea954a00b7defe09 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 08:38:51 -0600 Subject: [PATCH 80/90] Fix example layout again. --- python/cuspatial/cuspatial/core/geoseries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 51d728eda..89ccd1a35 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -729,9 +729,9 @@ def contains_properly(self, other, align=True): [Point(0, 0)], [Point(-1, 0)], [Point(-2, 0)], - [Point(0, 1)], - [Point(-1, 1)], - [Point(-2, 1)], + [Point(0, 0)], + [Point(-1, 0)], + [Point(-2, 0)], ) >>> gpdpolygon = gpd.GeoSeries( [ From 8db29b6dd9c966a97b6f0cf403b8094cd173c85d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 08:41:35 -0600 Subject: [PATCH 81/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 89ccd1a35..c7791f956 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -773,7 +773,7 @@ def contains_properly(self, other, align=True): within the corresponding polygon in the input. """ if contains_only_polygons(self) is False: - raise TypeError("left series contains non-polygons.") + raise TypeError("`.contains` can only be called with polygon series.") (lhs, rhs) = self.align(other) if align else (self, other) From f31b70e060385682f8e54467fa867bb590ce2f52 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 08:41:46 -0600 Subject: [PATCH 82/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index c7791f956..94a5474c7 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -772,7 +772,7 @@ def contains_properly(self, other, align=True): A Series of boolean values indicating whether each point falls within the corresponding polygon in the input. """ - if contains_only_polygons(self) is False: + if not contains_only_polygons(self): raise TypeError("`.contains` can only be called with polygon series.") (lhs, rhs) = self.align(other) if align else (self, other) From 80f5ea147232d705f18b706b1d3e3570af1a27a7 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 08:41:53 -0600 Subject: [PATCH 83/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Michael Wang --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 94a5474c7..ac472221e 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -780,7 +780,7 @@ def contains_properly(self, other, align=True): # RHS conditioning: mode = "POINTS" # point in polygon - if contains_only_linestrings(rhs) is True: + if contains_only_linestrings(rhs): # condition for linestrings mode = "LINESTRINGS" xy = rhs.lines From 4a594b9ad5a6516ec9dfd1f2fc125231cc49e311 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 08:49:12 -0600 Subject: [PATCH 84/90] Apply suggestions from code review Co-authored-by: Mark Harris --- python/cuspatial/cuspatial/core/binops/contains.py | 12 +++++------- python/cuspatial/cuspatial/core/geoseries.py | 11 +++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 2df3b9e69..7341b2386 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -21,13 +21,11 @@ def contains_properly( poly_points_y, ): """Compute from a series of points and a series of polygons which points - fall within each polygon. Note that polygons must be closed: the first and - last coordinate of each polygon must be the same. - - This implements `.contains_properly`, which shares a large - space of correct cases with `GeoPandas.contains` but they do not - produce identical results. In the future we will use intersection - testing to match .contains behavior. + are properly contained within the corresponding polygon. Polygon A contains Point B + properly if B intersects the interior of A but not the boundary (or exterior). + + Note that polygons must be closed: the first and last vertex of each + polygon must be the same. Parameters ---------- diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index ac472221e..26c56640a 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -692,9 +692,10 @@ def _gather( return self.iloc[gather_map] def contains_properly(self, other, align=True): - """Compute from a GeoSeries of points and a GeoSeries of polygons which - points fall within each polygon. Note that polygons must be closed: - the first and last coordinate of each polygon must be the same. + """Compute from a GeoSeries of points and a GeoSeries of polygons which + points are properly contained within the corresponding polygon. Polygon A + contains Point B properly if B intersects the interior of A but not the + boundary (or exterior). Parameters ---------- @@ -765,7 +766,9 @@ def contains_properly(self, other, align=True): indexed by poly_offsets. If there are rings in poly_ring_offsets that are not part of the polygons in poly_offsets, results are likely to be incorrect and behavior is undefined. - +Note +---- +Polygons must be closed: the first and last coordinate of each polygon must be the same. Returns ------- result : cudf.Series From 4a651f1cf66075fc8ae79a440a7e2262265d23f8 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 09:15:54 -0600 Subject: [PATCH 85/90] Get rid of GeoPandas round trip in .contains_properly example and fix docs once more. --- python/cuspatial/cuspatial/core/geoseries.py | 42 +- .../cuspatial/tests/test_contains.py | 362 ------------------ 2 files changed, 18 insertions(+), 386 deletions(-) delete mode 100644 python/cuspatial/cuspatial/tests/test_contains.py diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 89ccd1a35..8e5895a9d 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -709,23 +709,21 @@ def contains_properly(self, other, align=True): -------- Test if a polygon is inside another polygon: - >>> gpdpoint = gpd.GeoSeries( + >>> point = cuspatial.GeoSeries( [Point(0.5, 0.5)], ) - >>> gpdpolygon = gpd.GeoSeries( + >>> polygon = cuspatial.GeoSeries( [ Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), ] ) - >>> point = cuspatial.from_geopandas(gpdpoint) - >>> polygon = cuspatial.from_geopandas(gpdpolygon) >>> print(polygon.contains(point)) 0 False dtype: bool Test whether 3 points fall within either of two polygons - >>> gpdpoint = gpd.GeoSeries( + >>> point = cuspatial.GeoSeries( [Point(0, 0)], [Point(-1, 0)], [Point(-2, 0)], @@ -733,7 +731,7 @@ def contains_properly(self, other, align=True): [Point(-1, 0)], [Point(-2, 0)], ) - >>> gpdpolygon = gpd.GeoSeries( + >>> polygon = cuspatial.GeoSeries( [ Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), @@ -743,22 +741,15 @@ def contains_properly(self, other, align=True): Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), ] ) - >>> point = cuspatial.from_geopandas(gpdpoint) - >>> polygon = cuspatial.from_geopandas(gpdpolygon) >>> print(polygon.contains(point)) 0 False 1 False 2 False - 3 True - 4 True - 5 False + 3 False + 4 False + 5 True dtype: bool - Note - ---- - input Series x and y will not be index aligned, but computed as - sequential arrays. - Note ---- poly_ring_offsets must contain only the rings that make up the polygons @@ -772,7 +763,7 @@ def contains_properly(self, other, align=True): A Series of boolean values indicating whether each point falls within the corresponding polygon in the input. """ - if contains_only_polygons(self) is False: + if not contains_only_polygons(self): raise TypeError("left series contains non-polygons.") (lhs, rhs) = self.align(other) if align else (self, other) @@ -783,20 +774,20 @@ def contains_properly(self, other, align=True): if contains_only_linestrings(rhs) is True: # condition for linestrings mode = "LINESTRINGS" - xy = rhs.lines + geom = rhs.lines elif contains_only_polygons(rhs) is True: # polygon in polygon mode = "POLYGONS" - xy = rhs.polygons + geom = rhs.polygons elif contains_only_multipoints(rhs) is True: # mpoint in polygon mode = "MULTIPOINTS" - xy = rhs.multipoints + geom = rhs.multipoints else: # no conditioning is required - xy = rhs.points - xy_points = xy.xy - point_indices = xy.point_indices() + geom = rhs.points + xy_points = geom.xy + point_indices = geom.point_indices() points = GeoSeries(GeoColumn._from_points_xy(xy_points._column)).points # call pip on the three subtypes on the right: @@ -818,10 +809,13 @@ def contains_properly(self, other, align=True): result = cudf.DataFrame( {"idx": point_indices, "pip": point_result} ) + # if the number of points in the polygon is equal to the number of + # points, then the requirements for `.contains_properly` are met + # for this geometry type. df_result = ( result.groupby("idx").sum().sort_index() == result.groupby("idx").count().sort_index() - ).sort_index() + ) point_result = cudf.Series( df_result["pip"], index=cudf.RangeIndex(0, len(df_result)) ) diff --git a/python/cuspatial/cuspatial/tests/test_contains.py b/python/cuspatial/cuspatial/tests/test_contains.py deleted file mode 100644 index 5da281869..000000000 --- a/python/cuspatial/cuspatial/tests/test_contains.py +++ /dev/null @@ -1,362 +0,0 @@ -import geopandas as gpd -import pytest -from shapely.geometry import LineString, Point, Polygon - -import cuspatial - - -@pytest.mark.xfail(reason="The polygons share numerous boundaries.") -def test_manual_polygons(): - gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) - gpdrhs = gpd.GeoSeries( - [ - Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), - Polygon(((-2, -2), (-2, 2), (2, 2), (2, -2))), - Polygon(((-10, -2), (-10, 2), (-6, 2), (-6, -2))), - Polygon(((-2, 8), (-2, 12), (2, 12), (2, 8))), - Polygon(((6, 0), (8, 2), (10, 0), (8, -2))), - Polygon(((-2, -8), (-2, -4), (2, -4), (2, -8))), - ] - ) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains_properly(rhs).values_host - expected = gpdlhs.contains(gpdrhs).values - assert (got == expected).all() - got = rhs.contains_properly(lhs).values_host - expected = gpdrhs.contains(gpdlhs).values - assert (got == expected).all() - - -@pytest.mark.xfail( - reason="The linestring is colinear with one of the polygon edges." -) -def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): - gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() - - -@pytest.mark.xfail( - reason="""The linestring has boundaries inside of the polygon, and crosses - over a single inner ring.""" -) -def test_one_polygon_with_hole_one_linestring_crossing_it( - linestring_generator, -): - gpdlinestring = gpd.GeoSeries(LineString([[0.5, 2.0], [3.5, 2.0]])) - gpdpolygon = gpd.GeoSeries( - Polygon( - ( - [0, 0], - [0, 4], - [4, 4], - [4, 0], - [0, 0], - ), - [ - ( - [1, 1], - [1, 3], - [3, 3], - [3, 1], - [1, 1], - ) - ], - ) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() - - -@pytest.mark.xfail( - reason="""These points are on edges of their corresponding polygons. - Because of floating point error, they can have inconsistent results with - GeoPandas. In this case, GeoPandas results are incorrect due to their - geometry engine. It is possible that implementing `.contains_properly` - would correct this error. These boundary cases conflict with GeoPandas - results because they implement `contains` and we implement - `contains_properly`. - - The below test_float_precision_limits pairs with this test and shows - the inconsistency. - """ -) -@pytest.mark.parametrize( - "point, polygon, expects", - [ - [ - Point([0.6, 0.06]), - Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), - False, - ], - [ - Point([3.333, 1.111]), - Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), - True, - ], - [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], - ], -) -def test_float_precision_limits_failures(point, polygon, expects): - gpdpoint = gpd.GeoSeries(point) - gpdpolygon = gpd.GeoSeries(polygon) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert got == expected - assert got.values_host[0] == expects - - -@pytest.mark.parametrize( - "point, polygon, expects", - [ - [ - Point([0.66, 0.006]), - Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), - False, - ], - [ - Point([0.666, 0.0006]), - Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), - False, - ], - [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], - ], -) -def test_float_precision_limits(point, polygon, expects): - """Corner case to test point on edges with floating point precision - limits. - Unique success cases identified by @mharris. These go in a pair - with test_float_precision_limits_failures because these are - inconsistent results, where 0.6 fails above (as True, within the - polygon) and 0.66 below succeeds, though they are colinear. - """ - gpdpoint = gpd.GeoSeries(point) - gpdpolygon = gpd.GeoSeries(polygon) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert got == expected - assert got[0] == expects - - -clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) -clockwiseSquare = Polygon( - [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], [-0.5, -0.5]] -) - - -@pytest.mark.parametrize( - "point, polygon, expects", - [ - [Point([-0.5, -0.5]), clockwiseSquare, False], - [Point([-0.5, 0.5]), clockwiseSquare, False], - [Point([0.5, 0.5]), clockwiseSquare, False], - [Point([0.5, -0.5]), clockwiseSquare, False], - # clockwise square, should be true - [Point([-0.5, 0.0]), clockwiseSquare, False], - [Point([0.0, 0.5]), clockwiseSquare, False], - [Point([0.5, 0.0]), clockwiseSquare, False], - [Point([0.0, -0.5]), clockwiseSquare, False], - # wound clockwise, should be false - [Point([0, 0]), clockwiseTriangle, False], - [Point([0.0, 1.0]), clockwiseTriangle, False], - [Point([1.0, 1.0]), clockwiseTriangle, False], - [Point([0.0, 0.5]), clockwiseTriangle, False], - [Point([0.5, 0.5]), clockwiseTriangle, False], - [Point([0.5, 1]), clockwiseTriangle, False], - # wound clockwise, should be true - [Point([0.25, 0.5]), clockwiseTriangle, True], - [Point([0.75, 0.9]), clockwiseTriangle, True], - # wound counter clockwise, should be false - [Point([0.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([1.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([1.0, 1.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([0.5, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - [Point([0.5, 0.5]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], - # wound counter clockwise, should be true - [Point([0.5, 0.25]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], - [Point([0.9, 0.75]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], - ], -) -def test_point_in_polygon(point, polygon, expects): - gpdpoint = gpd.GeoSeries(point) - gpdpolygon = gpd.GeoSeries(polygon) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert got == expected - assert got[0] == expects - - -def test_two_points_one_polygon(): - gpdpoint = gpd.GeoSeries([Point(0, 0), Point(0, 0)]) - gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert (got == expected).all() - - -def test_one_point_two_polygons(): - gpdpoint = gpd.GeoSeries([Point(0, 0)]) - gpdpolygon = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), - Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), - ] - ) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert (got == expected).all() - - -def test_ten_pair_points(point_generator, polygon_generator): - gpdpoints = gpd.GeoSeries([*point_generator(10)]) - gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) - points = cuspatial.from_geopandas(gpdpoints) - polygons = cuspatial.from_geopandas(gpdpolygons) - got = polygons.contains_properly(points).values_host - expected = gpdpolygons.contains(gpdpoints).values - assert (got == expected).all() - - -def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): - gpdlinestring = gpd.GeoSeries(LineString([[1.5, 2.0], [2.5, 2.0]])) - gpdpolygon = gpd.GeoSeries( - Polygon( - ( - [0, 0], - [0, 4], - [4, 4], - [4, 0], - [0, 0], - ), - [ - ( - [1, 1], - [1, 3], - [3, 3], - [3, 1], - [1, 1], - ) - ], - ) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() - - -def test_one_polygon_one_linestring(linestring_generator): - gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 4)]) - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() - - -def test_six_polygons_six_linestrings(linestring_generator): - gpdlinestring = gpd.GeoSeries( - [ - LineString([[1.35, 0.35], [0.35, 0.65]]), - LineString([[0.35, 0.35], [0.35, 0.65]]), - LineString([[0.25, 0.25], [0.25, 0.75]]), - LineString([[0.15, 0.15], [0.15, 0.85]]), - LineString([[0.05, 0.05], [0.05, 0.95]]), - LineString([[0.05, 0.05], [1.05, 0.95]]), - ] - ) - gpdpolygon = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), - ] - ) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygon) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() - - -def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): - gpdlinestring = gpd.GeoSeries([*linestring_generator(31, 3)]) - gpdpolygons = gpd.GeoSeries([*polygon_generator(31, 0)]) - linestring = cuspatial.from_geopandas(gpdlinestring) - polygons = cuspatial.from_geopandas(gpdpolygons) - got = polygons.contains_properly(linestring).values_host - expected = gpdpolygons.contains(gpdlinestring).values - assert (got == expected).all() - - -def test_one_polygon_one_polygon(polygon_generator): - gpdlhs = gpd.GeoSeries(Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])) - gpdrhs = gpd.GeoSeries([*polygon_generator(1, 0)]) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains_properly(rhs).values_host - expected = gpdlhs.contains(gpdrhs).values - assert (expected == got).all() - got = rhs.contains_properly(lhs).values_host - expected = gpdrhs.contains(gpdlhs).values - assert (got == expected).all() - - -def test_max_polygons_max_polygons(simple_polygon_generator): - gpdlhs = gpd.GeoSeries([*simple_polygon_generator(31, 1, 3)]) - gpdrhs = gpd.GeoSeries([*simple_polygon_generator(31, 1.49, 2)]) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains_properly(rhs).values_host - expected = gpdlhs.contains(gpdrhs).values - assert (expected == got).all() - got = rhs.contains_properly(lhs).values_host - expected = gpdrhs.contains(gpdlhs).values - assert (got == expected).all() - - -def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): - gpdlhs = gpd.GeoSeries([*polygon_generator(1, 0)]) - gpdrhs = gpd.GeoSeries([*multipoint_generator(1, 5)]) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains_properly(rhs).values_host - expected = gpdlhs.contains(gpdrhs).values - assert (got == expected).all() - - -def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): - gpdlhs = gpd.GeoSeries([*polygon_generator(31, 0, 1)]) - gpdrhs = gpd.GeoSeries([*multipoint_generator(31, 10)]) - rhs = cuspatial.from_geopandas(gpdrhs) - lhs = cuspatial.from_geopandas(gpdlhs) - got = lhs.contains_properly(rhs).values_host - expected = gpdlhs.contains(gpdrhs).values - assert (got == expected).all() From 1c4f1bcf61a7eb486c1e131b8c2f14bc3bf61145 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 09:54:06 -0600 Subject: [PATCH 86/90] Style issue, black is hopefully not breaking flake8 in CI. --- python/cuspatial/cuspatial/core/binops/contains.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/contains.py b/python/cuspatial/cuspatial/core/binops/contains.py index 7341b2386..7fab07f76 100644 --- a/python/cuspatial/cuspatial/core/binops/contains.py +++ b/python/cuspatial/cuspatial/core/binops/contains.py @@ -21,9 +21,10 @@ def contains_properly( poly_points_y, ): """Compute from a series of points and a series of polygons which points - are properly contained within the corresponding polygon. Polygon A contains Point B - properly if B intersects the interior of A but not the boundary (or exterior). - + are properly contained within the corresponding polygon. Polygon A contains + Point B properly if B intersects the interior of A but not the boundary (or + exterior). + Note that polygons must be closed: the first and last vertex of each polygon must be the same. From d0075155c5370379ca2199e600945e522e35a8fd Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 13:56:21 -0600 Subject: [PATCH 87/90] Add __init__.py to binops hoping to resolve CI issue. --- python/cuspatial/cuspatial/core/binops/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 python/cuspatial/cuspatial/core/binops/__init__.py diff --git a/python/cuspatial/cuspatial/core/binops/__init__.py b/python/cuspatial/cuspatial/core/binops/__init__.py new file mode 100644 index 000000000..e69de29bb From 91c09af09e3ab852a19c4f9f0972f7469ae0bccd Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 14:35:19 -0600 Subject: [PATCH 88/90] Need test_contains_properly.py --- .../tests/binops/test_contains_properly.py | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 python/cuspatial/cuspatial/tests/binops/test_contains_properly.py diff --git a/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py b/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py new file mode 100644 index 000000000..5da281869 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py @@ -0,0 +1,362 @@ +import geopandas as gpd +import pytest +from shapely.geometry import LineString, Point, Polygon + +import cuspatial + + +@pytest.mark.xfail(reason="The polygons share numerous boundaries.") +def test_manual_polygons(): + gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) + gpdrhs = gpd.GeoSeries( + [ + Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), + Polygon(((-2, -2), (-2, 2), (2, 2), (2, -2))), + Polygon(((-10, -2), (-10, 2), (-6, 2), (-6, -2))), + Polygon(((-2, 8), (-2, 12), (2, 12), (2, 8))), + Polygon(((6, 0), (8, 2), (10, 0), (8, -2))), + Polygon(((-2, -8), (-2, -4), (2, -4), (2, -8))), + ] + ) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (got == expected).all() + got = rhs.contains_properly(lhs).values_host + expected = gpdrhs.contains(gpdlhs).values + assert (got == expected).all() + + +@pytest.mark.xfail( + reason="The linestring is colinear with one of the polygon edges." +) +def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): + gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) + gpdpolygon = gpd.GeoSeries( + Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() + + +@pytest.mark.xfail( + reason="""The linestring has boundaries inside of the polygon, and crosses + over a single inner ring.""" +) +def test_one_polygon_with_hole_one_linestring_crossing_it( + linestring_generator, +): + gpdlinestring = gpd.GeoSeries(LineString([[0.5, 2.0], [3.5, 2.0]])) + gpdpolygon = gpd.GeoSeries( + Polygon( + ( + [0, 0], + [0, 4], + [4, 4], + [4, 0], + [0, 0], + ), + [ + ( + [1, 1], + [1, 3], + [3, 3], + [3, 1], + [1, 1], + ) + ], + ) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() + + +@pytest.mark.xfail( + reason="""These points are on edges of their corresponding polygons. + Because of floating point error, they can have inconsistent results with + GeoPandas. In this case, GeoPandas results are incorrect due to their + geometry engine. It is possible that implementing `.contains_properly` + would correct this error. These boundary cases conflict with GeoPandas + results because they implement `contains` and we implement + `contains_properly`. + + The below test_float_precision_limits pairs with this test and shows + the inconsistency. + """ +) +@pytest.mark.parametrize( + "point, polygon, expects", + [ + [ + Point([0.6, 0.06]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [ + Point([3.333, 1.111]), + Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), + True, + ], + [Point([3.33, 1.11]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], + ], +) +def test_float_precision_limits_failures(point, polygon, expects): + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + got = polygon.contains_properly(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert got == expected + assert got.values_host[0] == expects + + +@pytest.mark.parametrize( + "point, polygon, expects", + [ + [ + Point([0.66, 0.006]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [ + Point([0.666, 0.0006]), + Polygon([[0, 0], [10, 1], [1, 1], [0, 0]]), + False, + ], + [Point([3.3, 1.1]), Polygon([[6, 2], [3, 1], [3, 4], [6, 2]]), True], + ], +) +def test_float_precision_limits(point, polygon, expects): + """Corner case to test point on edges with floating point precision + limits. + Unique success cases identified by @mharris. These go in a pair + with test_float_precision_limits_failures because these are + inconsistent results, where 0.6 fails above (as True, within the + polygon) and 0.66 below succeeds, though they are colinear. + """ + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + got = polygon.contains_properly(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert got == expected + assert got[0] == expects + + +clockwiseTriangle = Polygon([[0, 0], [0, 1], [1, 1], [0, 0]]) +clockwiseSquare = Polygon( + [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], [-0.5, -0.5]] +) + + +@pytest.mark.parametrize( + "point, polygon, expects", + [ + [Point([-0.5, -0.5]), clockwiseSquare, False], + [Point([-0.5, 0.5]), clockwiseSquare, False], + [Point([0.5, 0.5]), clockwiseSquare, False], + [Point([0.5, -0.5]), clockwiseSquare, False], + # clockwise square, should be true + [Point([-0.5, 0.0]), clockwiseSquare, False], + [Point([0.0, 0.5]), clockwiseSquare, False], + [Point([0.5, 0.0]), clockwiseSquare, False], + [Point([0.0, -0.5]), clockwiseSquare, False], + # wound clockwise, should be false + [Point([0, 0]), clockwiseTriangle, False], + [Point([0.0, 1.0]), clockwiseTriangle, False], + [Point([1.0, 1.0]), clockwiseTriangle, False], + [Point([0.0, 0.5]), clockwiseTriangle, False], + [Point([0.5, 0.5]), clockwiseTriangle, False], + [Point([0.5, 1]), clockwiseTriangle, False], + # wound clockwise, should be true + [Point([0.25, 0.5]), clockwiseTriangle, True], + [Point([0.75, 0.9]), clockwiseTriangle, True], + # wound counter clockwise, should be false + [Point([0.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([1.0, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([1.0, 1.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([0.5, 0.0]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + [Point([0.5, 0.5]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), False], + # wound counter clockwise, should be true + [Point([0.5, 0.25]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], + [Point([0.9, 0.75]), Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), True], + ], +) +def test_point_in_polygon(point, polygon, expects): + gpdpoint = gpd.GeoSeries(point) + gpdpolygon = gpd.GeoSeries(polygon) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + got = polygon.contains_properly(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert got == expected + assert got[0] == expects + + +def test_two_points_one_polygon(): + gpdpoint = gpd.GeoSeries([Point(0, 0), Point(0, 0)]) + gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + got = polygon.contains_properly(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert (got == expected).all() + + +def test_one_point_two_polygons(): + gpdpoint = gpd.GeoSeries([Point(0, 0)]) + gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 0]]), + Polygon([[-2, -2], [-2, 2], [2, 2], [-2, -2]]), + ] + ) + point = cuspatial.from_geopandas(gpdpoint) + polygon = cuspatial.from_geopandas(gpdpolygon) + got = polygon.contains_properly(point).values_host + expected = gpdpolygon.contains(gpdpoint).values + assert (got == expected).all() + + +def test_ten_pair_points(point_generator, polygon_generator): + gpdpoints = gpd.GeoSeries([*point_generator(10)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(10, 0)]) + points = cuspatial.from_geopandas(gpdpoints) + polygons = cuspatial.from_geopandas(gpdpolygons) + got = polygons.contains_properly(points).values_host + expected = gpdpolygons.contains(gpdpoints).values + assert (got == expected).all() + + +def test_one_polygon_with_hole_one_linestring_inside_it(linestring_generator): + gpdlinestring = gpd.GeoSeries(LineString([[1.5, 2.0], [2.5, 2.0]])) + gpdpolygon = gpd.GeoSeries( + Polygon( + ( + [0, 0], + [0, 4], + [4, 4], + [4, 0], + [0, 0], + ), + [ + ( + [1, 1], + [1, 3], + [3, 3], + [3, 1], + [1, 1], + ) + ], + ) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() + + +def test_one_polygon_one_linestring(linestring_generator): + gpdlinestring = gpd.GeoSeries([*linestring_generator(1, 4)]) + gpdpolygon = gpd.GeoSeries( + Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() + + +def test_six_polygons_six_linestrings(linestring_generator): + gpdlinestring = gpd.GeoSeries( + [ + LineString([[1.35, 0.35], [0.35, 0.65]]), + LineString([[0.35, 0.35], [0.35, 0.65]]), + LineString([[0.25, 0.25], [0.25, 0.75]]), + LineString([[0.15, 0.15], [0.15, 0.85]]), + LineString([[0.05, 0.05], [0.05, 0.95]]), + LineString([[0.05, 0.05], [1.05, 0.95]]), + ] + ) + gpdpolygon = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), + ] + ) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygon) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygon.contains(gpdlinestring).values + assert (got == expected).all() + + +def test_max_polygons_max_linestrings(linestring_generator, polygon_generator): + gpdlinestring = gpd.GeoSeries([*linestring_generator(31, 3)]) + gpdpolygons = gpd.GeoSeries([*polygon_generator(31, 0)]) + linestring = cuspatial.from_geopandas(gpdlinestring) + polygons = cuspatial.from_geopandas(gpdpolygons) + got = polygons.contains_properly(linestring).values_host + expected = gpdpolygons.contains(gpdlinestring).values + assert (got == expected).all() + + +def test_one_polygon_one_polygon(polygon_generator): + gpdlhs = gpd.GeoSeries(Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])) + gpdrhs = gpd.GeoSeries([*polygon_generator(1, 0)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (expected == got).all() + got = rhs.contains_properly(lhs).values_host + expected = gpdrhs.contains(gpdlhs).values + assert (got == expected).all() + + +def test_max_polygons_max_polygons(simple_polygon_generator): + gpdlhs = gpd.GeoSeries([*simple_polygon_generator(31, 1, 3)]) + gpdrhs = gpd.GeoSeries([*simple_polygon_generator(31, 1.49, 2)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (expected == got).all() + got = rhs.contains_properly(lhs).values_host + expected = gpdrhs.contains(gpdlhs).values + assert (got == expected).all() + + +def test_one_polygon_one_multipoint(multipoint_generator, polygon_generator): + gpdlhs = gpd.GeoSeries([*polygon_generator(1, 0)]) + gpdrhs = gpd.GeoSeries([*multipoint_generator(1, 5)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (got == expected).all() + + +def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): + gpdlhs = gpd.GeoSeries([*polygon_generator(31, 0, 1)]) + gpdrhs = gpd.GeoSeries([*multipoint_generator(31, 10)]) + rhs = cuspatial.from_geopandas(gpdrhs) + lhs = cuspatial.from_geopandas(gpdlhs) + got = lhs.contains_properly(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (got == expected).all() From 0d32a64ab85751ea2ef6191a81ec67880f635122 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 15:29:14 -0600 Subject: [PATCH 89/90] Remove xfails. --- .../tests/binops/test_contains_properly.py | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py b/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py index 5da281869..1128a04ca 100644 --- a/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py +++ b/python/cuspatial/cuspatial/tests/binops/test_contains_properly.py @@ -1,13 +1,13 @@ import geopandas as gpd +import numpy as np import pytest from shapely.geometry import LineString, Point, Polygon import cuspatial -@pytest.mark.xfail(reason="The polygons share numerous boundaries.") def test_manual_polygons(): - gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))) * 6]) + gpdlhs = gpd.GeoSeries([Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8)))] * 6) gpdrhs = gpd.GeoSeries( [ Polygon(((-8, -8), (-8, 8), (8, 8), (8, -8))), @@ -22,15 +22,18 @@ def test_manual_polygons(): lhs = cuspatial.from_geopandas(gpdlhs) got = lhs.contains_properly(rhs).values_host expected = gpdlhs.contains(gpdrhs).values - assert (got == expected).all() + assert (got == np.array([False, True, False, False, False, False])).all() + assert ( + expected == np.array([True, True, False, False, False, True]) + ).all() got = rhs.contains_properly(lhs).values_host expected = gpdrhs.contains(gpdlhs).values - assert (got == expected).all() + assert (got == np.array([False, False, False, False, False, False])).all() + assert ( + expected == np.array([True, False, False, False, False, False]) + ).all() -@pytest.mark.xfail( - reason="The linestring is colinear with one of the polygon edges." -) def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): gpdlinestring = gpd.GeoSeries(LineString([[0, 0], [1, 1]])) gpdpolygon = gpd.GeoSeries( @@ -40,13 +43,10 @@ def test_one_polygon_one_linestring_crosses_the_diagonal(linestring_generator): polygons = cuspatial.from_geopandas(gpdpolygon) got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() + assert not np.any(got) + assert np.all(expected) -@pytest.mark.xfail( - reason="""The linestring has boundaries inside of the polygon, and crosses - over a single inner ring.""" -) def test_one_polygon_with_hole_one_linestring_crossing_it( linestring_generator, ): @@ -75,22 +75,10 @@ def test_one_polygon_with_hole_one_linestring_crossing_it( polygons = cuspatial.from_geopandas(gpdpolygon) got = polygons.contains_properly(linestring).values_host expected = gpdpolygon.contains(gpdlinestring).values - assert (got == expected).all() + assert np.all(got) + assert not np.any(expected) -@pytest.mark.xfail( - reason="""These points are on edges of their corresponding polygons. - Because of floating point error, they can have inconsistent results with - GeoPandas. In this case, GeoPandas results are incorrect due to their - geometry engine. It is possible that implementing `.contains_properly` - would correct this error. These boundary cases conflict with GeoPandas - results because they implement `contains` and we implement - `contains_properly`. - - The below test_float_precision_limits pairs with this test and shows - the inconsistency. - """ -) @pytest.mark.parametrize( "point, polygon, expects", [ @@ -113,9 +101,10 @@ def test_float_precision_limits_failures(point, polygon, expects): point = cuspatial.from_geopandas(gpdpoint) polygon = cuspatial.from_geopandas(gpdpolygon) got = polygon.contains_properly(point).values_host - expected = gpdpolygon.contains(gpdpoint).values - assert got == expected - assert got.values_host[0] == expects + # GeoPandas results here are inconsistent. + # expected = gpdpolygon.contains(gpdpoint).values + # assert expected == True or False + assert not np.any(got) @pytest.mark.parametrize( From 70822a807a884e612e1424250d34cd378ff3bce5 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 30 Nov 2022 15:30:55 -0600 Subject: [PATCH 90/90] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris --- python/cuspatial/cuspatial/core/geoseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index d95fddc33..dc219887d 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -723,7 +723,7 @@ def contains_properly(self, other, align=True): dtype: bool - Test whether 3 points fall within either of two polygons + Test whether three points fall within either of two polygons >>> point = cuspatial.GeoSeries( [Point(0, 0)], [Point(-1, 0)],