From 96b28a3790fdba2c5ddbc76abc0e882b7edacd5a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 30 Mar 2023 12:55:30 -0500 Subject: [PATCH 001/126] Work on the most obvious parts of allpairs_points_equals_count --- .../allpairs_point_equals_count.cuh | 70 ++++++++++++++ .../detail/allpairs_point_equals_count.cuh | 66 +++++++++++++ .../spatial/allpairs_point_equals_count.cu | 96 +++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh create mode 100644 cpp/src/spatial/allpairs_point_equals_count.cu diff --git a/cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh b/cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh new file mode 100644 index 000000000..28d266b16 --- /dev/null +++ b/cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, 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 + +namespace cuspatial { + +/** + * @brief Sinusoidal projection of longitude/latitude relative to origin to Cartesian (x/y) + * coordinates in km. + * + * Can be used to approximately convert longitude/latitude coordinates to Cartesian coordinates + * given that all points are near the origin. Error increases with distance from the origin. + * See [Sinusoidal Projection](https://en.wikipedia.org/wiki/Sinusoidal_projection) for more detail. + * + * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` (Lat/Lon + * coordinates), and the output iterator must be able to accept for storage values of type + * `cuspatial::vec_2d` (Cartesian coordinates). + * + * @param[in] lon_lat_first beginning of range of input longitude/latitude coordinates. + * @param[in] lon_lat_last end of range of input longitude/latitude coordinates. + * @param[in] origin: longitude and latitude of origin. + * @param[out] xy_first: beginning of range of output x/y coordinates. + * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. + * + * @tparam InputIt Iterator over longitude/latitude locations. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OutputIt Iterator over Cartesian output points. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. + * @tparam T the floating-point coordinate value type of input longitude/latitude coordinates. + * + * @pre `lonlat_first` may equal `xy_first`, but the range `[lonlat_first, lonlat_last)` + * shall not otherwise overlap the range `[xy_first, xy_first + std::distance(lonlat_first, + * lonlat_last))`. + * + * @return Output iterator to the element past the last x/y coordinate computed. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +OutputIt allpairs_point_equals_count(InputIt lhs_first, + InputIt rhs_first, + InputIt lhs_last, + InputIt rhs_last, + OutputIt count_first, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); + +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh new file mode 100644 index 000000000..cdf1434b9 --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh @@ -0,0 +1,66 @@ +/* + * 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 +struct allpairs_point_equals_count_functor { + vec_2d __device__ operator()(vec_2d loc) {} +}; + +template +struct allpairs_point_equals_count_functor { +} + +} // namespace detail + +template +OutputIt allpairs_point_equals_count(InputIt lhs_first, + InputIt rhs_first, + InputIt lhs_last, + InputIt rhs_last, + OutputIt output, + rmm::cuda_stream_view stream) +{ + static_assert(is_same_floating_point>(), + "Origin and input must have the same base floating point type."); + + return thrust::transform(rmm::exec_policy(stream), + lhs_first, + rhs_first, + lhs_last, + rhs_last, + output, + detail::allpairs_point_equals_count_functor{}); +} + +} // namespace cuspatial diff --git a/cpp/src/spatial/allpairs_point_equals_count.cu b/cpp/src/spatial/allpairs_point_equals_count.cu new file mode 100644 index 000000000..b37ac738b --- /dev/null +++ b/cpp/src/spatial/allpairs_point_equals_count.cu @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 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 +#include +#include + +#include +#include + +namespace { + +struct dispatch_allpairs_point_equals_count { + template + std::enable_if_t::value, cudf::column> operator()(Args&&...) + { + CUSPATIAL_FAIL("Non-floating point operation is not supported"); + } + + template + std::enable_if_t::value, cudf::column> operator()( + cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto size = lhs.size(); + auto type = cudf::data_type(cudf::type_to_id()) + + auto result = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); + + cuspatial::allpairs_point_equals_count(lhs.begin(), + rhs.begin(), + lhs.begin() + lhs.size(), + rhs.begin() + rhs.size(), + result.mutable_vie().begin(), + stream); + + return std::make_pair(std::move(result)); + } +}; + +} // namespace + +namespace cuspatial { +namespace detail { + +cudf::column allpairs_point_equals_count(cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + lhs.type() CUSPATIAL_EXPECTS(lhs.type() == rhs.type(), "Column type mismatch"); + + return cudf::type_dispatcher( + lhs.type(), dispatch_allpairs_point_equals_count(), lhs, rhs, stream, mr); +} + +} // namespace detail + +cudf::column allpairs_point_equals_count(cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::mr::device_memory_resource* mr) +{ + return detail::allpairs_point_equals_count(lhs, rhs rmm::cuda_stream_default, mr); +} + +} // namespace cuspatial From b8785f7746306f66453bc4b2ea33a1e0a3503a59 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 30 Mar 2023 20:24:51 +0000 Subject: [PATCH 002/126] Build system. --- cpp/CMakeLists.txt | 1 + .../allpairs_multipoint_equals_count.hpp | 43 +++++ ...h => allpairs_multipoint_equals_count.cuh} | 14 +- .../allpairs_multipoint_equals_count.cuh | 77 ++++++++ .../detail/allpairs_point_equals_count.cuh | 66 ------- .../geometry_collection/multipoint_ref.cuh | 35 ++++ .../geometry_collection/multipoint_ref.cuh | 11 +- .../allpairs_multipoint_equals_count.cu | 98 +++++++++++ .../spatial/allpairs_point_equals_count.cu | 96 ---------- cpp/tests/CMakeLists.txt | 6 + .../allpairs_multipoint_equals_count_test.cu | 165 ++++++++++++++++++ .../allpairs_multipoint_equals_count_test.cu | 52 ++++++ 12 files changed, 492 insertions(+), 172 deletions(-) create mode 100644 cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp rename cpp/include/cuspatial/experimental/{allpairs_point_equals_count.cuh => allpairs_multipoint_equals_count.cuh} (84%) create mode 100644 cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh delete mode 100644 cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh create mode 100644 cpp/src/spatial/allpairs_multipoint_equals_count.cu delete mode 100644 cpp/src/spatial/allpairs_point_equals_count.cu create mode 100644 cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu create mode 100644 cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 74391b5ea..d96715803 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -122,6 +122,7 @@ add_library(cuspatial src/join/quadtree_point_in_polygon.cu src/join/quadtree_point_to_nearest_linestring.cu src/join/quadtree_bbox_filtering.cu + src/spatial/allpairs_multipoint_equals_count.cu src/spatial/polygon_bounding_box.cu src/spatial/linestring_bounding_box.cu src/spatial/point_in_polygon.cu diff --git a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp new file mode 100644 index 000000000..c6867c9e0 --- /dev/null +++ b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-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 + +namespace cuspatial { + +/** + * @addtogroup spatial + * @{ + */ + +/** + */ +std::unique_ptr allpairs_multipoint_equals_count( + cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +/** + * @} // end of doxygen group + */ + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh similarity index 84% rename from cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh rename to cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh index 28d266b16..25838cb5e 100644 --- a/cpp/include/cuspatial/experimental/allpairs_point_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh @@ -57,14 +57,12 @@ namespace cuspatial { * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator * "LegacyRandomAccessIterator" */ -template -OutputIt allpairs_point_equals_count(InputIt lhs_first, - InputIt rhs_first, - InputIt lhs_last, - InputIt rhs_last, - OutputIt count_first, - rmm::cuda_stream_view stream = rmm::cuda_stream_default); +template +OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs_first, + MultiPointRefB rhs_first, + OutputIt count_first, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); } // namespace cuspatial -#include +#include diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh new file mode 100644 index 000000000..00634b9a6 --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -0,0 +1,77 @@ +/* + * 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 + +#include +#include + +namespace cuspatial { + +namespace detail { + +template +void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, + MultiPointRefB rhs, + OutputIt output) +{ + using T = typename MultiPointRefA::point_t::value_type; + + static_assert(is_same_floating_point(), + "Origin and input must have the same base floating point type."); + + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.size() * rhs.size(); + idx += gridDim.x * blockDim.x) { + vec_2d lhs_point = *(lhs.point_tile_begin() + idx); + vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); + + atomicInc(&output[idx % lhs.size()], lhs_point == rhs_point); + } +} + +} // namespace detail + +template +OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs, + MultiPointRefB rhs, + OutputIt output, + rmm::cuda_stream_view stream) +{ + using T = typename MultiPointRefA::point_t::value_type; + + static_assert(is_same_floating_point(), + "Origin and input must have the same base floating point type."); + + auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); + detail::allpairs_point_equals_count_kernel<<>>( + lhs, rhs, output); + + CUSPATIAL_CHECK_CUDA(stream.value()); + return output + lhs.size(); +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh deleted file mode 100644 index cdf1434b9..000000000 --- a/cpp/include/cuspatial/experimental/detail/allpairs_point_equals_count.cuh +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 -struct allpairs_point_equals_count_functor { - vec_2d __device__ operator()(vec_2d loc) {} -}; - -template -struct allpairs_point_equals_count_functor { -} - -} // namespace detail - -template -OutputIt allpairs_point_equals_count(InputIt lhs_first, - InputIt rhs_first, - InputIt lhs_last, - InputIt rhs_last, - OutputIt output, - rmm::cuda_stream_view stream) -{ - static_assert(is_same_floating_point>(), - "Origin and input must have the same base floating point type."); - - return thrust::transform(rmm::exec_policy(stream), - lhs_first, - rhs_first, - lhs_last, - rhs_last, - output, - detail::allpairs_point_equals_count_functor{}); -} - -} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh index c4f8c9742..e257b1b1d 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh @@ -15,11 +15,32 @@ */ #pragma once #include +#include #include namespace cuspatial { +template +struct point_tile_functor { + VecIterator points_begin; + IndexType tile_size; + + CUSPATIAL_HOST_DEVICE auto operator()(IndexType i) { return points_begin[i % tile_size]; } +}; +template +point_tile_functor(VecIterator, IndexType) -> point_tile_functor; + +template +struct point_repeat_functor { + VecIterator points_begin; + IndexType repeat_size; + + CUSPATIAL_HOST_DEVICE auto operator()(IndexType i) { return points_begin[i / repeat_size]; } +}; +template +point_repeat_functor(VecIterator, IndexType) -> point_repeat_functor; + template CUSPATIAL_HOST_DEVICE multipoint_ref::multipoint_ref(VecIterator begin, VecIterator end) @@ -45,6 +66,20 @@ CUSPATIAL_HOST_DEVICE auto multipoint_ref::num_points() const return thrust::distance(_points_begin, _points_end); } +template +CUSPATIAL_HOST_DEVICE auto multipoint_ref::point_tile_begin() const +{ + return detail::make_counting_transform_iterator(0, + point_tile_functor{_points_begin, num_points()}); +} + +template +template +CUSPATIAL_HOST_DEVICE auto multipoint_ref::point_repeat_begin(IndexType repeats) const +{ + return detail::make_counting_transform_iterator(0, point_repeat_functor{_points_begin, repeats}); +} + template template CUSPATIAL_HOST_DEVICE auto multipoint_ref::operator[](IndexType i) diff --git a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh index 3d44a75ce..fed56ebd4 100644 --- a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh @@ -26,9 +26,9 @@ namespace cuspatial { */ template class multipoint_ref { + public: using point_t = iterator_value_type; - public: CUSPATIAL_HOST_DEVICE multipoint_ref(VecIterator begin, VecIterator end); /// Return iterator to the starting point of the multipoint. @@ -41,11 +41,18 @@ class multipoint_ref { /// Return iterator the the one-past the last point of the multipoint. CUSPATIAL_HOST_DEVICE auto end() const { return point_end(); } - /// Return the number of points in multipoint. + /// Return the number of points in multipoint CUSPATIAL_HOST_DEVICE auto num_points() const; /// Return the number of points in multipoint. CUSPATIAL_HOST_DEVICE auto size() const { return num_points(); } + /// Tiling iterator + CUSPATIAL_HOST_DEVICE auto point_tile_begin() const; + + // Repeating iterator + template + CUSPATIAL_HOST_DEVICE auto point_repeat_begin(IndexType repeats) const; + template CUSPATIAL_HOST_DEVICE auto operator[](IndexType point_idx); diff --git a/cpp/src/spatial/allpairs_multipoint_equals_count.cu b/cpp/src/spatial/allpairs_multipoint_equals_count.cu new file mode 100644 index 000000000..b9df3e061 --- /dev/null +++ b/cpp/src/spatial/allpairs_multipoint_equals_count.cu @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023, 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 +#include +#include +#include + +#include +#include + +namespace cuspatial { +namespace detail { +namespace { + +struct dispatch_allpairs_multipoint_equals_count { + template + std::enable_if_t::value, std::unique_ptr> operator()( + Args&&...) + { + CUSPATIAL_FAIL("Non-floating point operation is not supported"); + } + + template + std::enable_if_t::value, std::unique_ptr> operator()( + cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto size = lhs.size(); + auto type = cudf::data_type(cudf::type_to_id()); + + auto result = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); + + auto lhs_iterator = make_vec_2d_iterator(lhs.begin()); + auto rhs_iterator = make_vec_2d_iterator(rhs.begin()); + auto lhs_ref = multipoint_ref(lhs_iterator, lhs_iterator + lhs.size() / 2); + auto rhs_ref = multipoint_ref(rhs_iterator, rhs_iterator + rhs.size() / 2); + + cuspatial::allpairs_multipoint_equals_count( + lhs_ref, rhs_ref, result->mutable_view().begin(), stream); + + return result; + } +}; + +} // namespace + +std::unique_ptr allpairs_multipoint_equals_count(cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS(lhs.type() == rhs.type(), "Column type mismatch"); + + return cudf::type_dispatcher( + lhs.type(), dispatch_allpairs_multipoint_equals_count(), lhs, rhs, stream, mr); +} + +} // namespace detail + +std::unique_ptr allpairs_point_equals_count(cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::mr::device_memory_resource* mr) +{ + return detail::allpairs_multipoint_equals_count(lhs, rhs, rmm::cuda_stream_default, mr); +} + +} // namespace cuspatial diff --git a/cpp/src/spatial/allpairs_point_equals_count.cu b/cpp/src/spatial/allpairs_point_equals_count.cu deleted file mode 100644 index b37ac738b..000000000 --- a/cpp/src/spatial/allpairs_point_equals_count.cu +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2023, 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 -#include -#include - -#include -#include - -namespace { - -struct dispatch_allpairs_point_equals_count { - template - std::enable_if_t::value, cudf::column> operator()(Args&&...) - { - CUSPATIAL_FAIL("Non-floating point operation is not supported"); - } - - template - std::enable_if_t::value, cudf::column> operator()( - cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) - { - auto size = lhs.size(); - auto type = cudf::data_type(cudf::type_to_id()) - - auto result = - cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); - - cuspatial::allpairs_point_equals_count(lhs.begin(), - rhs.begin(), - lhs.begin() + lhs.size(), - rhs.begin() + rhs.size(), - result.mutable_vie().begin(), - stream); - - return std::make_pair(std::move(result)); - } -}; - -} // namespace - -namespace cuspatial { -namespace detail { - -cudf::column allpairs_point_equals_count(cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) -{ - lhs.type() CUSPATIAL_EXPECTS(lhs.type() == rhs.type(), "Column type mismatch"); - - return cudf::type_dispatcher( - lhs.type(), dispatch_allpairs_point_equals_count(), lhs, rhs, stream, mr); -} - -} // namespace detail - -cudf::column allpairs_point_equals_count(cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::mr::device_memory_resource* mr) -{ - return detail::allpairs_point_equals_count(lhs, rhs rmm::cuda_stream_default, mr); -} - -} // namespace cuspatial diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 10e4d269b..0a0f1747e 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -48,6 +48,9 @@ endfunction(ConfigureTest) ### test sources ################################################################################## ################################################################################################### +ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST + spatial/allpairs_multipoint_equals_count.cu) + ConfigureTest(SINUSOIDAL_PROJECTION_TEST spatial/sinusoidal_projection_test.cu) @@ -149,6 +152,9 @@ ConfigureTest(LINESTRING_INTERSECTION_TEST_EXP ConfigureTest(POINT_LINESTRING_NEAREST_POINT_TEST_EXP experimental/spatial/point_linestring_nearest_points_test.cu) +ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST_EXP + experimental/spatial/allpairs_multipoint_equals_count_test.cu) + ConfigureTest(SINUSOIDAL_PROJECTION_TEST_EXP experimental/spatial/sinusoidal_projection_test.cu) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu new file mode 100644 index 000000000..d79239fa8 --- /dev/null +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2022-2023, 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 + +template +struct AllPairsMultipointEqualsCount : public cuspatial::test::BaseFixture { + using Vec = cuspatial::vec_2d; + + void run_test(std::initializer_list lhs, std::initializer_list rhs) + { + auto _lhs = *make_multipoint_array(lhs); + + auto d_count = rmm::device_scalar{}; + + cuspatial::experimental::all_pairs_multipoint_equals_count( + d_lhs.begin(), d_lhs.end(), d_rhs.begin(), d_rhs.end(), d_count.data()); + + EXPECT_EQ(d_count.value(), lhs.size()); + + CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(h_expected, xy_output); + EXPECT_EQ(h_expected.size(), std::distance(xy_output.begin(), xy_end)); + } +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = ::testing::Types; +TYPED_TEST_CASE(SinusoidalProjectionTest, TestTypes); + +TYPED_TEST(SinusoidalProjectionTest, Empty) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{-90.66511046, 42.49197018}; + + auto h_point_lonlat = std::vector{}; + CUSPATIAL_RUN_TEST(this->run_test, h_point_lonlat, origin); +} + +TYPED_TEST(SinusoidalProjectionTest, Single) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{-90.66511046, 42.49197018}; + + auto h_point_lonlat = std::vector({{-90.664973, 42.493894}}); + CUSPATIAL_RUN_TEST(this->run_test, h_point_lonlat, origin); +} + +TYPED_TEST(SinusoidalProjectionTest, Extremes) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{0, 0}; + + auto h_points_lonlat = std::vector( + {{0.0, -90.0}, {0.0, 90.0}, {-180.0, 0.0}, {180.0, 0.0}, {45.0, 0.0}, {-180.0, -90.0}}); + CUSPATIAL_RUN_TEST(this->run_test, h_points_lonlat, origin); +} + +TYPED_TEST(SinusoidalProjectionTest, Multiple) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{-90.66511046, 42.49197018}; + + auto h_points_lonlat = std::vector({{-90.664973, 42.493894}, + {-90.665393, 42.491520}, + {-90.664976, 42.491420}, + {-90.664537, 42.493823}}); + CUSPATIAL_RUN_TEST(this->run_test, h_points_lonlat, origin); +} + +TYPED_TEST(SinusoidalProjectionTest, OriginOutOfBounds) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{-181, -91}; + + auto h_point_lonlat = std::vector{}; + auto h_expected = std::vector{}; + + auto point_lonlat = rmm::device_vector{}; + auto expected = rmm::device_vector{}; + + auto xy_output = rmm::device_vector{}; + + EXPECT_THROW(cuspatial::sinusoidal_projection( + point_lonlat.begin(), point_lonlat.end(), xy_output.begin(), origin), + cuspatial::logic_error); +} + +template +struct identity_xform { + using Location = cuspatial::vec_2d; + __device__ Location operator()(Location const& loc) { return loc; }; +}; + +// This test verifies that fancy iterators can be passed by using a pass-through transform_iterator +TYPED_TEST(SinusoidalProjectionTest, TransformIterator) +{ + using T = TypeParam; + using Loc = cuspatial::vec_2d; + using Cart = cuspatial::vec_2d; + + auto origin = Loc{-90.66511046, 42.49197018}; + + auto h_points_lonlat = std::vector({{-90.664973, 42.493894}, + {-90.665393, 42.491520}, + {-90.664976, 42.491420}, + {-90.664537, 42.493823}}); + auto h_expected = std::vector(h_points_lonlat.size()); + + std::transform(h_points_lonlat.begin(), + h_points_lonlat.end(), + h_expected.begin(), + sinusoidal_projection_functor(origin)); + + auto points_lonlat = rmm::device_vector{h_points_lonlat}; + auto expected = rmm::device_vector{h_expected}; + + auto xy_output = rmm::device_vector(4, Cart{-1, -1}); + + auto xform_begin = thrust::make_transform_iterator(points_lonlat.begin(), identity_xform{}); + auto xform_end = thrust::make_transform_iterator(points_lonlat.end(), identity_xform{}); + + auto xy_end = cuspatial::sinusoidal_projection(xform_begin, xform_end, xy_output.begin(), origin); + + CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(expected, xy_output); + EXPECT_EQ(4, std::distance(xy_output.begin(), xy_end)); +} diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu new file mode 100644 index 000000000..71b14a08c --- /dev/null +++ b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu @@ -0,0 +1,52 @@ +/* + * 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 + +#include + +using namespace cudf::test; + +constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; + +template +struct AllpairsMultipointEqualsCountTest : public BaseFixture { +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = Types; +TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); + +TYPED_TEST(AllpairsMultipointEqualsCountTest, Single) +{ + using T = TypeParam; + auto lhs = fixed_width_column_wrapper({}); + auto rhs = fixed_width_column_wrapper({}); + + auto output = cuspatial::allpairs_multipoint_equals_count(lhs, rhs); + + auto expected = fixed_width_column_wrapper({}); + + expect_columns_equivalent(expected, output->view(), verbosity); +} From c6e391047d8790a570a21de94a8b127e81e4c6b9 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 30 Mar 2023 16:26:16 -0500 Subject: [PATCH 003/126] Try a hand at copying point_polygon_distance_test.cu --- .../allpairs_multipoint_equals_count_test.cu | 158 ++++-------------- 1 file changed, 36 insertions(+), 122 deletions(-) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index d79239fa8..479dcfd2c 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -28,138 +29,51 @@ #include template -struct AllPairsMultipointEqualsCount : public cuspatial::test::BaseFixture { - using Vec = cuspatial::vec_2d; +struct AllpairsMultipointEqualsCountTest : public ::testing::Test { + rmm::cuda_stream_view stream() { return rmm::cuda_stream_default; } + rmm::mr::device_memory_resource* mr() { return rmm::mr::get_current_device_resource(); } - void run_test(std::initializer_list lhs, std::initializer_list rhs) + void run_single(std::initializer_list>> lhs_coordinates, + std::initializer_list>> rhs_coordinates, + std::initializer_list expected) { - auto _lhs = *make_multipoint_array(lhs); - - auto d_count = rmm::device_scalar{}; + std::vector> lhs_ref(lhs_coordinates); + std::vector> rhs_ref(rhs_coordinates); + // std::vector> multipolygon_coordinates_vec(multipolygon_coordinates); + return this->run_single(lhs_ref, rhs_ref, expected); + } - cuspatial::experimental::all_pairs_multipoint_equals_count( - d_lhs.begin(), d_lhs.end(), d_rhs.begin(), d_rhs.end(), d_count.data()); + void run_single(std::initializer_list>> lhs_coordinates, + std::initializer_list>> rhs_coordinates, + std::initializer_list expected) + { + auto d_lhs = make_multipoints_array(lhs_coordinates).ref; + auto d_rhs = make_multipoints_array(rhs_coordinates).ref; + auto got = rmm::device_uvector(d_lhs.size(), stream()); - EXPECT_EQ(d_count.value(), lhs.size()); + auto ret = allpairs_multipoint_equals_count(d_lhs, d_rhs, got.begin(), stream()); - CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(h_expected, xy_output); - EXPECT_EQ(h_expected.size(), std::distance(xy_output.begin(), xy_end)); + auto d_expected = cuspatial::test::make_device_vector(expected); + CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(got, d_expected); + EXPECT_EQ(ret, got.end()); } }; -// float and double are logically the same but would require separate tests due to precision. using TestTypes = ::testing::Types; -TYPED_TEST_CASE(SinusoidalProjectionTest, TestTypes); -TYPED_TEST(SinusoidalProjectionTest, Empty) -{ - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{-90.66511046, 42.49197018}; +TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); - auto h_point_lonlat = std::vector{}; - CUSPATIAL_RUN_TEST(this->run_test, h_point_lonlat, origin); -} - -TYPED_TEST(SinusoidalProjectionTest, Single) +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, ZeroPairs) { - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{-90.66511046, 42.49197018}; - - auto h_point_lonlat = std::vector({{-90.664973, 42.493894}}); - CUSPATIAL_RUN_TEST(this->run_test, h_point_lonlat, origin); -} - -TYPED_TEST(SinusoidalProjectionTest, Extremes) -{ - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{0, 0}; - - auto h_points_lonlat = std::vector( - {{0.0, -90.0}, {0.0, 90.0}, {-180.0, 0.0}, {180.0, 0.0}, {45.0, 0.0}, {-180.0, -90.0}}); - CUSPATIAL_RUN_TEST(this->run_test, h_points_lonlat, origin); -} - -TYPED_TEST(SinusoidalProjectionTest, Multiple) -{ - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{-90.66511046, 42.49197018}; - - auto h_points_lonlat = std::vector({{-90.664973, 42.493894}, - {-90.665393, 42.491520}, - {-90.664976, 42.491420}, - {-90.664537, 42.493823}}); - CUSPATIAL_RUN_TEST(this->run_test, h_points_lonlat, origin); -} - -TYPED_TEST(SinusoidalProjectionTest, OriginOutOfBounds) -{ - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{-181, -91}; - - auto h_point_lonlat = std::vector{}; - auto h_expected = std::vector{}; - - auto point_lonlat = rmm::device_vector{}; - auto expected = rmm::device_vector{}; - - auto xy_output = rmm::device_vector{}; - - EXPECT_THROW(cuspatial::sinusoidal_projection( - point_lonlat.begin(), point_lonlat.end(), xy_output.begin(), origin), - cuspatial::logic_error); -} - -template -struct identity_xform { - using Location = cuspatial::vec_2d; - __device__ Location operator()(Location const& loc) { return loc; }; -}; - -// This test verifies that fancy iterators can be passed by using a pass-through transform_iterator -TYPED_TEST(SinusoidalProjectionTest, TransformIterator) -{ - using T = TypeParam; - using Loc = cuspatial::vec_2d; - using Cart = cuspatial::vec_2d; - - auto origin = Loc{-90.66511046, 42.49197018}; - - auto h_points_lonlat = std::vector({{-90.664973, 42.493894}, - {-90.665393, 42.491520}, - {-90.664976, 42.491420}, - {-90.664537, 42.493823}}); - auto h_expected = std::vector(h_points_lonlat.size()); - - std::transform(h_points_lonlat.begin(), - h_points_lonlat.end(), - h_expected.begin(), - sinusoidal_projection_functor(origin)); - - auto points_lonlat = rmm::device_vector{h_points_lonlat}; - auto expected = rmm::device_vector{h_expected}; - - auto xy_output = rmm::device_vector(4, Cart{-1, -1}); - - auto xform_begin = thrust::make_transform_iterator(points_lonlat.begin(), identity_xform{}); - auto xform_end = thrust::make_transform_iterator(points_lonlat.end(), identity_xform{}); - - auto xy_end = cuspatial::sinusoidal_projection(xform_begin, xform_end, xy_output.begin(), origin); - - CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(expected, xy_output); - EXPECT_EQ(4, std::distance(xy_output.begin(), xy_end)); + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + std::initializer_list>{}, + {0}, + {0}, + {0}, + std::initializer_list

{}, + std::initializer_list{}); } From 45c128c14015aa6714261d2d7855ce0bfe84bad7 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 30 Mar 2023 15:07:02 -0700 Subject: [PATCH 004/126] recording progress with Thomson --- .../allpairs_multipoint_equals_count.cuh | 3 + cpp/include/cuspatial_test/base_fixture.hpp | 8 +- .../allpairs_multipoint_equals_count.cu | 6 +- cpp/tests/CMakeLists.txt | 2 +- .../allpairs_multipoint_equals_count_test.cu | 89 +++++++++++++------ .../allpairs_multipoint_equals_count_test.cu | 8 +- 6 files changed, 74 insertions(+), 42 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index 00634b9a6..b87b375ba 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -66,6 +67,8 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs, static_assert(is_same_floating_point(), "Origin and input must have the same base floating point type."); + detail::zero_data_async(output, output + lhs.size(), stream); + auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); detail::allpairs_point_equals_count_kernel<<>>( lhs, rhs, output); diff --git a/cpp/include/cuspatial_test/base_fixture.hpp b/cpp/include/cuspatial_test/base_fixture.hpp index 4e925025b..72e4ef0d0 100644 --- a/cpp/include/cuspatial_test/base_fixture.hpp +++ b/cpp/include/cuspatial_test/base_fixture.hpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#pragma once + #include #include @@ -54,8 +56,7 @@ class RMMResourceMixin { * class MyTestFixture : public cuspatial::test::BaseFixture {}; * ``` */ -class BaseFixture : public RMMResourceMixin, public ::testing::Test { -}; +class BaseFixture : public RMMResourceMixin, public ::testing::Test {}; /** * @brief Base test fixture class from which libcuspatial test with only value parameterization @@ -79,8 +80,7 @@ class BaseFixture : public RMMResourceMixin, public ::testing::Test { */ template class BaseFixtureWithParam : public RMMResourceMixin, - public ::testing::TestWithParam> { -}; + public ::testing::TestWithParam> {}; } // namespace test } // namespace cuspatial diff --git a/cpp/src/spatial/allpairs_multipoint_equals_count.cu b/cpp/src/spatial/allpairs_multipoint_equals_count.cu index b9df3e061..d9cd270cf 100644 --- a/cpp/src/spatial/allpairs_multipoint_equals_count.cu +++ b/cpp/src/spatial/allpairs_multipoint_equals_count.cu @@ -88,9 +88,9 @@ std::unique_ptr allpairs_multipoint_equals_count(cudf::column_view } // namespace detail -std::unique_ptr allpairs_point_equals_count(cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::mr::device_memory_resource* mr) +std::unique_ptr allpairs_multipoint_equals_count(cudf::column_view const& lhs, + cudf::column_view const& rhs, + rmm::mr::device_memory_resource* mr) { return detail::allpairs_multipoint_equals_count(lhs, rhs, rmm::cuda_stream_default, mr); } diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 0a0f1747e..a20453fb5 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -49,7 +49,7 @@ endfunction(ConfigureTest) ################################################################################################### ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST - spatial/allpairs_multipoint_equals_count.cu) + spatial/allpairs_multipoint_equals_count_test.cu) ConfigureTest(SINUSOIDAL_PROJECTION_TEST spatial/sinusoidal_projection_test.cu) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index 479dcfd2c..d510b729b 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -14,44 +14,41 @@ * limitations under the License. */ +#include +#include +#include + #include #include +#include #include #include -#include -#include -#include #include -#include - #include -template -struct AllpairsMultipointEqualsCountTest : public ::testing::Test { - rmm::cuda_stream_view stream() { return rmm::cuda_stream_default; } - rmm::mr::device_memory_resource* mr() { return rmm::mr::get_current_device_resource(); } +using namespace cuspatial; +using namespace cuspatial::test; +template +struct AllpairsMultipointEqualsCountTest : public BaseFixture { void run_single(std::initializer_list>> lhs_coordinates, std::initializer_list>> rhs_coordinates, - std::initializer_list expected) + std::initializer_list expected) { - std::vector> lhs_ref(lhs_coordinates); - std::vector> rhs_ref(rhs_coordinates); - // std::vector> multipolygon_coordinates_vec(multipolygon_coordinates); - return this->run_single(lhs_ref, rhs_ref, expected); - } + auto larray = make_multipoints_array(lhs_coordinates); + auto rarray = make_multipoints_array(rhs_coordinates); - void run_single(std::initializer_list>> lhs_coordinates, - std::initializer_list>> rhs_coordinates, - std::initializer_list expected) - { - auto d_lhs = make_multipoints_array(lhs_coordinates).ref; - auto d_rhs = make_multipoints_array(rhs_coordinates).ref; - auto got = rmm::device_uvector(d_lhs.size(), stream()); + auto lrange = larray.range(); + auto rrange = rarray.range(); + + auto lhs = lrange[0]; + auto rhs = rrange[0]; + + auto got = rmm::device_uvector(lhs.size(), stream()); - auto ret = allpairs_multipoint_equals_count(d_lhs, d_rhs, got.begin(), stream()); + auto ret = allpairs_multipoint_equals_count(lhs, rhs, got.begin(), stream()); auto d_expected = cuspatial::test::make_device_vector(expected); CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(got, d_expected); @@ -64,16 +61,50 @@ using TestTypes = ::testing::Types; TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); // Inputs are empty columns -TYPED_TEST(AllpairsMultipointEqualsCountTest, ZeroPairs) +TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) { using T = TypeParam; using P = vec_2d; CUSPATIAL_RUN_TEST(this->run_single, std::initializer_list>{}, - {0}, - {0}, - {0}, - std::initializer_list

{}, - std::initializer_list{}); + std::initializer_list>{}, + {}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}}}, {1}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 0}}}, {0}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneTwo) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 1}, {0, 0}}}, {1}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{1, 1}}}, {0, 1, 0}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0, 0, 0}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeEqualMiddle) +{ + CUSPATIAL_RUN_TEST( + this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}, {1, 1}, {-1, -1}}}, {0, 1, 0}); } diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu index 71b14a08c..5baa36501 100644 --- a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ * limitations under the License. */ +#include #include -#include -#include #include #include @@ -31,8 +30,7 @@ using namespace cudf::test; constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; template -struct AllpairsMultipointEqualsCountTest : public BaseFixture { -}; +struct AllpairsMultipointEqualsCountTest : public BaseFixture {}; // float and double are logically the same but would require separate tests due to precision. using TestTypes = Types; From 0f0a0d497b4d1db3bbd1e078a503bec95ed056ec Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 31 Mar 2023 08:05:22 -0700 Subject: [PATCH 005/126] fix 0 sized input error --- .../experimental/detail/allpairs_multipoint_equals_count.cuh | 4 ++++ .../spatial/allpairs_multipoint_equals_count_test.cu | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index b87b375ba..2c763fbc2 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -67,8 +67,12 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs, static_assert(is_same_floating_point(), "Origin and input must have the same base floating point type."); + if (lhs.size() == 0) return output; + detail::zero_data_async(output, output + lhs.size(), stream); + if (rhs.size() == 0) return output + lhs.size(); + auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); detail::allpairs_point_equals_count_kernel<<>>( lhs, rhs, output); diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index d510b729b..c9b56a570 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -67,8 +67,8 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) using P = vec_2d; CUSPATIAL_RUN_TEST(this->run_single, - std::initializer_list>{}, - std::initializer_list>{}, + std::initializer_list>{{}}, + std::initializer_list>{{}}, {}); } From d1fb60c7d3fdf81082b4aca28b9f0d0a8e9f3027 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 15:12:45 +0000 Subject: [PATCH 006/126] Pass all tests. --- .../allpairs_multipoint_equals_count.cuh | 3 ++- .../allpairs_multipoint_equals_count_test.cu | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index 2c763fbc2..8b6a35df7 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -50,7 +50,8 @@ void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, vec_2d lhs_point = *(lhs.point_tile_begin() + idx); vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); - atomicInc(&output[idx % lhs.size()], lhs_point == rhs_point); + size_t lhs_idx = idx % lhs.size(); + if (lhs_point == rhs_point) atomicInc(&output[lhs_idx], 1); } } diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index c9b56a570..a3184fe44 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -65,7 +65,7 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) { using T = TypeParam; using P = vec_2d; - + printf("Able to call CUSPATIAL_RUN_TEST?\n"); CUSPATIAL_RUN_TEST(this->run_single, std::initializer_list>{{}}, std::initializer_list>{{}}, @@ -102,9 +102,28 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneNotEqual) CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0, 0, 0}); } +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {1, 1}, {0, 0}}}, {1}); +} + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {0, 0}, {1, 1}}}, {1}); +} + // Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeEqualMiddle) { CUSPATIAL_RUN_TEST( this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}, {1, 1}, {-1, -1}}}, {0, 1, 0}); } + +// Inputs are empty columns +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNotEqualMiddle) +{ + CUSPATIAL_RUN_TEST( + this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{0, 0}, {-1, -1}, {2, 2}}}, {1, 0, 1}); +} From da247f8a063102669f8e284063104daa222c2133 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 10:18:55 -0500 Subject: [PATCH 007/126] Switch to atomicAdd --- .../experimental/detail/allpairs_multipoint_equals_count.cuh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index 8b6a35df7..98c9e250c 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -50,8 +50,7 @@ void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, vec_2d lhs_point = *(lhs.point_tile_begin() + idx); vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); - size_t lhs_idx = idx % lhs.size(); - if (lhs_point == rhs_point) atomicInc(&output[lhs_idx], 1); + atomicAdd(&output[idx % lhs.size()], lhs_point == rhs_point); } } From 2b7d7fa17212401e0b95b8fb6710912a85eb24e2 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 15:55:09 +0000 Subject: [PATCH 008/126] Write one more test. --- .../allpairs_multipoint_equals_count_test.cu | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu index 5baa36501..ea7165f71 100644 --- a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu @@ -30,13 +30,14 @@ using namespace cudf::test; constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; template -struct AllpairsMultipointEqualsCountTest : public BaseFixture {}; +struct AllpairsMultipointEqualsCountTest : public BaseFixture { +}; // float and double are logically the same but would require separate tests due to precision. using TestTypes = Types; TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); -TYPED_TEST(AllpairsMultipointEqualsCountTest, Single) +TYPED_TEST(AllpairsMultipointEqualsCountTest, Empty) { using T = TypeParam; auto lhs = fixed_width_column_wrapper({}); @@ -48,3 +49,13 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, Single) expect_columns_equivalent(expected, output->view(), verbosity); } + +TYPED_TEST(AllpairsMultipointEqualsCountTest, InvalidTypes) +{ + using T = TypeParam; + auto lhs = fixed_width_column_wrapper({}); + auto rhs = fixed_width_column_wrapper({}); + + EXPECT_THROW(auto output = cuspatial::allpairs_multipoint_equals_count(lhs, rhs), + cuspatial::logic_error); +} From dd01d6ca7654ce594cdf3cf125f7b2779c26ec17 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 31 Mar 2023 10:01:50 -0700 Subject: [PATCH 009/126] using sort-then-search algorithm --- .../allpairs_multipoint_equals_count.cuh | 4 +- .../allpairs_multipoint_equals_count.cuh | 76 ++++++++++++------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh index 25838cb5e..10601ce19 100644 --- a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh @@ -58,8 +58,8 @@ namespace cuspatial { * "LegacyRandomAccessIterator" */ template -OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs_first, - MultiPointRefB rhs_first, +OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs_first, + MultiPointRefB const& rhs_first, OutputIt count_first, rmm::cuda_stream_view stream = rmm::cuda_stream_default); diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index 8b6a35df7..b12c9e5bd 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -20,12 +20,16 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include #include @@ -35,31 +39,31 @@ namespace cuspatial { namespace detail { -template -void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, - MultiPointRefB rhs, - OutputIt output) -{ - using T = typename MultiPointRefA::point_t::value_type; +// template +// void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, +// SortedPointRange rhs, +// OutputIt output) +// { +// using T = typename MultiPointRefA::point_t::value_type; - static_assert(is_same_floating_point(), - "Origin and input must have the same base floating point type."); +// static_assert(is_same_floating_point(), +// "Origin and input must have the same base floating point type."); - for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.size() * rhs.size(); - idx += gridDim.x * blockDim.x) { - vec_2d lhs_point = *(lhs.point_tile_begin() + idx); - vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); +// for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.size() * rhs.size(); +// idx += gridDim.x * blockDim.x) { +// vec_2d lhs_point = *(lhs.point_tile_begin() + idx); +// vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); - size_t lhs_idx = idx % lhs.size(); - if (lhs_point == rhs_point) atomicInc(&output[lhs_idx], 1); - } -} +// size_t lhs_idx = idx % lhs.size(); +// if (lhs_point == rhs_point) atomicInc(&output[lhs_idx], 1); +// } +// } } // namespace detail template -OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs, - MultiPointRefB rhs, +OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, + MultiPointRefB const& rhs, OutputIt output, rmm::cuda_stream_view stream) { @@ -70,16 +74,34 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA lhs, if (lhs.size() == 0) return output; - detail::zero_data_async(output, output + lhs.size(), stream); - - if (rhs.size() == 0) return output + lhs.size(); - - auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); - detail::allpairs_point_equals_count_kernel<<>>( - lhs, rhs, output); + if (rhs.size() == 0) { + detail::zero_data_async(output, output + lhs.size(), stream); + return output + lhs.size(); + } - CUSPATIAL_CHECK_CUDA(stream.value()); - return output + lhs.size(); + rmm::device_uvector> rhs_sorted(rhs.size(), stream); + thrust::copy(rmm::exec_policy(stream), rhs.begin(), rhs.end(), rhs_sorted.begin()); + thrust::sort(rmm::exec_policy(stream), rhs_sorted.begin(), rhs_sorted.end()); + + // auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); + // detail::allpairs_point_equals_count_kernel<<>>( + // lhs, range(rhs_sorted.begin(), rhs), output); + + return thrust::transform( + rmm::exec_policy(stream), + lhs.begin(), + lhs.end(), + output, + [rhs_sorted_range = range(rhs_sorted.begin(), rhs_sorted.end())] __device__(auto lhs_point) { + auto [lower_it, upper_it] = thrust::equal_range( + thrust::seq, rhs_sorted_range.cbegin(), rhs_sorted_range.cend(), lhs_point); + return thrust::distance(lower_it, upper_it); + }); + + // CUSPATIAL_CHECK_CUDA(stream.value()); + // return output + + // lhs.size(); } } // namespace cuspatial From 3dc6195bce455d1952ab3778a4565dce1f214c75 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 31 Mar 2023 10:08:43 -0700 Subject: [PATCH 010/126] passes compilation! --- .../allpairs_multipoint_equals_count.cuh | 33 ------------------- .../cuspatial/experimental/ranges/range.cuh | 4 +++ .../allpairs_multipoint_equals_count_test.cu | 1 - 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index b12c9e5bd..f5d564959 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -37,30 +37,6 @@ namespace cuspatial { -namespace detail { - -// template -// void __global__ allpairs_point_equals_count_kernel(MultiPointRefA lhs, -// SortedPointRange rhs, -// OutputIt output) -// { -// using T = typename MultiPointRefA::point_t::value_type; - -// static_assert(is_same_floating_point(), -// "Origin and input must have the same base floating point type."); - -// for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.size() * rhs.size(); -// idx += gridDim.x * blockDim.x) { -// vec_2d lhs_point = *(lhs.point_tile_begin() + idx); -// vec_2d rhs_point = *(rhs.point_repeat_begin(lhs.size()) + idx); - -// size_t lhs_idx = idx % lhs.size(); -// if (lhs_point == rhs_point) atomicInc(&output[lhs_idx], 1); -// } -// } - -} // namespace detail - template OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, MultiPointRefB const& rhs, @@ -83,11 +59,6 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, thrust::copy(rmm::exec_policy(stream), rhs.begin(), rhs.end(), rhs_sorted.begin()); thrust::sort(rmm::exec_policy(stream), rhs_sorted.begin(), rhs_sorted.end()); - // auto [threads_per_block, block_size] = grid_1d(lhs.size() * rhs.size()); - // detail::allpairs_point_equals_count_kernel<<>>( - // lhs, range(rhs_sorted.begin(), rhs), output); - return thrust::transform( rmm::exec_policy(stream), lhs.begin(), @@ -98,10 +69,6 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, thrust::seq, rhs_sorted_range.cbegin(), rhs_sorted_range.cend(), lhs_point); return thrust::distance(lower_it, upper_it); }); - - // CUSPATIAL_CHECK_CUDA(stream.value()); - // return output + - // lhs.size(); } } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/range.cuh b/cpp/include/cuspatial/experimental/ranges/range.cuh index 923580fe2..b301e23aa 100644 --- a/cpp/include/cuspatial/experimental/ranges/range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/range.cuh @@ -49,6 +49,10 @@ class range { auto CUSPATIAL_HOST_DEVICE begin() { return _begin; } /// Return the end iterator to the range auto CUSPATIAL_HOST_DEVICE end() { return _end; } + /// Return the start const iterator to the range + auto CUSPATIAL_HOST_DEVICE cbegin() const { return _begin; } + /// Return the end const iterator to the range + auto CUSPATIAL_HOST_DEVICE cend() const { return _end; } /// Return the size of the range auto CUSPATIAL_HOST_DEVICE size() { return thrust::distance(_begin, _end); } diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index a3184fe44..b00a6540a 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -65,7 +65,6 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) { using T = TypeParam; using P = vec_2d; - printf("Able to call CUSPATIAL_RUN_TEST?\n"); CUSPATIAL_RUN_TEST(this->run_single, std::initializer_list>{{}}, std::initializer_list>{{}}, From dbd1bbf8c7eb2b8789f0c0152b39c50771a03d11 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 18:20:25 +0000 Subject: [PATCH 011/126] Cleaning up for PR. --- .../allpairs_multipoint_equals_count.hpp | 19 ++++++-- .../allpairs_multipoint_equals_count.cuh | 48 +++++++++++-------- .../allpairs_multipoint_equals_count.cuh | 2 + .../geometry_collection/multipoint_ref.cuh | 34 ------------- .../geometry_collection/multipoint_ref.cuh | 7 --- .../allpairs_multipoint_equals_count_test.cu | 13 ----- 6 files changed, 44 insertions(+), 79 deletions(-) diff --git a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp index c6867c9e0..422e12430 100644 --- a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp @@ -26,7 +26,20 @@ namespace cuspatial { /** * @addtogroup spatial - * @{ + * @brief Compute the number of pairs of multipoints that are equal. + * + * Given two columns of interleaved multipoint coordinates, returns a column + * containing the count of points in each multipoint in `lhs` that are equal to + * a point in the corresponding multipoint in `rhs`. + * + * @param lhs Geometry column with a multipoint of interleaved coordinates + * @param rhs Geometry column with a multipoint of interleaved coordinates + * @param mr Device memory resource used to allocate the returned column. + * @return A column of size len(lhs) containing the count that each point of + * the multipoint in `lhs` is equal to a point in `rhs`. + * + * @throw cuspatial::logic_error if `lhs` and `rhs` have different coordinate + * types. */ /** @@ -36,8 +49,4 @@ std::unique_ptr allpairs_multipoint_equals_count( cudf::column_view const& rhs, rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); -/** - * @} // end of doxygen group - */ - } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh index 10601ce19..2e44fc026 100644 --- a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh @@ -25,34 +25,42 @@ namespace cuspatial { /** - * @brief Sinusoidal projection of longitude/latitude relative to origin to Cartesian (x/y) - * coordinates in km. + * @brief Compute the number of multipoint pairs that are equal. * - * Can be used to approximately convert longitude/latitude coordinates to Cartesian coordinates - * given that all points are near the origin. Error increases with distance from the origin. - * See [Sinusoidal Projection](https://en.wikipedia.org/wiki/Sinusoidal_projection) for more detail. + * Given two sets of multipoints, each represented by a range of `vec_2d`s, + * computes the number of pairs of multipoints that are equal. Example: * - * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` (Lat/Lon - * coordinates), and the output iterator must be able to accept for storage values of type - * `cuspatial::vec_2d` (Cartesian coordinates). + * ``` + * lhs: { {0, 0}, {1, 1}, {2, 2} } + * rhs: { {0, 0}, {1, 1}, {2, 2} } + * count: { 1, 1, 1 } * - * @param[in] lon_lat_first beginning of range of input longitude/latitude coordinates. - * @param[in] lon_lat_last end of range of input longitude/latitude coordinates. - * @param[in] origin: longitude and latitude of origin. - * @param[out] xy_first: beginning of range of output x/y coordinates. + * lhs: { {0, 0} } + * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * count: { 1 } + * + * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * rhs: { {0, 0} } + * count: { 1, 0, 0, 0 } + * ``` + * + * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` + * and the output iterator must be able to accept for storage values of type + * `uint32_t`. + * + * @param[in] lhs_first multipoint_ref of first set of points + * @param[in] rhs_first multipoint_ref of second set of points + * @param[out] count_first: beginning of range of uint32_t counts * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. * - * @tparam InputIt Iterator over longitude/latitude locations. Must meet the requirements of + * @tparam MultiPointRefA Iterator over multipoint vec_2ds. Must meet the requirements of * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - * @tparam OutputIt Iterator over Cartesian output points. Must meet the requirements of + * @tparam MultiPointRefB Iterator over multipoint vec_2ds. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. - * @tparam T the floating-point coordinate value type of input longitude/latitude coordinates. - * - * @pre `lonlat_first` may equal `xy_first`, but the range `[lonlat_first, lonlat_last)` - * shall not otherwise overlap the range `[xy_first, xy_first + std::distance(lonlat_first, - * lonlat_last))`. * - * @return Output iterator to the element past the last x/y coordinate computed. + * @return Output iterator to the element past the last count result written. * * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator * "LegacyRandomAccessIterator" diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index f5d564959..e4984f898 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -55,10 +55,12 @@ OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, return output + lhs.size(); } + // Create a sorted copy of the rhs points. rmm::device_uvector> rhs_sorted(rhs.size(), stream); thrust::copy(rmm::exec_policy(stream), rhs.begin(), rhs.end(), rhs_sorted.begin()); thrust::sort(rmm::exec_policy(stream), rhs_sorted.begin(), rhs_sorted.end()); + // For each point in the lhs, count the number of points in the rhs that are equal. return thrust::transform( rmm::exec_policy(stream), lhs.begin(), diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh index e257b1b1d..5d4e8eeeb 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipoint_ref.cuh @@ -21,26 +21,6 @@ namespace cuspatial { -template -struct point_tile_functor { - VecIterator points_begin; - IndexType tile_size; - - CUSPATIAL_HOST_DEVICE auto operator()(IndexType i) { return points_begin[i % tile_size]; } -}; -template -point_tile_functor(VecIterator, IndexType) -> point_tile_functor; - -template -struct point_repeat_functor { - VecIterator points_begin; - IndexType repeat_size; - - CUSPATIAL_HOST_DEVICE auto operator()(IndexType i) { return points_begin[i / repeat_size]; } -}; -template -point_repeat_functor(VecIterator, IndexType) -> point_repeat_functor; - template CUSPATIAL_HOST_DEVICE multipoint_ref::multipoint_ref(VecIterator begin, VecIterator end) @@ -66,20 +46,6 @@ CUSPATIAL_HOST_DEVICE auto multipoint_ref::num_points() const return thrust::distance(_points_begin, _points_end); } -template -CUSPATIAL_HOST_DEVICE auto multipoint_ref::point_tile_begin() const -{ - return detail::make_counting_transform_iterator(0, - point_tile_functor{_points_begin, num_points()}); -} - -template -template -CUSPATIAL_HOST_DEVICE auto multipoint_ref::point_repeat_begin(IndexType repeats) const -{ - return detail::make_counting_transform_iterator(0, point_repeat_functor{_points_begin, repeats}); -} - template template CUSPATIAL_HOST_DEVICE auto multipoint_ref::operator[](IndexType i) diff --git a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh index fed56ebd4..a19517228 100644 --- a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh @@ -46,13 +46,6 @@ class multipoint_ref { /// Return the number of points in multipoint. CUSPATIAL_HOST_DEVICE auto size() const { return num_points(); } - /// Tiling iterator - CUSPATIAL_HOST_DEVICE auto point_tile_begin() const; - - // Repeating iterator - template - CUSPATIAL_HOST_DEVICE auto point_repeat_begin(IndexType repeats) const; - template CUSPATIAL_HOST_DEVICE auto operator[](IndexType point_idx); diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index b00a6540a..acee42346 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -60,7 +60,6 @@ using TestTypes = ::testing::Types; TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) { using T = TypeParam; @@ -71,56 +70,44 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) {}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}}}, {1}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneNotEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 0}}}, {0}); } -// Inputs are empty columns -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneTwo) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 1}, {0, 0}}}, {1}); } -// Inputs are empty columns -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{1, 1}}}, {0, 1, 0}); } -// Inputs are empty columns -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneNotEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0, 0, 0}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {1, 1}, {0, 0}}}, {1}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeNotEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {0, 0}, {1, 1}}}, {1}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeEqualMiddle) { CUSPATIAL_RUN_TEST( this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}, {1, 1}, {-1, -1}}}, {0, 1, 0}); } -// Inputs are empty columns TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNotEqualMiddle) { CUSPATIAL_RUN_TEST( From bd03668ff39f233a53b7190b9ce8dfb342c44513 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 13:36:17 -0500 Subject: [PATCH 012/126] Update docs, fix a typo. --- .../cuspatial/allpairs_multipoint_equals_count.hpp | 14 ++++++++++++-- .../geometry_collection/multipoint_ref.cuh | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp index 422e12430..8d75be191 100644 --- a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp @@ -29,8 +29,8 @@ namespace cuspatial { * @brief Compute the number of pairs of multipoints that are equal. * * Given two columns of interleaved multipoint coordinates, returns a column - * containing the count of points in each multipoint in `lhs` that are equal to - * a point in the corresponding multipoint in `rhs`. + * containing the count of points in each multipoint from `lhs` that are equal + * to a point in the corresponding multipoint in `rhs`. * * @param lhs Geometry column with a multipoint of interleaved coordinates * @param rhs Geometry column with a multipoint of interleaved coordinates @@ -40,6 +40,16 @@ namespace cuspatial { * * @throw cuspatial::logic_error if `lhs` and `rhs` have different coordinate * types. + * + * @example + * ``` + * lhs: 0, 0, 1, 1, 2, 2 + * rhs: 0, 0, 1, 1, 2, 2 + * result: 1, 1, 1 + * + * lhs: 0, 0, 1, 1, 2, 2 + * rhs: 0, 0 + * result: 1, 0, 0 */ /** diff --git a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh index a19517228..b854ba542 100644 --- a/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry_collection/multipoint_ref.cuh @@ -41,7 +41,7 @@ class multipoint_ref { /// Return iterator the the one-past the last point of the multipoint. CUSPATIAL_HOST_DEVICE auto end() const { return point_end(); } - /// Return the number of points in multipoint + /// Return the number of points in multipoint. CUSPATIAL_HOST_DEVICE auto num_points() const; /// Return the number of points in multipoint. CUSPATIAL_HOST_DEVICE auto size() const { return num_points(); } From 313c020d9c9462ab2a1f06a7b26a0a56be64b103 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 14:29:51 -0500 Subject: [PATCH 013/126] Rename cpp test. --- ...ls_count_test.cu => allpairs_multipoint_equals_count_test.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cpp/tests/spatial/{allpairs_multipoint_equals_count_test.cu => allpairs_multipoint_equals_count_test.cpp} (100%) diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp similarity index 100% rename from cpp/tests/spatial/allpairs_multipoint_equals_count_test.cu rename to cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp From 1c1e7ce7375b9d9794ccc6e2b7bbbaf3a3327d7e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 19:39:25 +0000 Subject: [PATCH 014/126] Strange test error. --- cpp/tests/CMakeLists.txt | 2 +- .../spatial/allpairs_multipoint_equals_count_test.cu | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index a20453fb5..d5ec996e8 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -49,7 +49,7 @@ endfunction(ConfigureTest) ################################################################################################### ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST - spatial/allpairs_multipoint_equals_count_test.cu) + spatial/allpairs_multipoint_equals_count_test.cpp) ConfigureTest(SINUSOIDAL_PROJECTION_TEST spatial/sinusoidal_projection_test.cu) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index acee42346..2d987b4f8 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -80,14 +80,17 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneNotEqual) CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 0}}}, {0}); } +TYPED_TEST(AllpairsMultipointEqualsCountTest, OneTwoEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 1}, {0, 0}}}, {1}); } +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{1, 1}}}, {0, 1, 0}); } +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneNotEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0, 0, 0}); } From 28d5c136189994b17354ceb82aa3a854aeccabc7 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 14:48:22 -0500 Subject: [PATCH 015/126] Write python bindings. --- .../_lib/allpairs_multipoint_equals_count.pyx | 30 ++++++++++++++++ .../cpp/allpairs_multipoint_equals_count.pxd | 13 +++++++ .../cuspatial/core/binops/equals_count.py | 35 +++++++++++++++++++ .../tests/binops/test_equals_count.py | 15 ++++++++ 4 files changed, 93 insertions(+) create mode 100644 python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx create mode 100644 python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd create mode 100644 python/cuspatial/cuspatial/core/binops/equals_count.py create mode 100644 python/cuspatial/cuspatial/tests/binops/test_equals_count.py diff --git a/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx b/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx new file mode 100644 index 000000000..b45e15bac --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx @@ -0,0 +1,30 @@ +# Copyright (c) 2023, 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.allpairs_multipoint_equals_count cimport ( + allpairs_multipoint_equals_count as cpp_allpairs_multipoint_equals_count, +) + + +def allpairs_multipoint_equals_count( + Column _lhs, + Column _rhs, +): + cdef column_view lhs = _lhs.view() + cdef column_view rhs = _rhs.view() + + cdef unique_ptr[column] result + + with nogil: + result = move( + cpp_allpairs_multipoint_equals_count( + lhs, + rhs, + ) + ) + + return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd b/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd new file mode 100644 index 000000000..4a0e18c00 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd @@ -0,0 +1,13 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.column cimport column, column_view + + +cdef extern from "cuspatial/allpairs_point_in_polygon.hpp" \ + namespace "cuspatial" nogil: + cdef unique_ptr[column] allpairs_point_in_polygon( + const column_view & lhs, + const column_view & rhs, + ) except + diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py new file mode 100644 index 000000000..94b81ede2 --- /dev/null +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +import cudf + +from cuspatial._lib.allpairs_multipoint_equals_count import ( + allpairs_multipoint_equals_count as c_allpairs_multipoint_equals_count, +) +from cuspatial.core.geoseries import GeoSeries +from cuspatial.utils.column_utils import contains_only_multipoints + + +def allpairs_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): + """ + Compute the count of times that each multipoint in the first GeoSeries + equals each multipoint in the second GeoSeries. + + Parameters + ---------- + multipoint : GeoSeries + A GeoSeries of multipoints. + multipoint : GeoSeries + A GeoSeries of multipoints. + + Returns + ------- + count : cudf.Series + A Series of counts of multipoint equality. + """ + if len(lhs) == 0: + return cudf.Series([]) + + if any(not contains_only_multipoints(s) for s in [lhs, rhs]): + raise ValueError("Input GeoSeries must contain only linestrings.") + + return c_allpairs_multipoint_equals_count(lhs._column, rhs._column) diff --git a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py new file mode 100644 index 000000000..3a789004e --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py @@ -0,0 +1,15 @@ +from pandas.testing import assert_series_equal +from shapely.geometry import Point + +import cudf + +import cuspatial +from cuspatial.core.binops.equals_count import allpairs_multipoint_equals_count + + +def test_allpairs_multipoint_equals_count(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) + p2 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) + got = allpairs_multipoint_equals_count(p1, p2) + expected = cudf.Series([1, 1, 1]) + assert_series_equal(got, expected) From 6abb44ea593bab840473e4089dd1d33e296cb4b2 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 20:35:12 +0000 Subject: [PATCH 016/126] Build python bindings and test. --- .../allpairs_multipoint_equals_count.cu | 3 +- .../cuspatial/cuspatial/_lib/CMakeLists.txt | 1 + .../cpp/allpairs_multipoint_equals_count.pxd | 4 +- .../_lib/cpp/linestring_intersection.pxd | 3 +- .../cuspatial/core/binops/equals_count.py | 8 +- .../tests/binops/test_equals_count.py | 76 +++++++++++++++++-- 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/cpp/src/spatial/allpairs_multipoint_equals_count.cu b/cpp/src/spatial/allpairs_multipoint_equals_count.cu index d9cd270cf..c30b79a9e 100644 --- a/cpp/src/spatial/allpairs_multipoint_equals_count.cu +++ b/cpp/src/spatial/allpairs_multipoint_equals_count.cu @@ -55,9 +55,8 @@ struct dispatch_allpairs_multipoint_equals_count { rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { - auto size = lhs.size(); + auto size = lhs.size() / 2; // lhs is a buffer of xy coords auto type = cudf::data_type(cudf::type_to_id()); - auto result = cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); diff --git a/python/cuspatial/cuspatial/_lib/CMakeLists.txt b/python/cuspatial/cuspatial/_lib/CMakeLists.txt index 4ffbfc7dc..522591f19 100644 --- a/python/cuspatial/cuspatial/_lib/CMakeLists.txt +++ b/python/cuspatial/cuspatial/_lib/CMakeLists.txt @@ -13,6 +13,7 @@ # ============================================================================= set(cython_sources + allpairs_multipoint_equals_count.pyx distance.pyx hausdorff.pyx intersection.pyx diff --git a/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd b/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd index 4a0e18c00..e5982cab0 100644 --- a/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd +++ b/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd @@ -5,9 +5,9 @@ from libcpp.memory cimport unique_ptr from cudf._lib.column cimport column, column_view -cdef extern from "cuspatial/allpairs_point_in_polygon.hpp" \ +cdef extern from "cuspatial/allpairs_multipoint_equals_count.hpp" \ namespace "cuspatial" nogil: - cdef unique_ptr[column] allpairs_point_in_polygon( + cdef unique_ptr[column] allpairs_multipoint_equals_count( const column_view & lhs, const column_view & rhs, ) except + diff --git a/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd b/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd index 71424a23f..e7d246e8a 100644 --- a/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd +++ b/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd @@ -1,4 +1,5 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# +Copyright (c) 2023, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index 94b81ede2..7e2f1bbc7 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -30,6 +30,10 @@ def allpairs_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): return cudf.Series([]) if any(not contains_only_multipoints(s) for s in [lhs, rhs]): - raise ValueError("Input GeoSeries must contain only linestrings.") + raise ValueError("Input GeoSeries must contain only multipoints.") - return c_allpairs_multipoint_equals_count(lhs._column, rhs._column) + result = c_allpairs_multipoint_equals_count( + lhs.multipoints.xy._column, rhs.multipoints.xy._column + ) + + return cudf.Series(result) diff --git a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py index 3a789004e..897975940 100644 --- a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py +++ b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py @@ -1,5 +1,5 @@ from pandas.testing import assert_series_equal -from shapely.geometry import Point +from shapely.geometry import MultiPoint, Point import cudf @@ -7,9 +7,73 @@ from cuspatial.core.binops.equals_count import allpairs_multipoint_equals_count -def test_allpairs_multipoint_equals_count(): - p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) - p2 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) +def test_allpairs_multipoint_equals_count_one_one_hit(): + p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) got = allpairs_multipoint_equals_count(p1, p2) - expected = cudf.Series([1, 1, 1]) - assert_series_equal(got, expected) + expected = cudf.Series([1], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_allpairs_multipoint_equals_count_one_one_miss(): + p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + p2 = cuspatial.GeoSeries([MultiPoint([Point(1, 1)])]) + got = allpairs_multipoint_equals_count(p1, p2) + expected = cudf.Series([0], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_allpairs_multipoint_equals_count_three_three_one_mismatch(): + p1 = cuspatial.GeoSeries( + [MultiPoint([Point(0, 0), Point(3, 3), Point(2, 2)])] + ) + p2 = cuspatial.GeoSeries( + [MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)])] + ) + got = allpairs_multipoint_equals_count(p1, p2) + expected = cudf.Series([1, 0, 1], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_allpairs_multipoint_equals_count_three_match_two_mismatch(): + p1 = cuspatial.GeoSeries( + [MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)])] + ) + p2 = cuspatial.GeoSeries( + [MultiPoint([Point(3, 3), Point(1, 1), Point(3, 3)])] + ) + got = allpairs_multipoint_equals_count(p1, p2) + expected = cudf.Series([0, 1, 0], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_allpairs_multipoint_equals_count_five(): + p1 = cuspatial.GeoSeries( + [ + MultiPoint( + [ + Point(0, 0), + Point(1, 1), + Point(2, 2), + Point(3, 3), + Point(4, 4), + ] + ) + ] + ) + p2 = cuspatial.GeoSeries( + [ + MultiPoint( + [ + Point(0, 0), + Point(0, 0), + Point(2, 2), + Point(2, 2), + Point(3, 3), + ] + ) + ] + ) + got = allpairs_multipoint_equals_count(p1, p2) + expected = cudf.Series([2, 0, 2, 1, 0], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) From c4006460ee7f76a6ba85074f8e6bb80681ccf437 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 15:41:00 -0500 Subject: [PATCH 017/126] Update cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh Co-authored-by: Michael Wang --- .../experimental/detail/allpairs_multipoint_equals_count.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh index e4984f898..99c266645 100644 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From d2ac98512286235f25375f760842ef8d279581f3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 15:41:07 -0500 Subject: [PATCH 018/126] Update cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp Co-authored-by: Michael Wang --- cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp index 8d75be191..49e02c332 100644 --- a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022, NVIDIA CORPORATION. + * Copyright (c) 2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 23f436320dbf6e4e33e2c63443e8ebb192f93f0d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 31 Mar 2023 15:41:50 -0500 Subject: [PATCH 019/126] Update cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu Co-authored-by: Michael Wang --- .../spatial/allpairs_multipoint_equals_count_test.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index 2d987b4f8..2a30f445f 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 63eea9d42545ca4032978df35ee9f0e2fd916e71 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 3 Apr 2023 20:25:27 +0000 Subject: [PATCH 020/126] Add specific test. --- .../spatial/allpairs_multipoint_equals_count_test.cu | 8 ++++++++ .../spatial/allpairs_multipoint_equals_count_test.cpp | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu index 2d987b4f8..3e1845ec1 100644 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu @@ -116,3 +116,11 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNotEqualMiddle) CUSPATIAL_RUN_TEST( this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{0, 0}, {-1, -1}, {2, 2}}}, {1, 0, 1}); } + +TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNeedRhsMultipoints) +{ + CUSPATIAL_RUN_TEST(this->run_single, + {{{0, 0}, {1, 1}, {2, 2}}}, + {{{0, 0}, {1, 1}}, {{2, 2}, {3, 3}}, {{0, 0}, {1, 1}}}, + {2, 0, 2}); +} diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp index ea7165f71..43f822148 100644 --- a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp +++ b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp @@ -52,7 +52,6 @@ TYPED_TEST(AllpairsMultipointEqualsCountTest, Empty) TYPED_TEST(AllpairsMultipointEqualsCountTest, InvalidTypes) { - using T = TypeParam; auto lhs = fixed_width_column_wrapper({}); auto rhs = fixed_width_column_wrapper({}); From 30c9c8648a79fd3830c289b54134f14e3c33864f Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 5 Apr 2023 14:54:37 -0700 Subject: [PATCH 021/126] refactors to multipoint-multipoint pairwise --- cpp/CMakeLists.txt | 2 +- .../allpairs_multipoint_equals_count.hpp | 62 --------- .../allpairs_multipoint_equals_count.cuh | 76 ----------- .../allpairs_multipoint_equals_count.cuh | 76 ----------- .../allpairs_multipoint_equals_count.cu | 97 -------------- cpp/tests/CMakeLists.txt | 8 +- .../allpairs_multipoint_equals_count_test.cu | 126 ------------------ .../allpairs_multipoint_equals_count_test.cpp | 60 --------- 8 files changed, 5 insertions(+), 502 deletions(-) delete mode 100644 cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp delete mode 100644 cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh delete mode 100644 cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh delete mode 100644 cpp/src/spatial/allpairs_multipoint_equals_count.cu delete mode 100644 cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu delete mode 100644 cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d96715803..fb55e4d68 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -122,7 +122,7 @@ add_library(cuspatial src/join/quadtree_point_in_polygon.cu src/join/quadtree_point_to_nearest_linestring.cu src/join/quadtree_bbox_filtering.cu - src/spatial/allpairs_multipoint_equals_count.cu + src/spatial/pairwise_multipoint_equals_count.cu src/spatial/polygon_bounding_box.cu src/spatial/linestring_bounding_box.cu src/spatial/point_in_polygon.cu diff --git a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp b/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp deleted file mode 100644 index 49e02c332..000000000 --- a/cpp/include/cuspatial/allpairs_multipoint_equals_count.hpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023, 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 - -namespace cuspatial { - -/** - * @addtogroup spatial - * @brief Compute the number of pairs of multipoints that are equal. - * - * Given two columns of interleaved multipoint coordinates, returns a column - * containing the count of points in each multipoint from `lhs` that are equal - * to a point in the corresponding multipoint in `rhs`. - * - * @param lhs Geometry column with a multipoint of interleaved coordinates - * @param rhs Geometry column with a multipoint of interleaved coordinates - * @param mr Device memory resource used to allocate the returned column. - * @return A column of size len(lhs) containing the count that each point of - * the multipoint in `lhs` is equal to a point in `rhs`. - * - * @throw cuspatial::logic_error if `lhs` and `rhs` have different coordinate - * types. - * - * @example - * ``` - * lhs: 0, 0, 1, 1, 2, 2 - * rhs: 0, 0, 1, 1, 2, 2 - * result: 1, 1, 1 - * - * lhs: 0, 0, 1, 1, 2, 2 - * rhs: 0, 0 - * result: 1, 0, 0 - */ - -/** - */ -std::unique_ptr allpairs_multipoint_equals_count( - cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); - -} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh deleted file mode 100644 index 2e44fc026..000000000 --- a/cpp/include/cuspatial/experimental/allpairs_multipoint_equals_count.cuh +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023, 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 - -namespace cuspatial { - -/** - * @brief Compute the number of multipoint pairs that are equal. - * - * Given two sets of multipoints, each represented by a range of `vec_2d`s, - * computes the number of pairs of multipoints that are equal. Example: - * - * ``` - * lhs: { {0, 0}, {1, 1}, {2, 2} } - * rhs: { {0, 0}, {1, 1}, {2, 2} } - * count: { 1, 1, 1 } - * - * lhs: { {0, 0} } - * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } - * count: { 1 } - * - * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } - * rhs: { {0, 0} } - * count: { 1, 0, 0, 0 } - * ``` - * - * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` - * and the output iterator must be able to accept for storage values of type - * `uint32_t`. - * - * @param[in] lhs_first multipoint_ref of first set of points - * @param[in] rhs_first multipoint_ref of second set of points - * @param[out] count_first: beginning of range of uint32_t counts - * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. - * - * @tparam MultiPointRefA Iterator over multipoint vec_2ds. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - * @tparam MultiPointRefB Iterator over multipoint vec_2ds. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. - * - * @return Output iterator to the element past the last count result written. - * - * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator - * "LegacyRandomAccessIterator" - */ -template -OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs_first, - MultiPointRefB const& rhs_first, - OutputIt count_first, - rmm::cuda_stream_view stream = rmm::cuda_stream_default); - -} // namespace cuspatial - -#include diff --git a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh deleted file mode 100644 index 99c266645..000000000 --- a/cpp/include/cuspatial/experimental/detail/allpairs_multipoint_equals_count.cuh +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023, 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 -#include -#include - -#include -#include -#include - -#include -#include - -namespace cuspatial { - -template -OutputIt allpairs_multipoint_equals_count(MultiPointRefA const& lhs, - MultiPointRefB const& rhs, - OutputIt output, - rmm::cuda_stream_view stream) -{ - using T = typename MultiPointRefA::point_t::value_type; - - static_assert(is_same_floating_point(), - "Origin and input must have the same base floating point type."); - - if (lhs.size() == 0) return output; - - if (rhs.size() == 0) { - detail::zero_data_async(output, output + lhs.size(), stream); - return output + lhs.size(); - } - - // Create a sorted copy of the rhs points. - rmm::device_uvector> rhs_sorted(rhs.size(), stream); - thrust::copy(rmm::exec_policy(stream), rhs.begin(), rhs.end(), rhs_sorted.begin()); - thrust::sort(rmm::exec_policy(stream), rhs_sorted.begin(), rhs_sorted.end()); - - // For each point in the lhs, count the number of points in the rhs that are equal. - return thrust::transform( - rmm::exec_policy(stream), - lhs.begin(), - lhs.end(), - output, - [rhs_sorted_range = range(rhs_sorted.begin(), rhs_sorted.end())] __device__(auto lhs_point) { - auto [lower_it, upper_it] = thrust::equal_range( - thrust::seq, rhs_sorted_range.cbegin(), rhs_sorted_range.cend(), lhs_point); - return thrust::distance(lower_it, upper_it); - }); -} - -} // namespace cuspatial diff --git a/cpp/src/spatial/allpairs_multipoint_equals_count.cu b/cpp/src/spatial/allpairs_multipoint_equals_count.cu deleted file mode 100644 index c30b79a9e..000000000 --- a/cpp/src/spatial/allpairs_multipoint_equals_count.cu +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2023, 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 -#include -#include -#include - -#include -#include - -namespace cuspatial { -namespace detail { -namespace { - -struct dispatch_allpairs_multipoint_equals_count { - template - std::enable_if_t::value, std::unique_ptr> operator()( - Args&&...) - { - CUSPATIAL_FAIL("Non-floating point operation is not supported"); - } - - template - std::enable_if_t::value, std::unique_ptr> operator()( - cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) - { - auto size = lhs.size() / 2; // lhs is a buffer of xy coords - auto type = cudf::data_type(cudf::type_to_id()); - auto result = - cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); - - auto lhs_iterator = make_vec_2d_iterator(lhs.begin()); - auto rhs_iterator = make_vec_2d_iterator(rhs.begin()); - auto lhs_ref = multipoint_ref(lhs_iterator, lhs_iterator + lhs.size() / 2); - auto rhs_ref = multipoint_ref(rhs_iterator, rhs_iterator + rhs.size() / 2); - - cuspatial::allpairs_multipoint_equals_count( - lhs_ref, rhs_ref, result->mutable_view().begin(), stream); - - return result; - } -}; - -} // namespace - -std::unique_ptr allpairs_multipoint_equals_count(cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) -{ - CUSPATIAL_EXPECTS(lhs.type() == rhs.type(), "Column type mismatch"); - - return cudf::type_dispatcher( - lhs.type(), dispatch_allpairs_multipoint_equals_count(), lhs, rhs, stream, mr); -} - -} // namespace detail - -std::unique_ptr allpairs_multipoint_equals_count(cudf::column_view const& lhs, - cudf::column_view const& rhs, - rmm::mr::device_memory_resource* mr) -{ - return detail::allpairs_multipoint_equals_count(lhs, rhs, rmm::cuda_stream_default, mr); -} - -} // namespace cuspatial diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index d5ec996e8..881343e1a 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -48,8 +48,8 @@ endfunction(ConfigureTest) ### test sources ################################################################################## ################################################################################################### -ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST - spatial/allpairs_multipoint_equals_count_test.cpp) +ConfigureTest(PAIRWISE_MULTIPOINT_EQUALS_COUNT_TEST + spatial/pairwise_multipoint_equals_count_test.cpp) ConfigureTest(SINUSOIDAL_PROJECTION_TEST spatial/sinusoidal_projection_test.cu) @@ -152,8 +152,8 @@ ConfigureTest(LINESTRING_INTERSECTION_TEST_EXP ConfigureTest(POINT_LINESTRING_NEAREST_POINT_TEST_EXP experimental/spatial/point_linestring_nearest_points_test.cu) -ConfigureTest(ALLPAIRS_MULTIPOINT_EQUALS_COUNT_TEST_EXP - experimental/spatial/allpairs_multipoint_equals_count_test.cu) +ConfigureTest(PAIRWISE_MULTIPOINT_EQUALS_COUNT_TEST_EXP + experimental/spatial/pairwise_multipoint_equals_count_test.cu) ConfigureTest(SINUSOIDAL_PROJECTION_TEST_EXP experimental/spatial/sinusoidal_projection_test.cu) diff --git a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu deleted file mode 100644 index 886120404..000000000 --- a/cpp/tests/experimental/spatial/allpairs_multipoint_equals_count_test.cu +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2023, 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 - -using namespace cuspatial; -using namespace cuspatial::test; - -template -struct AllpairsMultipointEqualsCountTest : public BaseFixture { - void run_single(std::initializer_list>> lhs_coordinates, - std::initializer_list>> rhs_coordinates, - std::initializer_list expected) - { - auto larray = make_multipoints_array(lhs_coordinates); - auto rarray = make_multipoints_array(rhs_coordinates); - - auto lrange = larray.range(); - auto rrange = rarray.range(); - - auto lhs = lrange[0]; - auto rhs = rrange[0]; - - auto got = rmm::device_uvector(lhs.size(), stream()); - - auto ret = allpairs_multipoint_equals_count(lhs, rhs, got.begin(), stream()); - - auto d_expected = cuspatial::test::make_device_vector(expected); - CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(got, d_expected); - EXPECT_EQ(ret, got.end()); - } -}; - -using TestTypes = ::testing::Types; - -TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); - -TYPED_TEST(AllpairsMultipointEqualsCountTest, EmptyInput) -{ - using T = TypeParam; - using P = vec_2d; - CUSPATIAL_RUN_TEST(this->run_single, - std::initializer_list>{{}}, - std::initializer_list>{{}}, - {}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}}}, {1}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneOneNotEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 0}}}, {0}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneTwoEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 1}, {0, 0}}}, {1}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{1, 1}}}, {0, 1, 0}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeOneNotEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0, 0, 0}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {1, 1}, {0, 0}}}, {1}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, OneThreeNotEqual) -{ - CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {0, 0}, {1, 1}}}, {1}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeEqualMiddle) -{ - CUSPATIAL_RUN_TEST( - this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}, {1, 1}, {-1, -1}}}, {0, 1, 0}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNotEqualMiddle) -{ - CUSPATIAL_RUN_TEST( - this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{0, 0}, {-1, -1}, {2, 2}}}, {1, 0, 1}); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, ThreeThreeNeedRhsMultipoints) -{ - CUSPATIAL_RUN_TEST(this->run_single, - {{{0, 0}, {1, 1}, {2, 2}}}, - {{{0, 0}, {1, 1}}, {{2, 2}, {3, 3}}, {{0, 0}, {1, 1}}}, - {2, 0, 2}); -} diff --git a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp b/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp deleted file mode 100644 index 43f822148..000000000 --- a/cpp/tests/spatial/allpairs_multipoint_equals_count_test.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2023, 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; - -constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; - -template -struct AllpairsMultipointEqualsCountTest : public BaseFixture { -}; - -// float and double are logically the same but would require separate tests due to precision. -using TestTypes = Types; -TYPED_TEST_CASE(AllpairsMultipointEqualsCountTest, TestTypes); - -TYPED_TEST(AllpairsMultipointEqualsCountTest, Empty) -{ - using T = TypeParam; - auto lhs = fixed_width_column_wrapper({}); - auto rhs = fixed_width_column_wrapper({}); - - auto output = cuspatial::allpairs_multipoint_equals_count(lhs, rhs); - - auto expected = fixed_width_column_wrapper({}); - - expect_columns_equivalent(expected, output->view(), verbosity); -} - -TYPED_TEST(AllpairsMultipointEqualsCountTest, InvalidTypes) -{ - auto lhs = fixed_width_column_wrapper({}); - auto rhs = fixed_width_column_wrapper({}); - - EXPECT_THROW(auto output = cuspatial::allpairs_multipoint_equals_count(lhs, rhs), - cuspatial::logic_error); -} From 2d64f3bbb6b2b9379bf045c6b14a9d2c2ab37dc8 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 5 Apr 2023 15:09:09 -0700 Subject: [PATCH 022/126] add missing files --- .../pairwise_multipoint_equals_count.cuh | 104 +++++++++++++ .../pairwise_multipoint_equals_count.cuh | 76 ++++++++++ .../pairwise_multipoint_equals_count.hpp | 64 ++++++++ .../pairwise_multipoint_equals_count.cu | 113 ++++++++++++++ .../pairwise_multipoint_equals_count_test.cu | 139 ++++++++++++++++++ .../pairwise_multipoint_equals_count_test.cpp | 75 ++++++++++ 6 files changed, 571 insertions(+) create mode 100644 cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh create mode 100644 cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh create mode 100644 cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp create mode 100644 cpp/src/spatial/pairwise_multipoint_equals_count.cu create mode 100644 cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu create mode 100644 cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh new file mode 100644 index 000000000..45599b5d5 --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, 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 + +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace cuspatial { + +namespace detail { + +template +void __global__ pairwise_multipoint_equals_count_kernel(MultiPointRangeA lhs, + MultiPointRangeB rhs, + OutputIt output) +{ + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.num_points(); + idx += gridDim.x * blockDim.x) { + auto geometry_id = lhs.geometry_idx_from_point_idx(idx); + auto lhs_point = lhs.point_begin()[idx]; + auto rhs_multipoint = rhs[geometry_id]; + + atomicAdd( + &output[geometry_id], + thrust::binary_search(thrust::seq, rhs_multipoint.begin(), rhs_multipoint.end(), lhs_point)); + } +} + +} // namespace detail + +template +OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs, + MultiPointRangeB rhs, + OutputIt output, + rmm::cuda_stream_view stream) +{ + using T = typename MultiPointRangeA::point_t::value_type; + using index_t = typename MultiPointRangeB::index_t; + + static_assert(is_same_floating_point(), + "Origin and input must have the same base floating point type."); + + CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), "Input should have the same number of pairs."); + + if (lhs.size() == 0) return output; + + // Create a sorted copy of the rhs points. + auto key_it = make_geometry_id_iterator(rhs.offsets_begin(), rhs.offsets_end()); + + rmm::device_uvector rhs_keys(rhs.num_points(), stream); + rmm::device_uvector> rhs_point_sorted(rhs.num_points(), stream); + + thrust::copy(rmm::exec_policy(stream), key_it, key_it + rhs.num_points(), rhs_keys.begin()); + thrust::copy( + rmm::exec_policy(stream), rhs.point_begin(), rhs.point_end(), rhs_point_sorted.begin()); + + thrust::sort_by_key( + rmm::exec_policy(stream), rhs_keys.begin(), rhs_keys.end(), rhs_point_sorted.begin()); + + auto rhs_sorted = multipoint_range{ + rhs.offsets_begin(), rhs.offsets_end(), rhs_point_sorted.begin(), rhs_point_sorted.end()}; + + detail::zero_data_async(output, output + lhs.size(), stream); + auto [tpb, n_blocks] = grid_1d(lhs.num_points()); + detail::pairwise_multipoint_equals_count_kernel<<>>( + lhs, rhs_sorted, output); + + CUSPATIAL_CHECK_CUDA(stream.value()); + + return output + lhs.size(); +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh new file mode 100644 index 000000000..a1066ac42 --- /dev/null +++ b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 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 + +namespace cuspatial { + +/** + * @brief Compute the number of multipoint pairs that are equal. + * + * Given two sets of multipoints, each represented by a range of `vec_2d`s, + * computes the number of pairs of multipoints that are equal. Example: + * + * ``` + * lhs: { {0, 0}, {1, 1}, {2, 2} } + * rhs: { {0, 0}, {1, 1}, {2, 2} } + * count: { 1, 1, 1 } + * + * lhs: { {0, 0} } + * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * count: { 1 } + * + * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * rhs: { {0, 0} } + * count: { 1, 0, 0, 0 } + * ``` + * + * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` + * and the output iterator must be able to accept for storage values of type + * `uint32_t`. + * + * @param[in] lhs_first multipoint_ref of first set of points + * @param[in] rhs_first multipoint_ref of second set of points + * @param[out] count_first: beginning of range of uint32_t counts + * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. + * + * @tparam MultiPointRefA Iterator over multipoint vec_2ds. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam MultiPointRefB Iterator over multipoint vec_2ds. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. + * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. + * + * @return Output iterator to the element past the last count result written. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs_first, + MultiPointRangeB rhs_first, + OutputIt count_first, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); + +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp new file mode 100644 index 000000000..033fa692d --- /dev/null +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, 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 + +namespace cuspatial { + +/** + * @addtogroup spatial + * @brief Compute the number of pairs of multipoints that are equal. + * + * Given two columns of interleaved multipoint coordinates, returns a column + * containing the count of points in each multipoint from `lhs` that are equal + * to a point in the corresponding multipoint in `rhs`. + * + * @param lhs Geometry column with a multipoint of interleaved coordinates + * @param rhs Geometry column with a multipoint of interleaved coordinates + * @param mr Device memory resource used to allocate the returned column. + * @return A column of size len(lhs) containing the count that each point of + * the multipoint in `lhs` is equal to a point in `rhs`. + * + * @throw cuspatial::logic_error if `lhs` and `rhs` have different coordinate + * types. + * + * @example + * ``` + * lhs: 0, 0, 1, 1, 2, 2 + * rhs: 0, 0, 1, 1, 2, 2 + * result: 1, 1, 1 + * + * lhs: 0, 0, 1, 1, 2, 2 + * rhs: 0, 0 + * result: 1, 0, 0 + */ + +/** + */ +std::unique_ptr pairwise_multipoint_equals_count( + geometry_column_view const& lhs, + geometry_column_view const& rhs, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +} // namespace cuspatial diff --git a/cpp/src/spatial/pairwise_multipoint_equals_count.cu b/cpp/src/spatial/pairwise_multipoint_equals_count.cu new file mode 100644 index 000000000..404068421 --- /dev/null +++ b/cpp/src/spatial/pairwise_multipoint_equals_count.cu @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023, 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 "utility/multi_geometry_dispatch.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace cuspatial { +namespace detail { +namespace { + +template +struct pairwise_multipoint_equals_count_impl { + using SizeType = cudf::device_span::size_type; + + template )> + std::unique_ptr operator()(geometry_column_view const& lhs, + geometry_column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto size = lhs.size() / 2; // lhs is a buffer of xy coords + auto type = cudf::data_type(cudf::type_to_id()); + auto result = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); + + auto lhs_range = make_multipoint_range(lhs); + auto rhs_range = make_multipoint_range(rhs); + + cuspatial::pairwise_multipoint_equals_count( + lhs_range, rhs_range, result->mutable_view().begin(), stream); + + return result; + } + + template ), typename... Args> + std::unique_ptr operator()(Args&&...) + + { + CUSPATIAL_FAIL("pairwise_multipoint_equals_count only supports floating point types."); + } +}; + +} // namespace + +template +struct pairwise_multipoint_equals_count { + std::unique_ptr operator()(geometry_column_view lhs, + geometry_column_view rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + return cudf::type_dispatcher( + lhs.coordinate_type(), + pairwise_multipoint_equals_count_impl{}, + lhs, + rhs, + stream, + mr); + } +}; + +} // namespace detail + +std::unique_ptr pairwise_multipoint_equals_count(geometry_column_view const& lhs, + geometry_column_view const& rhs, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS(lhs.geometry_type() == geometry_type_id::POINT && + rhs.geometry_type() == geometry_type_id::POINT, + + "pairwise_multipoint_equals_count only supports POINT geometries" + "for both lhs and rhs"); + + CUSPATIAL_EXPECTS(lhs.coordinate_type() == rhs.coordinate_type(), + "Input geometries must have the same coordinate data types."); + + printf("calling dispatch\n"); + return multi_geometry_double_dispatch( + lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); +} + +} // namespace cuspatial diff --git a/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu new file mode 100644 index 000000000..9caa459e4 --- /dev/null +++ b/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023, 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 + +using namespace cuspatial; +using namespace cuspatial::test; + +template +struct PairwiseMultipointEqualsCountTest : public BaseFixture { + void run_single(std::initializer_list>> lhs_coordinates, + std::initializer_list>> rhs_coordinates, + std::initializer_list expected) + { + auto larray = make_multipoints_array(lhs_coordinates); + auto rarray = make_multipoints_array(rhs_coordinates); + + auto lhs = larray.range(); + auto rhs = rarray.range(); + + auto got = rmm::device_uvector(lhs.size(), stream()); + + auto ret = pairwise_multipoint_equals_count(lhs, rhs, got.begin(), stream()); + + auto d_expected = make_device_vector(expected); + + CUSPATIAL_EXPECT_VECTORS_EQUIVALENT(got, d_expected); + EXPECT_EQ(ret, got.end()); + } +}; + +using TestTypes = ::testing::Types; + +TYPED_TEST_CASE(PairwiseMultipointEqualsCountTest, TestTypes); + +TYPED_TEST(PairwiseMultipointEqualsCountTest, EmptyInput) +{ + using T = TypeParam; + using P = vec_2d; + CUSPATIAL_RUN_TEST(this->run_single, + std::initializer_list>{}, + std::initializer_list>{}, + {}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OneOneEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OneOneNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{1, 0}}}, {0}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OnePairWithTwoEachEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}}}, {{{1, 1}, {0, 0}}}, {2}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OnePairithTwoNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {2, 1}}}, {{{1, 1}, {0, 0}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OnePairThreeOneEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{1, 1}, {1, 1}, {1, 1}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OnePairFourOneEqual) +{ + CUSPATIAL_RUN_TEST( + this->run_single, {{{0, 0}, {1, 1}, {1, 1}, {2, 2}}}, {{{1, 1}, {1, 1}, {1, 1}}}, {2}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OnePair) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}}}, {0}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OneThreeEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {1, 1}, {0, 0}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, OneThreeNotEqual) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{1, 1}}}, {{{0, 0}, {0, 0}, {1, 1}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, ThreeThreeEqualMiddle) +{ + CUSPATIAL_RUN_TEST( + this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{-1, -1}, {1, 1}, {-1, -1}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, ThreeThreeNotEqualMiddle) +{ + CUSPATIAL_RUN_TEST( + this->run_single, {{{0, 0}, {1, 1}, {2, 2}}}, {{{0, 0}, {-1, -1}, {2, 2}}}, {2}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, ThreeThreeNeedRhsMultipoints) +{ + CUSPATIAL_RUN_TEST(this->run_single, + { + {{0, 0}}, + {{1, 1}}, + {{2, 2}}, + }, + {{{0, 0}, {1, 1}}, {{2, 2}, {3, 3}}, {{0, 0}, {1, 1}}}, + {1, 0, 0}); +} diff --git a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp new file mode 100644 index 000000000..44845c4b9 --- /dev/null +++ b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 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 + +using namespace cuspatial; +using namespace cuspatial::test; + +using namespace cudf::test; + +constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; + +template +struct PairwiseMultipointEqualsCountTest : public BaseFixture { + rmm::cuda_stream_view stream() { return cudf::get_default_stream(); } +}; + +// float and double are logically the same but would require separate tests due to precision. +using TestTypes = Types; +TYPED_TEST_CASE(PairwiseMultipointEqualsCountTest, TestTypes); + +TYPED_TEST(PairwiseMultipointEqualsCountTest, Empty) +{ + using T = TypeParam; + auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); + auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); + + auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); + auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); + + auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv); + + auto expected = fixed_width_column_wrapper({}); + + expect_columns_equivalent(expected, output->view(), verbosity); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, InvalidTypes) +{ + using T = TypeParam; + auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); + auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); + + auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); + auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); + + EXPECT_THROW(auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv), + cuspatial::logic_error); +} From f99810f42ebde063e2916eb5fe34299fb055eece Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 5 Apr 2023 17:37:57 -0500 Subject: [PATCH 023/126] Get all tests passing. --- .../detail/pairwise_multipoint_equals_count.cuh | 10 +++++++--- cpp/include/cuspatial_test/test_util.cuh | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh index 45599b5d5..f8a8d090f 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh @@ -45,10 +45,12 @@ void __global__ pairwise_multipoint_equals_count_kernel(MultiPointRangeA lhs, MultiPointRangeB rhs, OutputIt output) { + using T = typename MultiPointRangeA::point_t::value_type; + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.num_points(); idx += gridDim.x * blockDim.x) { auto geometry_id = lhs.geometry_idx_from_point_idx(idx); - auto lhs_point = lhs.point_begin()[idx]; + vec_2d lhs_point = lhs.point_begin()[idx]; auto rhs_multipoint = rhs[geometry_id]; atomicAdd( @@ -85,8 +87,10 @@ OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs, thrust::copy( rmm::exec_policy(stream), rhs.point_begin(), rhs.point_end(), rhs_point_sorted.begin()); - thrust::sort_by_key( - rmm::exec_policy(stream), rhs_keys.begin(), rhs_keys.end(), rhs_point_sorted.begin()); + auto rhs_with_keys = + thrust::make_zip_iterator(thrust::make_tuple(rhs_keys.begin(), rhs_point_sorted.begin())); + + thrust::sort(rmm::exec_policy(stream), rhs_with_keys, rhs_with_keys + rhs.num_points()); auto rhs_sorted = multipoint_range{ rhs.offsets_begin(), rhs.offsets_end(), rhs_point_sorted.begin(), rhs_point_sorted.end()}; diff --git a/cpp/include/cuspatial_test/test_util.cuh b/cpp/include/cuspatial_test/test_util.cuh index 2310e74fb..10434b0e2 100644 --- a/cpp/include/cuspatial_test/test_util.cuh +++ b/cpp/include/cuspatial_test/test_util.cuh @@ -99,5 +99,16 @@ void print_device_range(Iter begin, std::cout << post; } +template +void print_device_vector(Vector const& vec, std::string_view pre = "", std::string_view post = "\n") +{ + using T = typename Vector::value_type; + auto hvec = to_host(vec); + + std::cout << pre; + std::for_each(hvec.begin(), hvec.end(), [](auto const& x) { std::cout << x << " "; }); + std::cout << post; +} + } // namespace test } // namespace cuspatial From 7f606f9b3ee7e1aa0ac3a18823a388b40a1c2e65 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 6 Apr 2023 13:59:50 +0000 Subject: [PATCH 024/126] Fix a bug in column wrapper. --- cpp/src/spatial/pairwise_multipoint_equals_count.cu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/src/spatial/pairwise_multipoint_equals_count.cu b/cpp/src/spatial/pairwise_multipoint_equals_count.cu index 404068421..65ee0a62d 100644 --- a/cpp/src/spatial/pairwise_multipoint_equals_count.cu +++ b/cpp/src/spatial/pairwise_multipoint_equals_count.cu @@ -49,7 +49,7 @@ struct pairwise_multipoint_equals_count_impl { rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { - auto size = lhs.size() / 2; // lhs is a buffer of xy coords + auto size = lhs.size(); // lhs is a buffer of xy coords auto type = cudf::data_type(cudf::type_to_id()); auto result = cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); @@ -105,7 +105,6 @@ std::unique_ptr pairwise_multipoint_equals_count(geometry_column_v CUSPATIAL_EXPECTS(lhs.coordinate_type() == rhs.coordinate_type(), "Input geometries must have the same coordinate data types."); - printf("calling dispatch\n"); return multi_geometry_double_dispatch( lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); } From ec245740f699fcd1adbc86ae0e26cd2b0fe28330 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 6 Apr 2023 21:35:31 +0000 Subject: [PATCH 025/126] Make test untyped.: --- .../pairwise_multipoint_equals_count_test.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp index 44845c4b9..80f5a3c3b 100644 --- a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp +++ b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp @@ -37,15 +37,19 @@ using namespace cudf::test; constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; template -struct PairwiseMultipointEqualsCountTest : public BaseFixture { +struct PairwiseMultipointEqualsCountTestTyped : public BaseFixture { + rmm::cuda_stream_view stream() { return cudf::get_default_stream(); } +}; + +struct PairwiseMultipointEqualsCountTestUntyped : public BaseFixture { rmm::cuda_stream_view stream() { return cudf::get_default_stream(); } }; // float and double are logically the same but would require separate tests due to precision. using TestTypes = Types; -TYPED_TEST_CASE(PairwiseMultipointEqualsCountTest, TestTypes); +TYPED_TEST_CASE(PairwiseMultipointEqualsCountTestTyped, TestTypes); -TYPED_TEST(PairwiseMultipointEqualsCountTest, Empty) +TYPED_TEST(PairwiseMultipointEqualsCountTestTyped, Empty) { using T = TypeParam; auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); @@ -56,16 +60,15 @@ TYPED_TEST(PairwiseMultipointEqualsCountTest, Empty) auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv); - auto expected = fixed_width_column_wrapper({}); + auto expected = fixed_width_column_wrapper({}); expect_columns_equivalent(expected, output->view(), verbosity); } -TYPED_TEST(PairwiseMultipointEqualsCountTest, InvalidTypes) +TEST_F(PairwiseMultipointEqualsCountTestUntyped, InvalidTypes) { - using T = TypeParam; - auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); - auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); + auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); + auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); From b1f620749e43ead84c6bdcbb4dec807ec3b37352 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 13:10:49 +0000 Subject: [PATCH 026/126] Update docs --- .../pairwise_multipoint_equals_count.cuh | 21 +++++++++++-------- .../pairwise_multipoint_equals_count_test.cu | 18 ++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh index a1066ac42..66eab049b 100644 --- a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh @@ -27,22 +27,25 @@ namespace cuspatial { /** * @brief Compute the number of multipoint pairs that are equal. * - * Given two sets of multipoints, each represented by a range of `vec_2d`s, - * computes the number of pairs of multipoints that are equal. Example: + * Given two arrays of multipoints, each represented by a vector of vec_2ds, and + * a vector of counts, this function computes the number of multipoint pairs + * that are equal. * - * ``` - * lhs: { {0, 0}, {1, 1}, {2, 2} } - * rhs: { {0, 0}, {1, 1}, {2, 2} } - * count: { 1, 1, 1 } + * Counts the number of points in the lhs that are contained in the rhs. + * + * @example * * lhs: { {0, 0} } * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } * count: { 1 } - * + * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } * rhs: { {0, 0} } - * count: { 1, 0, 0, 0 } - * ``` + * count: { 1 } + + * lhs: { { {3, 3}, {3, 3}, {0, 0} }, { {0, 0}, {1, 1}, {2, 2} }, { {0, 0} } } + * rhs: { { {0, 0}, {2, 2}, {1, 1} }, { {2, 2}, {0, 0}, {1, 1} }, { {1, 1} } } + * count: { 1, 3, 0 } * * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` * and the output iterator must be able to accept for storage values of type diff --git a/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu b/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu index 9caa459e4..221530c77 100644 --- a/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu +++ b/cpp/tests/experimental/spatial/pairwise_multipoint_equals_count_test.cu @@ -68,6 +68,24 @@ TYPED_TEST(PairwiseMultipointEqualsCountTest, EmptyInput) {}); } +TYPED_TEST(PairwiseMultipointEqualsCountTest, ExampleOne) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}, {1, 1}, {2, 2}, {3, 3}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, ExampleTwo) +{ + CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}, {1, 1}, {2, 2}, {3, 3}}}, {{{0, 0}}}, {1}); +} + +TYPED_TEST(PairwiseMultipointEqualsCountTest, ExampleThree) +{ + CUSPATIAL_RUN_TEST(this->run_single, + {{{3, 3}, {3, 3}, {0, 0}}, {{0, 0}, {1, 1}, {2, 2}}, {{0, 0}}}, + {{{0, 0}, {2, 2}, {1, 1}}, {{2, 2}, {0, 0}, {1, 1}}, {{1, 1}}}, + {1, 3, 0}); +} + TYPED_TEST(PairwiseMultipointEqualsCountTest, OneOneEqual) { CUSPATIAL_RUN_TEST(this->run_single, {{{0, 0}}}, {{{0, 0}}}, {1}); From 0050be0800730774d295a0c94d588dcab1045c02 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 13:36:02 +0000 Subject: [PATCH 027/126] Transfer python updates from later branch. --- .../cuspatial/experimental/ranges/range.cuh | 4 - .../pairwise_multipoint_equals_count.hpp | 46 ++++--- .../cuspatial/cuspatial/_lib/CMakeLists.txt | 2 +- .../_lib/allpairs_multipoint_equals_count.pyx | 30 ----- .../cpp/allpairs_multipoint_equals_count.pxd | 13 -- .../cpp/pairwise_multipoint_equals_count.pxd | 17 +++ .../_lib/pairwise_multipoint_equals_count.pyx | 43 +++++++ .../cuspatial/core/binops/equals_count.py | 12 +- .../tests/binops/test_equals_count.py | 112 ++++++++++++------ 9 files changed, 173 insertions(+), 106 deletions(-) delete mode 100644 python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx delete mode 100644 python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd create mode 100644 python/cuspatial/cuspatial/_lib/cpp/pairwise_multipoint_equals_count.pxd create mode 100644 python/cuspatial/cuspatial/_lib/pairwise_multipoint_equals_count.pyx diff --git a/cpp/include/cuspatial/experimental/ranges/range.cuh b/cpp/include/cuspatial/experimental/ranges/range.cuh index b301e23aa..923580fe2 100644 --- a/cpp/include/cuspatial/experimental/ranges/range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/range.cuh @@ -49,10 +49,6 @@ class range { auto CUSPATIAL_HOST_DEVICE begin() { return _begin; } /// Return the end iterator to the range auto CUSPATIAL_HOST_DEVICE end() { return _end; } - /// Return the start const iterator to the range - auto CUSPATIAL_HOST_DEVICE cbegin() const { return _begin; } - /// Return the end const iterator to the range - auto CUSPATIAL_HOST_DEVICE cend() const { return _end; } /// Return the size of the range auto CUSPATIAL_HOST_DEVICE size() { return thrust::distance(_begin, _end); } diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index 033fa692d..fc61198f5 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -28,33 +28,43 @@ namespace cuspatial { /** * @addtogroup spatial - * @brief Compute the number of pairs of multipoints that are equal. + * @brief Compute the number of points within pairs of multipoints that are equal. * - * Given two columns of interleaved multipoint coordinates, returns a column - * containing the count of points in each multipoint from `lhs` that are equal - * to a point in the corresponding multipoint in `rhs`. + * Given two columns of multipoint arrays, returns a column containing the count + * of points in each multipoint from `lhs` that are equal to a point in the + * corresponding multipoint in `rhs`. * - * @param lhs Geometry column with a multipoint of interleaved coordinates - * @param rhs Geometry column with a multipoint of interleaved coordinates + * @param lhs Geometry column of multipoints with interleaved coordinates + * @param rhs Geometry column of multipoints with interleaved coordinates * @param mr Device memory resource used to allocate the returned column. - * @return A column of size len(lhs) containing the count that each point of - * the multipoint in `lhs` is equal to a point in `rhs`. + * @return A column of size len(lhs) containing the number of points in each + * multipoint from `lhs` that are equal to a point in the corresponding + * multipoint in `rhs`. * * @throw cuspatial::logic_error if `lhs` and `rhs` have different coordinate - * types. + * types or lengths. * * @example * ``` - * lhs: 0, 0, 1, 1, 2, 2 - * rhs: 0, 0, 1, 1, 2, 2 - * result: 1, 1, 1 - * - * lhs: 0, 0, 1, 1, 2, 2 - * rhs: 0, 0 - * result: 1, 0, 0 - */ + * lhs: MultiPoint(0, 0) + * rhs: MultiPoint((0, 0), (1, 1), (2, 2), (3, 3)) + * result: 1 -/** + * lhs: MultiPoint((0, 0), (1, 1), (2, 2), (3, 3)) + * rhs: MultiPoint((0, 0)) + * result: 1 + + * lhs: ( + * MultiPoint((3, 3), (3, 3), (0, 0)), + * MultiPoint((0, 0), (1, 1), (2, 2)), + * MultiPoint((0, 0)) + * ) + * rhs: ( + * MultiPoint((0, 0), (2, 2), (1, 1)), + * MultiPoint((2, 2), (0, 0), (1, 1)), + * MultiPoint((1, 1)) + * ) + * result: ( 1, 3, 0 ) */ std::unique_ptr pairwise_multipoint_equals_count( geometry_column_view const& lhs, diff --git a/python/cuspatial/cuspatial/_lib/CMakeLists.txt b/python/cuspatial/cuspatial/_lib/CMakeLists.txt index 522591f19..6a0f0d012 100644 --- a/python/cuspatial/cuspatial/_lib/CMakeLists.txt +++ b/python/cuspatial/cuspatial/_lib/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources - allpairs_multipoint_equals_count.pyx + pairwise_multipoint_equals_count.pyx distance.pyx hausdorff.pyx intersection.pyx diff --git a/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx b/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx deleted file mode 100644 index b45e15bac..000000000 --- a/python/cuspatial/cuspatial/_lib/allpairs_multipoint_equals_count.pyx +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2023, 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.allpairs_multipoint_equals_count cimport ( - allpairs_multipoint_equals_count as cpp_allpairs_multipoint_equals_count, -) - - -def allpairs_multipoint_equals_count( - Column _lhs, - Column _rhs, -): - cdef column_view lhs = _lhs.view() - cdef column_view rhs = _rhs.view() - - cdef unique_ptr[column] result - - with nogil: - result = move( - cpp_allpairs_multipoint_equals_count( - lhs, - rhs, - ) - ) - - return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd b/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd deleted file mode 100644 index e5982cab0..000000000 --- a/python/cuspatial/cuspatial/_lib/cpp/allpairs_multipoint_equals_count.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. - -from libcpp.memory cimport unique_ptr - -from cudf._lib.column cimport column, column_view - - -cdef extern from "cuspatial/allpairs_multipoint_equals_count.hpp" \ - namespace "cuspatial" nogil: - cdef unique_ptr[column] allpairs_multipoint_equals_count( - const column_view & lhs, - const column_view & rhs, - ) except + diff --git a/python/cuspatial/cuspatial/_lib/cpp/pairwise_multipoint_equals_count.pxd b/python/cuspatial/cuspatial/_lib/cpp/pairwise_multipoint_equals_count.pxd new file mode 100644 index 000000000..5af573bd0 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/cpp/pairwise_multipoint_equals_count.pxd @@ -0,0 +1,17 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.column cimport column, column_view + +from cuspatial._lib.cpp.column.geometry_column_view cimport ( + geometry_column_view, +) + + +cdef extern from "cuspatial/pairwise_multipoint_equals_count.hpp" \ + namespace "cuspatial" nogil: + cdef unique_ptr[column] pairwise_multipoint_equals_count( + const geometry_column_view lhs, + const geometry_column_view rhs, + ) except + diff --git a/python/cuspatial/cuspatial/_lib/pairwise_multipoint_equals_count.pyx b/python/cuspatial/cuspatial/_lib/pairwise_multipoint_equals_count.pyx new file mode 100644 index 000000000..aea144568 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/pairwise_multipoint_equals_count.pyx @@ -0,0 +1,43 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from libcpp.memory cimport make_shared, shared_ptr, unique_ptr +from libcpp.utility cimport move + +from cudf._lib.column cimport Column, column + +from cuspatial._lib.cpp.column.geometry_column_view cimport ( + geometry_column_view, +) +from cuspatial._lib.cpp.pairwise_multipoint_equals_count cimport ( + pairwise_multipoint_equals_count as cpp_pairwise_multipoint_equals_count, +) +from cuspatial._lib.cpp.types cimport collection_type_id, geometry_type_id + + +def pairwise_multipoint_equals_count( + Column _lhs, + Column _rhs, +): + cdef shared_ptr[geometry_column_view] lhs = \ + make_shared[geometry_column_view]( + _lhs.view(), + collection_type_id.MULTI, + geometry_type_id.POINT) + + cdef shared_ptr[geometry_column_view] rhs = \ + make_shared[geometry_column_view]( + _rhs.view(), + collection_type_id.MULTI, + geometry_type_id.POINT) + + cdef unique_ptr[column] result + + with nogil: + result = move( + cpp_pairwise_multipoint_equals_count( + lhs.get()[0], + rhs.get()[0], + ) + ) + + return Column.from_unique_ptr(move(result)) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index 7e2f1bbc7..c5b955094 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -2,14 +2,14 @@ import cudf -from cuspatial._lib.allpairs_multipoint_equals_count import ( - allpairs_multipoint_equals_count as c_allpairs_multipoint_equals_count, +from cuspatial._lib.pairwise_multipoint_equals_count import ( + pairwise_multipoint_equals_count as c_pairwise_multipoint_equals_count, ) from cuspatial.core.geoseries import GeoSeries from cuspatial.utils.column_utils import contains_only_multipoints -def allpairs_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): +def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): """ Compute the count of times that each multipoint in the first GeoSeries equals each multipoint in the second GeoSeries. @@ -32,8 +32,8 @@ def allpairs_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): if any(not contains_only_multipoints(s) for s in [lhs, rhs]): raise ValueError("Input GeoSeries must contain only multipoints.") - result = c_allpairs_multipoint_equals_count( - lhs.multipoints.xy._column, rhs.multipoints.xy._column - ) + lhs_column = lhs._column.mpoints._column + rhs_column = rhs._column.mpoints._column + result = c_pairwise_multipoint_equals_count(lhs_column, rhs_column) return cudf.Series(result) diff --git a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py index 897975940..5d1ed7a72 100644 --- a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py +++ b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py @@ -4,76 +4,120 @@ import cudf import cuspatial -from cuspatial.core.binops.equals_count import allpairs_multipoint_equals_count +from cuspatial.core.binops.equals_count import pairwise_multipoint_equals_count -def test_allpairs_multipoint_equals_count_one_one_hit(): +def test_pairwise_multipoint_equals_count_one_one_hit(): p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) - got = allpairs_multipoint_equals_count(p1, p2) + got = pairwise_multipoint_equals_count(p1, p2) expected = cudf.Series([1], dtype="uint32") assert_series_equal(got.to_pandas(), expected.to_pandas()) -def test_allpairs_multipoint_equals_count_one_one_miss(): +def test_pairwise_multipoint_equals_count_one_one_miss(): p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) p2 = cuspatial.GeoSeries([MultiPoint([Point(1, 1)])]) - got = allpairs_multipoint_equals_count(p1, p2) + got = pairwise_multipoint_equals_count(p1, p2) expected = cudf.Series([0], dtype="uint32") assert_series_equal(got.to_pandas(), expected.to_pandas()) -def test_allpairs_multipoint_equals_count_three_three_one_mismatch(): +def test_pairwise_multipoint_equals_count_three_three_one_mismatch(): p1 = cuspatial.GeoSeries( - [MultiPoint([Point(0, 0), Point(3, 3), Point(2, 2)])] + [ + MultiPoint([Point(0, 0)]), + MultiPoint([Point(3, 3)]), + MultiPoint([Point(2, 2)]), + ] ) p2 = cuspatial.GeoSeries( - [MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)])] + [ + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + ] ) - got = allpairs_multipoint_equals_count(p1, p2) + got = pairwise_multipoint_equals_count(p1, p2) expected = cudf.Series([1, 0, 1], dtype="uint32") assert_series_equal(got.to_pandas(), expected.to_pandas()) -def test_allpairs_multipoint_equals_count_three_match_two_mismatch(): +def test_pairwise_multipoint_equals_count_three_match_two_mismatch(): p1 = cuspatial.GeoSeries( - [MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)])] + [ + MultiPoint([Point(3, 3)]), + MultiPoint([Point(0, 0)]), + MultiPoint([Point(3, 3)]), + ] ) p2 = cuspatial.GeoSeries( - [MultiPoint([Point(3, 3), Point(1, 1), Point(3, 3)])] + [ + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + ] ) - got = allpairs_multipoint_equals_count(p1, p2) + got = pairwise_multipoint_equals_count(p1, p2) expected = cudf.Series([0, 1, 0], dtype="uint32") assert_series_equal(got.to_pandas(), expected.to_pandas()) -def test_allpairs_multipoint_equals_count_five(): +def test_pairwise_multipoint_equals_count_five(): + p1 = cuspatial.GeoSeries( + [ + MultiPoint([Point(0, 0)]), + MultiPoint([Point(1, 1)]), + MultiPoint([Point(2, 2)]), + MultiPoint([Point(3, 3)]), + MultiPoint([Point(4, 4)]), + ] + ) + p2 = cuspatial.GeoSeries( + [ + MultiPoint([Point(0, 0)]), + MultiPoint([Point(0, 0)]), + MultiPoint([Point(2, 2)]), + MultiPoint([Point(2, 2)]), + MultiPoint([Point(3, 3)]), + ] + ) + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([1, 0, 1, 0, 0], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_pairwise_multipoint_equals_two_and_three(): + p1 = cuspatial.GeoSeries( + [ + MultiPoint([Point(0, 0), Point(1, 1), Point(1, 1)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(1, 1)]), + ] + ) + p2 = cuspatial.GeoSeries( + [ + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + ] + ) + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([3, 3], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_pairwise_multipoint_equals_two_and_three_one_match(): p1 = cuspatial.GeoSeries( [ - MultiPoint( - [ - Point(0, 0), - Point(1, 1), - Point(2, 2), - Point(3, 3), - Point(4, 4), - ] - ) + MultiPoint([Point(0, 0), Point(1, 1), Point(1, 1)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(1, 1)]), ] ) p2 = cuspatial.GeoSeries( [ - MultiPoint( - [ - Point(0, 0), - Point(0, 0), - Point(2, 2), - Point(2, 2), - Point(3, 3), - ] - ) + MultiPoint([Point(0, 0), Point(2, 2), Point(2, 2)]), + MultiPoint([Point(2, 2), Point(2, 2), Point(0, 0)]), ] ) - got = allpairs_multipoint_equals_count(p1, p2) - expected = cudf.Series([2, 0, 2, 1, 0], dtype="uint32") + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([1, 1], dtype="uint32") assert_series_equal(got.to_pandas(), expected.to_pandas()) From c1784148f738d5c1ec0030e783af2de3507cd620 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 13:51:39 +0000 Subject: [PATCH 028/126] Improve .cpp file testing based on length requirement. --- cpp/src/spatial/pairwise_multipoint_equals_count.cu | 3 +++ .../pairwise_multipoint_equals_count_test.cpp | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/cpp/src/spatial/pairwise_multipoint_equals_count.cu b/cpp/src/spatial/pairwise_multipoint_equals_count.cu index 65ee0a62d..48f340024 100644 --- a/cpp/src/spatial/pairwise_multipoint_equals_count.cu +++ b/cpp/src/spatial/pairwise_multipoint_equals_count.cu @@ -105,6 +105,9 @@ std::unique_ptr pairwise_multipoint_equals_count(geometry_column_v CUSPATIAL_EXPECTS(lhs.coordinate_type() == rhs.coordinate_type(), "Input geometries must have the same coordinate data types."); + CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), + "Input geometries must have the same number of points."); + return multi_geometry_double_dispatch( lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); } diff --git a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp index 80f5a3c3b..1d2923e4b 100644 --- a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp +++ b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp @@ -65,6 +65,19 @@ TYPED_TEST(PairwiseMultipointEqualsCountTestTyped, Empty) expect_columns_equivalent(expected, output->view(), verbosity); } +TYPED_TEST(PairwiseMultipointEqualsCountTestTyped, InvalidLength) +{ + using T = TypeParam; + auto [ptype, lhs] = make_point_column({0, 1}, {0.0, 0.0}, this->stream()); + auto [pytpe, rhs] = make_point_column({0, 1, 2}, {1.0, 1.0, 0.0, 0.0}, this->stream()); + + auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); + auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); + + EXPECT_THROW(auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv), + cuspatial::logic_error); +} + TEST_F(PairwiseMultipointEqualsCountTestUntyped, InvalidTypes) { auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); From 719d065aa0a0d58928efed89fdab010dbd5c0581 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 13:52:47 +0000 Subject: [PATCH 029/126] Tweak error message. --- cpp/src/spatial/pairwise_multipoint_equals_count.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/spatial/pairwise_multipoint_equals_count.cu b/cpp/src/spatial/pairwise_multipoint_equals_count.cu index 48f340024..e855e9c8b 100644 --- a/cpp/src/spatial/pairwise_multipoint_equals_count.cu +++ b/cpp/src/spatial/pairwise_multipoint_equals_count.cu @@ -106,7 +106,7 @@ std::unique_ptr pairwise_multipoint_equals_count(geometry_column_v "Input geometries must have the same coordinate data types."); CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), - "Input geometries must have the same number of points."); + "Input geometries must have the same number of multipoints."); return multi_geometry_double_dispatch( lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); From 05090bb6d68ae203bc7b59cf07e1169a276742d8 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 14:00:54 +0000 Subject: [PATCH 030/126] Update equals_count docs and tests. --- .../cuspatial/core/binops/equals_count.py | 53 +++++++++++++++++-- .../tests/binops/test_equals_count.py | 36 +++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index c5b955094..d0e7e8371 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -10,9 +10,16 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): - """ - Compute the count of times that each multipoint in the first GeoSeries - equals each multipoint in the second GeoSeries. + """Compute the number of points in each multipoint in the lhs that are + equal to points in the corresponding multipoint in the rhs. + + For each point that exists in a multipoint in the lhs, search the + corresponding multipoint in the rhs for a point that is equal to the + point in the lhs. If a point is found, increment the count for that + multipoint in the lhs. + + Counts the number of points in each multipoint in the lhs that are + equal to points in the corresponding multipoint in the rhs. Parameters ---------- @@ -21,10 +28,48 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): multipoint : GeoSeries A GeoSeries of multipoints. + Examples + -------- + >>> import cudf + >>> import cuspatial + >>> from shapely.geometry import MultiPoint + >>> p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + >>> p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + >>> cuspatial.pairwise_multipoint_equals_count(p1, p2) + 0 1 + dtype: uint32 + + >>> p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + >>> p2 = cuspatial.GeoSeries([MultiPoint([Point(1, 1)])]) + >>> cuspatial.pairwise_multipoint_equals_count(p1, p2) + 0 0 + dtype: uint32 + + >>> p1 = cuspatial.GeoSeries( + ... [ + ... MultiPoint([Point(0, 0)]), + ... MultiPoint([Point(3, 3)]), + ... MultiPoint([Point(2, 2)]), + ... ] + ... ) + >>> p2 = cuspatial.GeoSeries( + ... [ + ... MultiPoint([Point(2, 2), Point(0, 0), Point(1, 1)]), + ... MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + ... MultiPoint([Point(1, 1), Point(2, 2), Point(0, 0)]), + ... ] + ... ) + >>> cuspatial.pairwise_multipoint_equals_count(p1, p2) + 0 1 + 1 0 + 2 1 + dtype: uint32 + Returns ------- count : cudf.Series - A Series of counts of multipoint equality. + A Series of the number of points in each multipoint in the lhs that + are equal to points in the corresponding multipoint in the rhs. """ if len(lhs) == 0: return cudf.Series([]) diff --git a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py index 5d1ed7a72..07994d8d9 100644 --- a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py +++ b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py @@ -7,6 +7,42 @@ from cuspatial.core.binops.equals_count import pairwise_multipoint_equals_count +def test_pairwise_multipoint_equals_count_example_1(): + p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([1], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_pairwise_multipoint_equals_count_example_2(): + p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) + p2 = cuspatial.GeoSeries([MultiPoint([Point(1, 1)])]) + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([0], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + +def test_pairwise_multipoint_equals_count_example_3(): + p1 = cuspatial.GeoSeries( + [ + MultiPoint([Point(0, 0)]), + MultiPoint([Point(3, 3)]), + MultiPoint([Point(2, 2)]), + ] + ) + p2 = cuspatial.GeoSeries( + [ + MultiPoint([Point(2, 2), Point(0, 0), Point(1, 1)]), + MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), + MultiPoint([Point(1, 1), Point(2, 2), Point(0, 0)]), + ] + ) + got = pairwise_multipoint_equals_count(p1, p2) + expected = cudf.Series([1, 0, 1], dtype="uint32") + assert_series_equal(got.to_pandas(), expected.to_pandas()) + + def test_pairwise_multipoint_equals_count_one_one_hit(): p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) From ca1218dcd77d525e9e7dd986ad255ff0dcd20981 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 14:02:37 +0000 Subject: [PATCH 031/126] Minor docs tweak. --- python/cuspatial/cuspatial/core/binops/equals_count.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index d0e7e8371..3813829fb 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -23,14 +23,13 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): Parameters ---------- - multipoint : GeoSeries + lhs : GeoSeries A GeoSeries of multipoints. - multipoint : GeoSeries + rhs : GeoSeries A GeoSeries of multipoints. Examples -------- - >>> import cudf >>> import cuspatial >>> from shapely.geometry import MultiPoint >>> p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) From 76c8aac9c9cc4b03afebac813a012c6c5d12b952 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 14:03:35 +0000 Subject: [PATCH 032/126] Remove repeat tests. --- .../tests/binops/test_equals_count.py | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py index 07994d8d9..120cf0f21 100644 --- a/python/cuspatial/cuspatial/tests/binops/test_equals_count.py +++ b/python/cuspatial/cuspatial/tests/binops/test_equals_count.py @@ -43,42 +43,6 @@ def test_pairwise_multipoint_equals_count_example_3(): assert_series_equal(got.to_pandas(), expected.to_pandas()) -def test_pairwise_multipoint_equals_count_one_one_hit(): - p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) - p2 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) - got = pairwise_multipoint_equals_count(p1, p2) - expected = cudf.Series([1], dtype="uint32") - assert_series_equal(got.to_pandas(), expected.to_pandas()) - - -def test_pairwise_multipoint_equals_count_one_one_miss(): - p1 = cuspatial.GeoSeries([MultiPoint([Point(0, 0)])]) - p2 = cuspatial.GeoSeries([MultiPoint([Point(1, 1)])]) - got = pairwise_multipoint_equals_count(p1, p2) - expected = cudf.Series([0], dtype="uint32") - assert_series_equal(got.to_pandas(), expected.to_pandas()) - - -def test_pairwise_multipoint_equals_count_three_three_one_mismatch(): - p1 = cuspatial.GeoSeries( - [ - MultiPoint([Point(0, 0)]), - MultiPoint([Point(3, 3)]), - MultiPoint([Point(2, 2)]), - ] - ) - p2 = cuspatial.GeoSeries( - [ - MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), - MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), - MultiPoint([Point(0, 0), Point(1, 1), Point(2, 2)]), - ] - ) - got = pairwise_multipoint_equals_count(p1, p2) - expected = cudf.Series([1, 0, 1], dtype="uint32") - assert_series_equal(got.to_pandas(), expected.to_pandas()) - - def test_pairwise_multipoint_equals_count_three_match_two_mismatch(): p1 = cuspatial.GeoSeries( [ From 5c781461ab00ac4e06c0c57a0a6f8ae4cae97f03 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 7 Apr 2023 14:53:07 +0000 Subject: [PATCH 033/126] Fix docs --- .../experimental/pairwise_multipoint_equals_count.cuh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh index 66eab049b..1540a45d6 100644 --- a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh @@ -51,14 +51,14 @@ namespace cuspatial { * and the output iterator must be able to accept for storage values of type * `uint32_t`. * - * @param[in] lhs_first multipoint_ref of first set of points - * @param[in] rhs_first multipoint_ref of second set of points + * @param[in] lhs_first multipoint_range of first array of multipoints + * @param[in] rhs_first multipoint_range of second array of multipoints * @param[out] count_first: beginning of range of uint32_t counts * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. * - * @tparam MultiPointRefA Iterator over multipoint vec_2ds. Must meet the requirements of + * @tparam MultiPointRangeA Iterator over multipoints. Must meet the requirements of * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - * @tparam MultiPointRefB Iterator over multipoint vec_2ds. Must meet the requirements of + * @tparam MultiPointRangeB Iterator over multipoints. Must meet the requirements of * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. From 3d28ee92dce6e1b63aebbe63040bd2adeaa5154a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:32:57 -0500 Subject: [PATCH 034/126] Update cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- .../experimental/detail/pairwise_multipoint_equals_count.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh index f8a8d090f..e47ba567a 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh @@ -73,7 +73,7 @@ OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs, static_assert(is_same_floating_point(), "Origin and input must have the same base floating point type."); - CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), "Input should have the same number of pairs."); + CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), "lhs and rhs inputs should have the same size."); if (lhs.size() == 0) return output; From 32050f91628430f42cfe8321f3a0b6774f9b68dd Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 14:33:07 +0000 Subject: [PATCH 035/126] Doc tweak --- python/cuspatial/cuspatial/core/binops/equals_count.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index 3813829fb..f630c6528 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -18,9 +18,6 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): point in the lhs. If a point is found, increment the count for that multipoint in the lhs. - Counts the number of points in each multipoint in the lhs that are - equal to points in the corresponding multipoint in the rhs. - Parameters ---------- lhs : GeoSeries From ac9b7559bce2944cd86f5b21bb35ae27e7b942a0 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:33:30 -0500 Subject: [PATCH 036/126] Update cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index fc61198f5..afd19be3d 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -28,7 +28,7 @@ namespace cuspatial { /** * @addtogroup spatial - * @brief Compute the number of points within pairs of multipoints that are equal. + * @brief Count the number of equal points in pairs of multipoints.. * * Given two columns of multipoint arrays, returns a column containing the count * of points in each multipoint from `lhs` that are equal to a point in the From d904762f3080d2bb1b3c33871587246bb5e7de9f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:33:50 -0500 Subject: [PATCH 037/126] Update cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index afd19be3d..f9451616c 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -30,9 +30,9 @@ namespace cuspatial { * @addtogroup spatial * @brief Count the number of equal points in pairs of multipoints.. * - * Given two columns of multipoint arrays, returns a column containing the count - * of points in each multipoint from `lhs` that are equal to a point in the - * corresponding multipoint in `rhs`. + * Given two columns of multipoints, returns a column containing the count + * of points in each multipoint from `lhs` that exist in the corresponding + * multipoint in `rhs`. * * @param lhs Geometry column of multipoints with interleaved coordinates * @param rhs Geometry column of multipoints with interleaved coordinates From 102915b505f61794dabd236b5e2adea24d11d9aa Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:34:01 -0500 Subject: [PATCH 038/126] Update python/cuspatial/cuspatial/core/binops/equals_count.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- python/cuspatial/cuspatial/core/binops/equals_count.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index 3813829fb..e9ba8efe4 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -13,7 +13,7 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): """Compute the number of points in each multipoint in the lhs that are equal to points in the corresponding multipoint in the rhs. - For each point that exists in a multipoint in the lhs, search the + For each point in a multipoint in the lhs, search the corresponding multipoint in the rhs for a point that is equal to the point in the lhs. If a point is found, increment the count for that multipoint in the lhs. From fbcd2bd403dd51e7421241ed4ec0c5414cdeb141 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:35:33 -0500 Subject: [PATCH 039/126] Update python/cuspatial/cuspatial/core/binops/equals_count.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- python/cuspatial/cuspatial/core/binops/equals_count.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index bf8904d32..80f63027e 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -10,8 +10,8 @@ def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): - """Compute the number of points in each multipoint in the lhs that are - equal to points in the corresponding multipoint in the rhs. + """Compute the number of points in each multipoint in the lhs that exist + in the corresponding multipoint in the rhs. For each point in a multipoint in the lhs, search the corresponding multipoint in the rhs for a point that is equal to the From f7c2c98412f9d7aa1ebc80ce638cc80fe05822b0 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 09:36:28 -0500 Subject: [PATCH 040/126] Update cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- .../experimental/pairwise_multipoint_equals_count.cuh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh index 1540a45d6..5e9c800d7 100644 --- a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh @@ -27,11 +27,8 @@ namespace cuspatial { /** * @brief Compute the number of multipoint pairs that are equal. * - * Given two arrays of multipoints, each represented by a vector of vec_2ds, and - * a vector of counts, this function computes the number of multipoint pairs - * that are equal. - * - * Counts the number of points in the lhs that are contained in the rhs. + * Given two ranges of multipoints, this function counts points in the left-hand + * multipoint that exist in the corresponding right-hand multipoint. * * @example * From 89a54c5e2d4c6231c6f271207a787d84d0ee6d12 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 14:38:55 +0000 Subject: [PATCH 041/126] Address comment about multipoint_range docs. --- .../experimental/pairwise_multipoint_equals_count.cuh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh index 1540a45d6..ad520f88a 100644 --- a/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh @@ -47,8 +47,9 @@ namespace cuspatial { * rhs: { { {0, 0}, {2, 2}, {1, 1} }, { {2, 2}, {0, 0}, {1, 1} }, { {1, 1} } } * count: { 1, 3, 0 } * - * @note All input iterators must have a `value_type` of `cuspatial::vec_2d` - * and the output iterator must be able to accept for storage values of type + * @note All input iterators must conform to the specification defined by + * `multipoint_range.cuh` and the output iterator must be able to accept for + * storage values of type * `uint32_t`. * * @param[in] lhs_first multipoint_range of first array of multipoints From e63c5949e7024d149d0ac569d92a5b6ab8ab35eb Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 15:09:46 +0000 Subject: [PATCH 042/126] Tweaking because of format issue. --- cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index f9451616c..8dca3185e 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -30,9 +30,9 @@ namespace cuspatial { * @addtogroup spatial * @brief Count the number of equal points in pairs of multipoints.. * - * Given two columns of multipoints, returns a column containing the count - * of points in each multipoint from `lhs` that exist in the corresponding - * multipoint in `rhs`. + * Given two columns of multipoints, returns a column containing the + * count of points in each multipoint from `lhs` that exist in the + * corresponding multipoint in `rhs`. * * @param lhs Geometry column of multipoints with interleaved coordinates * @param rhs Geometry column of multipoints with interleaved coordinates From 66498cfdfe7f3d4fc92de935841e210ebe1dff2a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 15:26:36 +0000 Subject: [PATCH 043/126] Tweaking problem with docs. --- cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index 8dca3185e..cf436edeb 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -30,10 +30,6 @@ namespace cuspatial { * @addtogroup spatial * @brief Count the number of equal points in pairs of multipoints.. * - * Given two columns of multipoints, returns a column containing the - * count of points in each multipoint from `lhs` that exist in the - * corresponding multipoint in `rhs`. - * * @param lhs Geometry column of multipoints with interleaved coordinates * @param rhs Geometry column of multipoints with interleaved coordinates * @param mr Device memory resource used to allocate the returned column. From ce1df18de595e67d8516e5bdc529b47d02f3c5cf Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 16:05:40 +0000 Subject: [PATCH 044/126] Repair docs? --- cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp index cf436edeb..8dca3185e 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.hpp @@ -30,6 +30,10 @@ namespace cuspatial { * @addtogroup spatial * @brief Count the number of equal points in pairs of multipoints.. * + * Given two columns of multipoints, returns a column containing the + * count of points in each multipoint from `lhs` that exist in the + * corresponding multipoint in `rhs`. + * * @param lhs Geometry column of multipoints with interleaved coordinates * @param rhs Geometry column of multipoints with interleaved coordinates * @param mr Device memory resource used to allocate the returned column. From 07401fe9f6426e0651cc3a4a404a9cf0154b4226 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:19:15 +0000 Subject: [PATCH 045/126] CR at beginning of file typo --- .../cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd b/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd index e7d246e8a..71424a23f 100644 --- a/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd +++ b/python/cuspatial/cuspatial/_lib/cpp/linestring_intersection.pxd @@ -1,5 +1,4 @@ -# -Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr From 86ad906efa98c1e324166ee7c32f3d8627c08c01 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:24:56 +0000 Subject: [PATCH 046/126] First commit of test dispatch branch. --- .../tests/binpreds/binpred_test_dispatch.py | 520 ++++++++++++++++++ ...summarize_binpred_test_dispatch_results.py | 11 + .../binpreds/test_binpred_test_dispatch.py | 110 ++++ python/cuspatial/cuspatial/tests/conftest.py | 24 + 4 files changed, 665 insertions(+) create mode 100644 python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py new file mode 100644 index 000000000..68e9b5a60 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -0,0 +1,520 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +import pytest +from shapely.geometry import LineString, Point, Polygon + +import cuspatial + +"""Test Dispatch""" + +"""This file is used to generate tests for all possible combinations +of geometry types and binary predicates. The tests are generated +using the fixtures defined in this file. The fixtures are combined +in the test function in `test_binpreds_test_dispatch.py` to make +a Tuple: (predicate, feature-name, feature-lhs, feature-rhs). The +feature-name is not used in the tests but is useful for debugging. +""" + + +"""The collection of all supported binary predicates""" + + +@pytest.fixture( + params=[ + "contains", + "geom_equals", + "intersects", + "covers", + "crosses", + "disjoint", + "overlaps", + "touches", + "within", + ] +) +def predicate(request): + return request.param + + +"""The fundamental set of tests. This section is dispatched based +on the feature type. Each feature pairing has a specific set of +comparisons that need to be performed to cover the entire test +space. This section will be contains specific feature representations +that cover all possible geometric combinations.""" + + +point_polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]) +features = { + "point-point-disjoint": ( + """Two points apart.""", + Point(0.0, 0.0), + Point(1.0, 0.0), + ), + "point-point-equal": ( + """Two points together.""", + Point(0.0, 0.0), + Point(0.0, 0.0), + ), + "point-linestring-disjoint": ( + """Point and linestring are disjoint.""", + Point(0.0, 0.0), + LineString([(1.0, 0.0), (2.0, 0.0)]), + ), + "point-linestring-point": ( + """Point and linestring share a point.""", + Point(0.0, 0.0), + LineString([(0.0, 0.0), (2.0, 0.0)]), + ), + "point-linestring-edge": ( + """Point and linestring intersect.""", + Point(0.5, 0.0), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "point-polygon-disjoint": ( + """Point and polygon are disjoint.""", + Point(-0.5, 0.5), + point_polygon, + ), + "point-polygon-point": ( + """Point and polygon share a point.""", + Point(0.0, 0.0), + point_polygon, + ), + "point-polygon-edge": ( + """Point and polygon intersect.""", + Point(0.5, 0.0), + point_polygon, + ), + "point-polygon-in": ( + """Point is in polygon interior.""", + Point(0.5, 0.5), + point_polygon, + ), + "linestring-linestring-disjoint": ( + """ + x---x + + x---x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ), + "linestring-linestring-same": ( + """ + x---x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "linestring-linestring-touches": ( + """ + x + | + | + | + x---x + """, + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (1.0, 0.0)]), + ), + "linestring-linestring-touch-edge": ( + """ + x + | + | + | + x-x-x + """, + LineString([(0.0, 0.0), (1.0, 0.0)]), + LineString([(0.5, 0.0), (0.5, 1.0)]), + ), + "linestring-linestring-crosses": ( + """ + x + | + x-|-x + | + x + """, + LineString([(0.5, 0.0), (0.5, 1.0)]), + LineString([(0.0, 0.5), (1.0, 0.5)]), + ), + "linestring-polygon-disjoint": ( + """ + point_polygon above is drawn as + ----- + | | + | | + | | + ----- + and the corresponding linestring is drawn as + x---x + or + x + | + | + | + x + """ + """ + x ----- + | | | + | | | + | | | + x ----- + """, + LineString([(-0.5, 0.0), (-0.5, 1.0)]), + point_polygon, + ), + "linestring-polygon-touch-point": ( + """ + x---x---- + | | + | | + | | + ----- + """, + LineString([(-1.0, 0.0), (0.0, 0.0)]), + point_polygon, + ), + "linestring-polygon-touch-edge": ( + """ + ----- + | | + x---x | + | | + ----- + """, + LineString([(-1.0, 0.5), (0.0, 0.5)]), + point_polygon, + ), + "linestring-polygon-overlap-edge": ( + """ + x---- + | | + | | + | | + x---- + """, + LineString([(0.0, 0.0), (0.0, 1.0)]), + point_polygon, + ), + "linestring-polygon-intersect-edge": ( + """ + ----- + | | + | | + | | + x---x-- + """, + LineString([(-0.5, 0.0), (0.5, 0.0)]), + point_polygon, + ), + "linestring-polygon-intersect-inner-edge": ( + """ + ----- + x | + | | + x | + ----- + + The linestring in this case is shorter than the corners of the polygon. + """, + LineString([(0.25, 0.0), (0.75, 0.0)]), + point_polygon, + ), + "linestring-polygon-point-interior": ( + """ + ----x + | /| + | / | + |/ | + x---- + """, + LineString([(0.0, 0.0), (1.0, 1.0)]), + point_polygon, + ), + "linestring-polygon-edge-interior": ( + """ + --x-- + | | | + | | | + | | | + --x-- + """, + LineString([(0.5, 0.0), (0.5, 1.0)]), + point_polygon, + ), + "linestring-polygon-in": ( + """ + ----- + | x | + | | | + | x | + ----- + """, + LineString([(0.5, 0.25), (0.5, 0.75)]), + point_polygon, + ), + "linestring-polygon-in-out": ( + """ + ----- + | | + | x | + | | | + --|-- + | + x + """, + LineString([(0.5, 0.5), (0.5, -0.5)]), + point_polygon, + ), + "linestring-polygon-crosses": ( + """ + x + --|-- + | | | + | | | + | | | + --|-- + x + """, + LineString([(0.5, 1.25), (0.5, -0.25)]), + point_polygon, + ), + "polygon-polygon-disjoint": ( + """ + Polygon polygon tests use a triangle for the lhs and a square for the rhs. + The triangle is drawn as + x---x + | / + | / + |/ + x + + The square is drawn as + + ----- + | | + | | + | | + ----- + """, + Polygon([(0.0, 2.0), (0.0, 3.0), (1.0, 3.0)]), + point_polygon, + ), + "polygon-polygon-touch-point": ( + """ + x---x + | / + | / + |/ + x---- + | | + | | + | | + ----- + """, + Polygon([(0.0, 1.0), (0.0, 2.0), (1.0, 2.0)]), + point_polygon, + ), + "polygon-polygon-touch-edge": ( + """ + x---x + | / + | / + |/ + -x--x + | | + | | + | | + ----- + """, + Polygon([(0.25, 1.0), (0.25, 2.0), (1.25, 2.0)]), + point_polygon, + ), + "polygon-polygon-overlap-edge": ( + """ + x + |\\ + | \\ + | \\ + x---x + | | + | | + | | + ----- + """, + Polygon([(0.0, 1.0), (0.0, 2.0), (1.0, 2.0)]), + point_polygon, + ), + "polygon-polygon-point-inside": ( + """ + x---x + | / + | / + --|/- + | x | + | | + | | + ----- + """, + Polygon([(0.5, 0.5), (0.5, 1.5), (1.5, 1.5)]), + point_polygon, + ), + "polygon-polygon-point-outside": ( + """ + x + -|\\-- + |x-x| + | | + | | + ----- + """, + Polygon([(0.25, 0.75), (0.25, 1.25), (0.75, 0.75)]), + point_polygon, + ), + "polygon-polygon-in-out-point": ( + """ + x + |\\ + --|-x + | |/| + | x | + | | + x---- + """, + Polygon([(0.5, 0.5), (0.5, 1.5), (1.0, 1.0)]), + point_polygon, + ), + "polygon-polygon-in-point-point": ( + """ + x---- + |\\ | + | x | + |/ | + x---- + """, + Polygon([(0.0, 0.0), (0.0, 1.0), (0.5, 0.5)]), + point_polygon, + ), + "polygon-polygon-contained": ( + """ + ----- + | x| + | /|| + |x-x| + ----- + """, + Polygon([(0.25, 0.25), (0.75, 0.75), (0.75, 0.25)]), + point_polygon, + ), + "polygon-polygon-same": ( + """ + x---x + | | + | | + | | + x---x + """, + point_polygon, + point_polygon, + ), +} + +point_point_dispatch_list = [ + "point-point-disjoint", + "point-point-equal", +] + +point_linestring_dispatch_list = [ + "point-linestring-disjoint", + "point-linestring-point", + "point-linestring-edge", +] + +point_polygon_dispatch_list = [ + "point-polygon-disjoint", + "point-polygon-point", + "point-polygon-edge", + "point-polygon-in", +] + +linestring_linestring_dispatch_list = [ + "linestring-linestring-disjoint", + "linestring-linestring-same", + "linestring-linestring-touches", + "linestring-linestring-crosses", +] + +linestring_polygon_dispatch_list = [ + "linestring-polygon-disjoint", + "linestring-polygon-touch-point", + "linestring-polygon-touch-edge", + "linestring-polygon-overlap-edge", + "linestring-polygon-intersect-edge", + "linestring-polygon-intersect-inner-edge", + "linestring-polygon-point-interior", + "linestring-polygon-edge-interior", + "linestring-polygon-in", + "linestring-polygon-crosses", +] + +polygon_polygon_dispatch_list = [ + "polygon-polygon-disjoint", + "polygon-polygon-touch-point", + "polygon-polygon-touch-edge", + "polygon-polygon-overlap-edge", + "polygon-polygon-point-inside", + "polygon-polygon-point-outside", + "polygon-polygon-in-out-point", + "polygon-polygon-in-point-point", + "polygon-polygon-contained", + "polygon-polygon-same", +] + + +def object_dispatch(name_list): + for name in name_list: + yield (name, features[name][0], features[name][1], features[name][2]) + + +type_dispatch = { + (Point, Point): object_dispatch(point_point_dispatch_list), + (Point, LineString): object_dispatch(point_linestring_dispatch_list), + (Point, Polygon): object_dispatch(point_polygon_dispatch_list), + (LineString, LineString): object_dispatch( + linestring_linestring_dispatch_list + ), + (LineString, Polygon): object_dispatch(linestring_polygon_dispatch_list), + (Polygon, Polygon): object_dispatch(polygon_polygon_dispatch_list), +} + + +def simple_test_dispatch(): + for types in type_dispatch: + generator = type_dispatch[types] + for test_name, test_description, lhs, rhs in generator: + yield ( + test_name, + test_description, + cuspatial.GeoSeries( + [ + lhs, + rhs if types[0] == types[1] else lhs, + lhs, + ] + ), + cuspatial.GeoSeries( + [ + rhs, + rhs, + rhs, + ] + ), + ) + + +@pytest.fixture(params=simple_test_dispatch()) +def simple_test(request): + return request.param diff --git a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py new file mode 100644 index 000000000..5efa9493d --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py @@ -0,0 +1,11 @@ +import pandas as pd + +pp = pd.read_csv("predicate_passes.csv") +pf = pd.read_csv("predicate_fails.csv") +fp = pd.read_csv("feature_passes.csv") +ff = pd.read_csv("feature_fails.csv") + +print(pp) +print(pf) +print(fp) +print(ff) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py new file mode 100644 index 000000000..83efa1c99 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from functools import wraps + +import pandas as pd +import pytest +from binpred_test_dispatch import predicate, simple_test # noqa: F401 + +"""Decorator function that xfails a test if an exception is throw +by the test function. Will be removed when all tests are passing.""" + + +def xfail_on_exception(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + pytest.xfail(f"Xfailling due to an exception: {e}") + + return wrapper + + +"""Parameterized test fixture that runs a binary predicate test +for each combination of geometry types and binary predicates.""" + +out_file = open("test_binpred_test_dispatch.log", "w") + + +# @xfail_on_exception # TODO: Remove when all tests are passing +def test_simple_features( + predicate, # noqa: F811 + simple_test, # noqa: F811 + predicate_passes, + predicate_fails, + feature_passes, + feature_fails, + request, +): + try: + (lhs, rhs) = simple_test[2], simple_test[3] + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + pred_fn = getattr(lhs, predicate) + got = pred_fn(rhs) + gpd_pred_fn = getattr(gpdlhs, predicate) + expected = gpd_pred_fn(gpdrhs) + assert (got.values_host == expected.values).all() + try: + predicate_passes[predicate] = ( + 1 + if predicate not in predicate_passes + else predicate_passes[predicate] + 1 + ) + feature_passes[(lhs.column_type, rhs.column_type)] = ( + 1 + if (lhs.column_type, rhs.column_type) not in feature_passes + else feature_passes[(lhs.column_type, rhs.column_type)] + 1 + ) + passes_df = pd.DataFrame( + { + "predicate": list(predicate_passes.keys()), + "predicate_passes": list(predicate_passes.values()), + } + ) + passes_df.to_csv("predicate_passes.csv", index=False) + passes_df = pd.DataFrame( + { + "feature": list(feature_passes.keys()), + "feature_passes": list(feature_passes.values()), + } + ) + passes_df.to_csv("feature_passes.csv", index=False) + print(passes_df) + except Exception as e: + raise ValueError(e) + except Exception as e: + out_file.write( + f"""{predicate}, +------------ +{simple_test[0]}\n{simple_test[1]}\nfailed +test: {request.node.name}\n\n""" + ) + predicate_fails[predicate] = ( + 1 + if predicate not in predicate_fails + else predicate_fails[predicate] + 1 + ) + feature_fails[(lhs.column_type, rhs.column_type)] = ( + 1 + if (lhs.column_type, rhs.column_type) not in feature_fails + else feature_fails[(lhs.column_type, rhs.column_type)] + 1 + ) + # TODO: Uncomment when all tests are passing + predicate_fails_df = pd.DataFrame( + { + "predicate": list(predicate_fails.keys()), + "predicate_fails": list(predicate_fails.values()), + } + ) + predicate_fails_df.to_csv("predicate_fails.csv", index=False) + feature_fails_df = pd.DataFrame( + { + "feature": list(feature_fails.keys()), + "feature_fails": list(feature_fails.values()), + } + ) + feature_fails_df.to_csv("feature_fails.csv", index=False) + raise e # TODO: Remove when all tests are passing. + # pytest.fail(f"Assertion failed: {e}") diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 1c37cee77..ac4fd111a 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -315,3 +315,27 @@ def naturalearth_cities(): @pytest.fixture def naturalearth_lowres(): return gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + + +@pytest.fixture(scope="session") +def predicate_passes(): + data = {} + return data + + +@pytest.fixture(scope="session") +def predicate_fails(): + data = {} + return data + + +@pytest.fixture(scope="session") +def feature_passes(): + data = {} + return data + + +@pytest.fixture(scope="session") +def feature_fails(): + data = {} + return data From 1a0072c1678669516ae574af416d0942f7c19345 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:36:19 +0000 Subject: [PATCH 047/126] Enable xfail. --- .../cuspatial/tests/binpreds/test_binpred_test_dispatch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 83efa1c99..b88fd7fc1 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -27,7 +27,7 @@ def wrapper(*args, **kwargs): out_file = open("test_binpred_test_dispatch.log", "w") -# @xfail_on_exception # TODO: Remove when all tests are passing +@xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 @@ -91,7 +91,6 @@ def test_simple_features( if (lhs.column_type, rhs.column_type) not in feature_fails else feature_fails[(lhs.column_type, rhs.column_type)] + 1 ) - # TODO: Uncomment when all tests are passing predicate_fails_df = pd.DataFrame( { "predicate": list(predicate_fails.keys()), @@ -107,4 +106,3 @@ def test_simple_features( ) feature_fails_df.to_csv("feature_fails.csv", index=False) raise e # TODO: Remove when all tests are passing. - # pytest.fail(f"Assertion failed: {e}") From b2ea9303c20affa4f631fb9f3abacefc11fd2e20 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 18:49:12 +0000 Subject: [PATCH 048/126] Binpred test dispatch documented and ready for review. --- .../tests/binpreds/binpred_test_dispatch.py | 32 ++++++++++--- ...summarize_binpred_test_dispatch_results.py | 5 ++ .../binpreds/test_binpred_test_dispatch.py | 47 +++++++++++++++++-- python/cuspatial/cuspatial/tests/conftest.py | 4 ++ 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 68e9b5a60..40c77b2a7 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -11,14 +11,12 @@ of geometry types and binary predicates. The tests are generated using the fixtures defined in this file. The fixtures are combined in the test function in `test_binpreds_test_dispatch.py` to make -a Tuple: (predicate, feature-name, feature-lhs, feature-rhs). The -feature-name is not used in the tests but is useful for debugging. +a Tuple: (feature-name, feature-description, feature-lhs, +feature-rhs). The feature-name and feature-descriptions are not used +in the test but are used for development and debugging. """ -"""The collection of all supported binary predicates""" - - @pytest.fixture( params=[ "contains", @@ -33,13 +31,14 @@ ] ) def predicate(request): + """The collection of all supported binary predicates""" return request.param """The fundamental set of tests. This section is dispatched based on the feature type. Each feature pairing has a specific set of comparisons that need to be performed to cover the entire test -space. This section will be contains specific feature representations +space. This section contains specific feature representations that cover all possible geometric combinations.""" @@ -117,6 +116,17 @@ def predicate(request): LineString([(0.0, 0.0), (0.0, 1.0)]), LineString([(0.0, 0.0), (1.0, 0.0)]), ), + "linestring-linestring-touch-interior": ( + """ + x x + | / + | / + |/ + x---x + """, + LineString([(0.0, 1.0), (0.0, 0.0), (1.0, 0.0)]), + LineString([(0.0, 0.0), (1.0, 1.0)]), + ), "linestring-linestring-touch-edge": ( """ x @@ -475,12 +485,18 @@ def predicate(request): def object_dispatch(name_list): + """Generate a list of test cases for a given set of test names.""" for name in name_list: yield (name, features[name][0], features[name][1], features[name][2]) type_dispatch = { - (Point, Point): object_dispatch(point_point_dispatch_list), + """A dictionary of test cases for each geometry type combination. + Still needs MultiPoint."""( + Point, Point + ): object_dispatch( + point_point_dispatch_list + ), (Point, LineString): object_dispatch(point_linestring_dispatch_list), (Point, Polygon): object_dispatch(point_polygon_dispatch_list), (LineString, LineString): object_dispatch( @@ -492,6 +508,7 @@ def object_dispatch(name_list): def simple_test_dispatch(): + """Generates a list of test cases for each geometry type combination.""" for types in type_dispatch: generator = type_dispatch[types] for test_name, test_description, lhs, rhs in generator: @@ -517,4 +534,5 @@ def simple_test_dispatch(): @pytest.fixture(params=simple_test_dispatch()) def simple_test(request): + """Generates a unique test case for each geometry type combination.""" return request.param diff --git a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py index 5efa9493d..38742c7ba 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py +++ b/python/cuspatial/cuspatial/tests/binpreds/summarize_binpred_test_dispatch_results.py @@ -1,3 +1,8 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +"""Prints a summary of the results of the binary predicate test dispatch. +""" + import pandas as pd pp = pd.read_csv("predicate_passes.csv") diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index b88fd7fc1..e176b9240 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -21,9 +21,7 @@ def wrapper(*args, **kwargs): return wrapper -"""Parameterized test fixture that runs a binary predicate test -for each combination of geometry types and binary predicates.""" - +# In the below file, all failing tests are recorded with visualizations. out_file = open("test_binpred_test_dispatch.log", "w") @@ -37,6 +35,42 @@ def test_simple_features( feature_fails, request, ): + """Parameterized test fixture that runs a binary predicate test + for each combination of geometry types and binary predicates. + + Uses four fixtures from `conftest.py` to store the number of times + each binary predicate has passed and failed, and the number of times + each combination of geometry types has passed and failed. These + results are saved to CSV files after each test. + + Uses the @xfail_on_exception decorator to mark a test as xfailed + if an exception is thrown. This is a temporary measure to allow + the test suite to run to completion while we work on fixing the + failing tests. + + Parameters + ---------- + predicate : str + The name of the binary predicate to test. + simple_test : tuple + A tuple containing the name of the test, a docstring that + describes the test, and the left and right geometry objects. + predicate_passes : dict + A dictionary fixture containing the number of times each binary + predicate has passed. + predicate_fails : dict + A dictionary fixture containing the number of times each binary + predicate has failed. + feature_passes : dict + A dictionary fixture containing the number of times each combination + of geometry types has passed. + feature_fails : dict + A dictionary fixture containing the number of times each combination + of geometry types has failed. + request : pytest.FixtureRequest + The pytest request object. Used to print the test name in + diagnostic output. + """ try: (lhs, rhs) = simple_test[2], simple_test[3] gpdlhs = lhs.to_geopandas() @@ -46,7 +80,10 @@ def test_simple_features( gpd_pred_fn = getattr(gpdlhs, predicate) expected = gpd_pred_fn(gpdrhs) assert (got.values_host == expected.values).all() + + # The test is complete, the rest is just logging. try: + # The test passed, store the results. predicate_passes[predicate] = ( 1 if predicate not in predicate_passes @@ -71,10 +108,10 @@ def test_simple_features( } ) passes_df.to_csv("feature_passes.csv", index=False) - print(passes_df) except Exception as e: raise ValueError(e) except Exception as e: + # The test failed, store the results. out_file.write( f"""{predicate}, ------------ @@ -105,4 +142,4 @@ def test_simple_features( } ) feature_fails_df.to_csv("feature_fails.csv", index=False) - raise e # TODO: Remove when all tests are passing. + raise e diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index ac4fd111a..b204efde2 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -319,23 +319,27 @@ def naturalearth_lowres(): @pytest.fixture(scope="session") def predicate_passes(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def predicate_fails(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def feature_passes(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data @pytest.fixture(scope="session") def feature_fails(): + """Used by test_binpred_test_dispatch.py to store test results.""" data = {} return data From dfcd58339858971af206efa20ca05f4bd75ba1c8 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 19:01:49 +0000 Subject: [PATCH 049/126] Fix bug in test list. --- .../cuspatial/tests/binpreds/binpred_test_dispatch.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 40c77b2a7..ed8ca4236 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -491,12 +491,9 @@ def object_dispatch(name_list): type_dispatch = { - """A dictionary of test cases for each geometry type combination. - Still needs MultiPoint."""( - Point, Point - ): object_dispatch( - point_point_dispatch_list - ), + # A dictionary of test cases for each geometry type combination. + # Still needs MultiPoint. + (Point, Point): object_dispatch(point_point_dispatch_list), (Point, LineString): object_dispatch(point_linestring_dispatch_list), (Point, Polygon): object_dispatch(point_polygon_dispatch_list), (LineString, LineString): object_dispatch( From 1766437774767cab604e2b15dec55671d138f2d1 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 19:08:52 +0000 Subject: [PATCH 050/126] Basic binpred first fork of big PR. --- .../core/binpreds/binpred_interface.py | 36 +- .../binpreds/complex_geometry_predicate.py | 204 +++++++++ .../core/binpreds/feature_contains.py | 387 +++--------------- .../binpreds/feature_contains_properly.py | 268 ++++++++++++ .../cuspatial/tests/binpreds/test_contains.py | 91 ++++ .../tests/binpreds/test_contains_properly.py | 55 +-- .../tests/binpreds/test_pip_only_binpreds.py | 201 ++++----- .../cuspatial/utils/binpred_utils.py | 155 +++++++ 8 files changed, 875 insertions(+), 522 deletions(-) create mode 100644 python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py create mode 100644 python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_contains.py diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py index f380c425a..4729b56e1 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py @@ -26,9 +26,10 @@ class BinPredConfig: def __init__(self, **kwargs): self.align = kwargs.get("align", True) + self.kwargs = kwargs def __repr__(self): - return f"BinpredConfig(align={self.align}, allpairs={self.allpairs})" + return f"BinPredConfig(align={self.align}, kwargs={self.kwargs})" def __str__(self): return self.__repr__() @@ -45,7 +46,7 @@ class PreprocessorResult: The left-hand GeoSeries. rhs : GeoSeries The right-hand GeoSeries. - final_rhs : GeoSeries + points : GeoSeries The rhs GeoSeries, if modified by the preprocessor. For example the contains preprocessor converts any complex feature type into a collection of points. @@ -63,12 +64,12 @@ def __init__( ): self.lhs = lhs self.rhs = rhs - self.final_rhs = final_rhs + self.points = final_rhs self.point_indices = point_indices def __repr__(self): return f"PreprocessorResult(lhs={self.lhs}, rhs={self.rhs}, \ - points={self.points}, point_indices={self.point_indices})" + final_rhs={self.points}, point_indices={self.point_indices})" def __str__(self): return self.__repr__() @@ -85,31 +86,30 @@ class ContainsOpResult(OpResult): Parameters ---------- - result : cudf.DataFrame + pip_result : cudf.DataFrame A cudf.DataFrame containing two columns: "polygon_index" and Point_index". The "polygon_index" column contains the index of the polygon that contains each point. The "point_index" column contains the index of each point that is contained by a polygon. - points : GeoSeries - A GeoSeries of points. - point_indices : cudf.Series - A cudf.Series of indices that map each point in `points` to its - corresponding feature in the right-hand GeoSeries. + intersection_result: Tuple + A tuple containing the result of the intersection operation + between the left-hand GeoSeries and the right-hand GeoSeries. """ def __init__( self, - result: Series, - points: "GeoSeries" = None, - point_indices: Series = None, + pip_result: Series, + preprocessor_result: PreprocessorResult, + intersection_result: Tuple = None, ): - self.result = result - self.points = points - self.point_indices = point_indices + self.pip_result = pip_result + self.preprocessor_result = preprocessor_result + self.intersection_result = intersection_result def __repr__(self): - return f"OpResult(result={self.result}, points={self.points}, \ - point_indices={self.point_indices})" + return f"OpResult(pip_result={self.pip_result},\n \ + preprocessor_result={self.preprocessor_result},\n \ + intersection_result={self.intersection_result})\n" def __str__(self): return self.__repr__() diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py new file mode 100644 index 000000000..e4f80a376 --- /dev/null +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -0,0 +1,204 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from typing import Union + +import cupy as cp + +import cudf +from cudf.core.dataframe import DataFrame +from cudf.core.series import Series + +from cuspatial.core._column.geocolumn import GeoColumn +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + PreprocessorResult, +) +from cuspatial.utils.binpred_utils import ( + _count_results_in_multipoint_geometries, + _false_series, +) +from cuspatial.utils.column_utils import ( + contains_only_linestrings, + contains_only_multipoints, + contains_only_polygons, +) + + +class ComplexGeometryPredicate(BinPred): + def _preprocess_multi(self, lhs, rhs): + # Breaks down complex geometries into their constituent parts. + # Passes a tuple o the preprocessed geometries and a tuple of + # the indices of the points in the original geometry. + # This is used by the postprocessor to reconstruct the original + # geometry. + # Child classes should not implement this method. + """Flatten any rhs into only its points xy array. This is necessary + because the basic predicate for contains, point-in-polygon, + only accepts points. + + Parameters + ---------- + lhs : GeoSeries + The left-hand GeoSeries. + rhs : GeoSeries + The right-hand GeoSeries. + + Returns + ------- + result : GeoSeries + A GeoSeries of boolean values indicating whether each feature in + the right-hand GeoSeries satisfies the requirements of the point- + in-polygon basic predicate with its corresponding feature in the + left-hand GeoSeries. + """ + # RHS conditioning: + point_indices = None + # point in polygon + if contains_only_linestrings(rhs): + # condition for linestrings + geom = rhs.lines + elif contains_only_polygons(rhs) is True: + # polygon in polygon + geom = rhs.polygons + elif contains_only_multipoints(rhs) is True: + # mpoint in polygon + geom = rhs.multipoints + else: + # no conditioning is required + geom = rhs.points + xy_points = geom.xy + + # Arrange into shape for calling point-in-polygon, intersection, or + # equals + point_indices = geom.point_indices() + from cuspatial.core.geoseries import GeoSeries + + final_rhs = GeoSeries(GeoColumn._from_points_xy(xy_points._column)) + preprocess_result = PreprocessorResult( + lhs, rhs, final_rhs, point_indices + ) + return preprocess_result + + def _convert_quadtree_result_from_part_to_polygon_indices( + self, lhs, point_result + ): + """Convert the result of a quadtree contains_properly call from + part indices to polygon indices. + + Parameters + ---------- + point_result : cudf.Series + The result of a quadtree contains_properly call. This result + contains the `part_index` of the polygon that contains the + point, not the polygon index. + + Returns + ------- + cudf.Series + The result of a quadtree contains_properly call. This result + contains the `polygon_index` of the polygon that contains the + point, not the part index. + """ + # Get the length of each part, map it to indices, and store + # the result in a dataframe. + rings_to_parts = cp.array(lhs.polygons.part_offset) + part_sizes = rings_to_parts[1:] - rings_to_parts[:-1] + parts_map = cudf.Series( + cp.arange(len(part_sizes)), name="part_index" + ).repeat(part_sizes) + parts_index_mapping_df = parts_map.reset_index(drop=True).reset_index() + # Map the length of each polygon in a similar fashion, then + # join them below. + parts_to_geoms = cp.array(lhs.polygons.geometry_offset) + geometry_sizes = parts_to_geoms[1:] - parts_to_geoms[:-1] + geometry_map = cudf.Series( + cp.arange(len(geometry_sizes)), name="polygon_index" + ).repeat(geometry_sizes) + geom_index_mapping_df = geometry_map.reset_index(drop=True) + geom_index_mapping_df.index.name = "part_index" + geom_index_mapping_df = geom_index_mapping_df.reset_index() + # Replace the part index with the polygon index by join + part_result = parts_index_mapping_df.merge( + point_result, on="part_index" + ) + # Replace the polygon index with the row index by join + return geom_index_mapping_df.merge(part_result, on="part_index")[ + ["polygon_index", "point_index"] + ] + + def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: + """Prepare the allpairs result of a contains_properly call as + the first step of postprocessing. + + Parameters + ---------- + lhs : GeoSeries + The left-hand side of the binary predicate. + op_result : ContainsProperlyOpResult + The result of the contains_properly call. + + Returns + ------- + cudf.DataFrame + + """ + # Convert the quadtree part indices df into a polygon indices df + polygon_indices = ( + self._convert_quadtree_result_from_part_to_polygon_indices( + lhs, op_result.pip_result + ) + ) + # Because the quadtree contains_properly call returns a list of + # points that are contained in each part, parts can be duplicated + # once their index is converted to a polygon index. + allpairs_result = polygon_indices.drop_duplicates() + + # Replace the polygon index with the original index + allpairs_result["polygon_index"] = allpairs_result[ + "polygon_index" + ].replace(Series(lhs.index, index=cp.arange(len(lhs.index)))) + + return allpairs_result + + def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): + # Doesn't use op_result, but uses preprocessor_result to + # reconstruct the original geometry. + # Child classes should call this method to reconstruct the + # original geometry. + + # Complex geometry postprocessor + point_indices = preprocessor_result.point_indices + allpairs_result = self._reindex_allpairs(lhs, op_result) + if isinstance(allpairs_result, Series): + return allpairs_result + (hits, expected_count,) = _count_results_in_multipoint_geometries( + point_indices, allpairs_result + ) + result_df = hits.reset_index().merge( + expected_count.reset_index(), on="rhs_index" + ) + result_df["feature_in_polygon"] = ( + result_df["point_index_x"] >= result_df["point_index_y"] + ) + final_result = _false_series(len(rhs)) + final_result.loc[ + result_df["rhs_index"][result_df["feature_in_polygon"]] + ] = True + return final_result + + def _postprocess_simple(self, lhs, rhs, preprocessor_result, op_result): + allpairs_result = self._reindex_allpairs(lhs, op_result) + final_result = _false_series(len(rhs)) + if len(lhs) == len(rhs): + matches = ( + allpairs_result["polygon_index"] + == allpairs_result["point_index"] + ) + polygon_indexes = allpairs_result["polygon_index"][matches] + final_result.loc[ + preprocessor_result.point_indices[polygon_indexes] + ] = True + return final_result + else: + final_result.loc[allpairs_result["polygon_index"]] = True + return final_result diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index c5852698b..3a222319b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -1,50 +1,28 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from typing import Generic, TypeVar, Union +from typing import TypeVar -import cupy as cp - -import cudf -from cudf.core.dataframe import DataFrame -from cudf.core.series import Series - -from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core.binpreds.binpred_interface import ( - BinPred, - ContainsOpResult, + ImpossiblePredicate, NotImplementedPredicate, - PreprocessorResult, ) -from cuspatial.core.binpreds.contains import contains_properly +from cuspatial.core.binpreds.complex_geometry_predicate import ( + ComplexGeometryPredicate, +) from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, Point, Polygon, - _count_results_in_multipoint_geometries, - _false_series, -) -from cuspatial.utils.column_utils import ( - contains_only_linestrings, - contains_only_multipoints, - contains_only_polygons, - has_multipolygons, + _multipoints_from_geometry, ) GeoSeries = TypeVar("GeoSeries") -class ContainsPredicateBase(BinPred, Generic[GeoSeries]): - """Base class for binary predicates that are defined in terms of a - `contains` basic predicate. This class implements the logic that underlies - `polygon.contains` primarily, and is implemented for many cases. - - Subclasses are selected using the `DispatchDict` located at the end - of this file. - """ - +class ContainsPredicateBase(ComplexGeometryPredicate): def __init__(self, **kwargs): - """`ContainsPredicateBase` constructor. + """`ContainsProperlyPredicateBase` constructor. Parameters ---------- @@ -55,322 +33,79 @@ def __init__(self, **kwargs): """ super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) + self.config.mode = kwargs.get("mode", "full") def _preprocess(self, lhs, rhs): - """Flatten any rhs into only its points xy array. This is necessary - because the basic predicate for contains, point-in-polygon, - only accepts points. - - Parameters - ---------- - lhs : GeoSeries - The left-hand GeoSeries. - rhs : GeoSeries - The right-hand GeoSeries. - - Returns - ------- - result : GeoSeries - A GeoSeries of boolean values indicating whether each feature in - the right-hand GeoSeries satisfies the requirements of the point- - in-polygon basic predicate with its corresponding feature in the - left-hand GeoSeries. - """ - # RHS conditioning: - point_indices = None - # point in polygon - if contains_only_linestrings(rhs): - # condition for linestrings - geom = rhs.lines - elif contains_only_polygons(rhs) is True: - # polygon in polygon - geom = rhs.polygons - elif contains_only_multipoints(rhs) is True: - # mpoint in polygon - geom = rhs.multipoints - else: - # no conditioning is required - geom = rhs.points - xy_points = geom.xy - - # Arrange into shape for calling point-in-polygon, intersection, or - # equals - point_indices = geom.point_indices() - from cuspatial.core.geoseries import GeoSeries - - final_rhs = GeoSeries(GeoColumn._from_points_xy(xy_points._column)) - preprocess_result = PreprocessorResult( - lhs, rhs, final_rhs, point_indices - ) - return self._compute_predicate(lhs, rhs, preprocess_result) - - def _should_use_quadtree(self, lhs): - """Determine if the quadtree should be used for the binary predicate. - - Returns - ------- - bool - True if the quadtree should be used, False otherwise. - - Notes - ----- - 1. Quadtree is always used if user requests `allpairs=True`. - 2. If the number of polygons in the lhs is less than 32, we use the - byte-limited algorithm because it is faster and has less memory - overhead. - 3. If the lhs contains more than 32 polygons, we use the quadtree - because it does not have a polygon-count limit. - 4. If the lhs contains multipolygons, we use quadtree because the - performance between quadtree and byte-limited is similar, but - code complexity would be higher if we did multipolygon - reconstruction on both code paths. - """ - return len(lhs) >= 32 or has_multipolygons(lhs) or self.config.allpairs - - def _compute_predicate( - self, - lhs: "GeoSeries", - rhs: "GeoSeries", - preprocessor_result: PreprocessorResult, - ): - """Compute the contains_properly relationship between two GeoSeries. - A feature A contains another feature B if no points of B lie in the - exterior of A, and at least one point of the interior of B lies in the - interior of A. This is the inverse of `within`.""" - if not contains_only_polygons(lhs): - raise TypeError( - "`.contains` can only be called with polygon series." - ) - points = preprocessor_result.final_rhs - point_indices = preprocessor_result.point_indices - if self._should_use_quadtree(lhs): - result = contains_properly(lhs, points, how="quadtree") - else: - result = contains_properly(lhs, points, how="byte-limited") - op_result = ContainsOpResult(result, points, point_indices) - return self._postprocess(lhs, rhs, op_result) - - def _convert_quadtree_result_from_part_to_polygon_indices( - self, lhs, point_result - ): - """Convert the result of a quadtree contains_properly call from - part indices to polygon indices. - - Parameters - ---------- - point_result : cudf.Series - The result of a quadtree contains_properly call. This result - contains the `part_index` of the polygon that contains the - point, not the polygon index. - - Returns - ------- - cudf.Series - The result of a quadtree contains_properly call. This result - contains the `polygon_index` of the polygon that contains the - point, not the part index. - """ - # Get the length of each part, map it to indices, and store - # the result in a dataframe. - rings_to_parts = cp.array(lhs.polygons.part_offset) - part_sizes = rings_to_parts[1:] - rings_to_parts[:-1] - parts_map = cudf.Series( - cp.arange(len(part_sizes)), name="part_index" - ).repeat(part_sizes) - parts_index_mapping_df = parts_map.reset_index(drop=True).reset_index() - # Map the length of each polygon in a similar fashion, then - # join them below. - parts_to_geoms = cp.array(lhs.polygons.geometry_offset) - geometry_sizes = parts_to_geoms[1:] - parts_to_geoms[:-1] - geometry_map = cudf.Series( - cp.arange(len(geometry_sizes)), name="polygon_index" - ).repeat(geometry_sizes) - geom_index_mapping_df = geometry_map.reset_index(drop=True) - geom_index_mapping_df.index.name = "part_index" - geom_index_mapping_df = geom_index_mapping_df.reset_index() - # Replace the part index with the polygon index by join - part_result = parts_index_mapping_df.merge( - point_result, on="part_index" + # Preprocess multi-geometries and complex geometries into + # the correct input type for the contains predicate. + # This is done by saving the shapes of multi-geometries, + # then converting them all to single geometries. + # Single geometries are converted from their original + # lhs and rhs types to the types needed for the contains predicate. + + # point_indices: the indices of the points in the original + # geometry. + # geometry_offsets: the offsets of the multi-geometries in + # the original geometry. + preprocessor_result = super()._preprocess_multi(lhs, rhs) + return self._compute_predicate(lhs, rhs, preprocessor_result) + + def _compute_predicate(self, lhs, rhs, preprocessor_result): + contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + rhs_points = _multipoints_from_geometry(rhs) + intersects = lhs._basic_intersects_count(rhs_points).reset_index( + drop=True ) - # Replace the polygon index with the row index by join - return geom_index_mapping_df.merge(part_result, on="part_index")[ - ["polygon_index", "point_index"] - ] + # TODO: Need to handle multipolygon case. The [0] below ignores all + # but the first polygon in a multipolygon. + # TODO: Need better point counting in intersection. + return contains + intersects >= rhs.sizes - def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: - """Prepare the allpairs result of a contains_properly call as - the first step of postprocessing. - Parameters - ---------- - lhs : GeoSeries - The left-hand side of the binary predicate. - op_result : ContainsOpResult - The result of the contains_properly call. +class ContainsPredicate(ContainsPredicateBase): + def _compute_results(self, lhs, rhs, preprocessor_result): + # Compute the contains predicate for the given lhs and rhs. + # lhs and rhs are both cudf.Series of shapely geometries. + # Returns a ContainsOpResult object. + return lhs._contains(rhs) - Returns - ------- - cudf.DataFrame - """ - # Convert the quadtree part indices df into a polygon indices df - polygon_indices = ( - self._convert_quadtree_result_from_part_to_polygon_indices( - lhs, op_result.result - ) - ) - # Because the quadtree contains_properly call returns a list of - # points that are contained in each part, parts can be duplicated - # once their index is converted to a polygon index. - allpairs_result = polygon_indices.drop_duplicates() - - # Replace the polygon index with the original index - allpairs_result["polygon_index"] = allpairs_result[ - "polygon_index" - ].replace(Series(lhs.index, index=cp.arange(len(lhs.index)))) - - return allpairs_result - - def _postprocess(self, lhs, rhs, op_result): - """Postprocess the output GeoSeries to ensure that they are of the - correct type for the predicate. - - Postprocess for contains_properly has to handle multiple input and - output configurations. - - The input can be a single polygon, a single multipolygon, or a - GeoSeries containing a mix of polygons and multipolygons. - - The input to postprocess is `point_indices`, which can be either a - cudf.DataFrame with one row per point and one column per polygon or - a cudf.DataFrame containing the point index and the part index for - each point in the polygon. - - Parameters - ---------- - lhs : GeoSeries - The left-hand side of the binary predicate. - rhs : GeoSeries - The right-hand side of the binary predicate. - preprocessor_output : ContainsOpResult - The result of the contains_properly call. - - Returns - ------- - cudf.Series or cudf.DataFrame - A Series of boolean values indicating whether each feature in - the rhs GeoSeries is contained in the lhs GeoSeries in the - case of allpairs=False. Otherwise, a DataFrame containing the - point index and the polygon index for each point in the - polygon. - """ - if len(op_result.result) == 0: - return _false_series(len(lhs)) - - # Convert the quadtree part indices df into a polygon indices df. - # Helps with handling multipolygons. - allpairs_result = self._reindex_allpairs(lhs, op_result) - - # If the user wants all pairs, return the result. Otherwise, - # return a boolean series indicating whether each point is - # contained in the corresponding polygon. - if self.config.allpairs: - return allpairs_result - else: - # for each input pair i: result[i] =  true iff point[i] is - # contained in at least one polygon of multipolygon[i]. - # pairwise - final_result = _false_series(len(rhs)) - if len(lhs) == len(rhs): - matches = ( - allpairs_result["polygon_index"] - == allpairs_result["point_index"] - ) - polygon_indexes = allpairs_result["polygon_index"][matches] - final_result.loc[ - op_result.point_indices[polygon_indexes] - ] = True - return final_result - else: - final_result.loc[allpairs_result["polygon_index"]] = True - return final_result - - -class PolygonComplexContains(ContainsPredicateBase): - """Base class for contains operations that use a complex object on - the right hand side. - - This class is shared by the Polygon*Contains classes that use - a non-points object on the right hand side: MultiPoint, LineString, - MultiLineString, Polygon, and MultiPolygon. - - Used by: - (Polygon, MultiPoint) - (Polygon, LineString) - (Polygon, Polygon) - """ - - def _postprocess(self, lhs, rhs, preprocessor_output): - # for each input pair i: result[i] =  true iff point[i] is - # contained in at least one polygon of multipolygon[i]. - # pairwise - point_indices = preprocessor_output.point_indices - allpairs_result = self._reindex_allpairs(lhs, preprocessor_output) - if isinstance(allpairs_result, Series): - return allpairs_result - - (hits, expected_count,) = _count_results_in_multipoint_geometries( - point_indices, allpairs_result - ) - result_df = hits.reset_index().merge( - expected_count.reset_index(), on="rhs_index" - ) - result_df["feature_in_polygon"] = ( - result_df["point_index_x"] >= result_df["point_index_y"] - ) - final_result = _false_series(len(rhs)) - final_result.loc[ - result_df["rhs_index"][result_df["feature_in_polygon"]] - ] = True - return final_result +class PointPointContains(ContainsPredicateBase): + def _preprocess(self, lhs, rhs): + return lhs._basic_equals(rhs) -class ContainsByIntersection(BinPred): - """Point types are contained only by an intersection test. +class LineStringMultiPointContainsPredicate(ContainsPredicateBase): + def _compute_results(self, lhs, rhs, preprocessor_result): + # Compute the contains predicate for the given lhs and rhs. + # lhs and rhs are both cudf.Series of shapely geometries. + # Returns a ContainsOpResult object. + return lhs._linestring_multipoint_contains(rhs) - Used by: - (Point, Point) - (LineString, Point) - """ +class LineStringLineStringContainsPredicate(ContainsPredicateBase): def _preprocess(self, lhs, rhs): - from cuspatial.core.binpreds.binpred_dispatch import ( - INTERSECTS_DISPATCH, - ) - - predicate = INTERSECTS_DISPATCH[(lhs.column_type, rhs.column_type)]( - align=self.config.align - ) - return predicate(lhs, rhs) + count = lhs._basic_equals_count(rhs) + return count == rhs.sizes """DispatchDict listing the classes to use for each combination of left and right hand side types. """ DispatchDict = { - (Point, Point): ContainsByIntersection, - (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, - (Point, Polygon): NotImplementedPredicate, + (Point, Point): PointPointContains, + (Point, MultiPoint): ImpossiblePredicate, + (Point, LineString): ImpossiblePredicate, + (Point, Polygon): ImpossiblePredicate, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): ContainsByIntersection, - (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, - (LineString, Polygon): NotImplementedPredicate, + (LineString, Point): ContainsPredicateBase, + (LineString, MultiPoint): LineStringMultiPointContainsPredicate, + (LineString, LineString): LineStringLineStringContainsPredicate, + (LineString, Polygon): ImpossiblePredicate, (Polygon, Point): ContainsPredicateBase, - (Polygon, MultiPoint): PolygonComplexContains, - (Polygon, LineString): PolygonComplexContains, - (Polygon, Polygon): PolygonComplexContains, + (Polygon, MultiPoint): ContainsPredicateBase, + (Polygon, LineString): ContainsPredicateBase, + (Polygon, Polygon): ContainsPredicateBase, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py new file mode 100644 index 000000000..3d35fc814 --- /dev/null +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -0,0 +1,268 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from typing import TypeVar + +import cupy as cp + +import cudf + +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + ContainsOpResult, + ImpossiblePredicate, + NotImplementedPredicate, + PreprocessorResult, +) +from cuspatial.core.binpreds.contains import contains_properly +from cuspatial.core.binpreds.feature_contains import ( + ComplexGeometryPredicate, + ContainsPredicateBase, +) +from cuspatial.utils.binpred_utils import ( + LineString, + MultiPoint, + Point, + Polygon, + _false_series, + _is_complex, +) +from cuspatial.utils.column_utils import ( + contains_only_polygons, + has_multipolygons, +) + +GeoSeries = TypeVar("GeoSeries") + + +class ContainsProperlyPredicate( + ContainsPredicateBase, ComplexGeometryPredicate +): + def __init__(self, **kwargs): + """Base class for binary predicates that are defined in terms of a + `contains` basic predicate. This class implements the logic that + underlies `polygon.contains` primarily, and is implemented for many + cases. + + Subclasses are selected using the `DispatchDict` located at the end + of this file. + + Parameters + ---------- + allpairs: bool + Whether to compute all pairs of features in the left-hand and + right-hand GeoSeries. If False, the feature will be compared in a + 1:1 fashion with the corresponding feature in the other GeoSeries. + """ + super().__init__(**kwargs) + self.config.allpairs = kwargs.get("allpairs", False) + self.config.mode = kwargs.get("mode", "full") + + def _preprocess(self, lhs, rhs): + # Preprocess multi-geometries and complex geometries into + # the correct input type for the contains predicate. + # This is done by saving the shapes of multi-geometries, + # then converting them all to single geometries. + # Single geometries are converted from their original + # lhs and rhs types to the types needed for the contains predicate. + + # point_indices: the indices of the points in the original + # geometry. + # geometry_offsets: the offsets of the multi-geometries in + # the original geometry. + preprocessor_result = super()._preprocess_multi(lhs, rhs) + return self._compute_predicate(lhs, rhs, preprocessor_result) + + def _should_use_quadtree(self, lhs): + """Determine if the quadtree should be used for the binary predicate. + + Returns + ------- + bool + True if the quadtree should be used, False otherwise. + + Notes + ----- + 1. Quadtree is always used if user requests `allpairs=True`. + 2. If the number of polygons in the lhs is less than 32, we use the + byte-limited algorithm because it is faster and has less memory + overhead. + 3. If the lhs contains more than 32 polygons, we use the quadtree + because it does not have a polygon-count limit. + 4. If the lhs contains multipolygons, we use quadtree because the + performance between quadtree and byte-limited is similar, but + code complexity would be higher if we did multipolygon + reconstruction on both code paths. + """ + return len(lhs) >= 32 or has_multipolygons(lhs) or self.config.allpairs + + def _compute_predicate( + self, + lhs: "GeoSeries", + rhs: "GeoSeries", + preprocessor_result: PreprocessorResult, + ): + # _compute predicate no longer cares about preprocessor result + # because information is passed directly to the postprocessor. + # Creates an op_result and passes it and the preprocessor result + # to the postprocessor. + + # Calls various _basic_predicate methods to compute the + # predicate. + # .contains calls .basic_contains_properly and also .basic_intersects + # in order to assemble boundary-exclusive contains with intersection + # results. + """Compute the contains_properly relationship between two GeoSeries. + A feature A contains another feature B if no points of B lie in the + exterior of A, and at least one point of the interior of B lies in the + interior of A. This is the inverse of `within`.""" + if not contains_only_polygons(lhs): + raise TypeError( + "`.contains` can only be called with polygon series." + ) + if self._should_use_quadtree(lhs): + pip_result = contains_properly( + lhs, preprocessor_result.points, how="quadtree" + ) + else: + pip_result = contains_properly( + lhs, preprocessor_result.points, how="byte-limited" + ) + op_result = ContainsOpResult(pip_result, preprocessor_result) + return self._postprocess(lhs, rhs, preprocessor_result, op_result) + + def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): + """Return the result of the basic predicate without any + postprocessing. + """ + reindex_pip_result = self._reindex_allpairs(lhs, op_result) + if len(reindex_pip_result) == 0: + if self.config.mode == "basic_count": + return cudf.Series(cp.zeros(len(lhs), dtype="int32")) + else: + return _false_series(len(lhs)) + # Postprocessing early termination. Basic requests, or allpairs + # requests do not do object reconstruction. + if self.config.allpairs: + return reindex_pip_result + elif self.config.mode == "basic_none": + final_result = cudf.Series(cp.repeat([True], len(lhs))) + final_result.loc[reindex_pip_result["polygon_index"]] = False + return final_result + elif self.config.mode == "basic_any": + final_result = _false_series(len(lhs)) + final_result.loc[reindex_pip_result["polygon_index"]] = True + return final_result + elif self.config.mode == "basic_all": + sizes = ( + preprocessor_result.point_indices[1:] + - preprocessor_result.point_indices[:-1] + ) + result_sizes = reindex_pip_result["polygon_index"].value_counts() + final_result = _false_series( + len(preprocessor_result.point_indices) + ) + final_result.loc[sizes == result_sizes] = True + return final_result + elif self.config.mode == "basic_count": + return reindex_pip_result["polygon_index"].value_counts() + + def _postprocess(self, lhs, rhs, preprocessor_result, op_result): + # Downstream predicates inherit from ComplexGeometryPredicate + # that implements + # point reconstruction for complex types separately. + # Early return if individual points are required for downstream + # predicates. Handle `any`, `all`, `none` modes. + """Postprocess the output GeoSeries to ensure that they are of the + correct type for the predicate. + + Postprocess for contains_properly has to handle multiple input and + output configurations. + + The input can be a single polygon, a single multipolygon, or a + GeoSeries containing a mix of polygons and multipolygons. + + The input to postprocess is `point_indices`, which can be either a + cudf.DataFrame with one row per point and one column per polygon or + a cudf.DataFrame containing the point index and the part index for + each point in the polygon. + + Parameters + ---------- + lhs : GeoSeries + The left-hand side of the binary predicate. + rhs : GeoSeries + The right-hand side of the binary predicate. + preprocessor_output : ContainsOpResult + The result of the contains_properly call. + + Returns + ------- + cudf.Series or cudf.DataFrame + A Series of boolean values indicating whether each feature in + the rhs GeoSeries is contained in the lhs GeoSeries in the + case of allpairs=False. Otherwise, a DataFrame containing the + point index and the polygon index for each point in the + polygon. + """ + if self.config.mode != "full" or self.config.allpairs: + return self._return_unprocessed_result( + lhs, op_result, preprocessor_result + ) + + # for each input pair i: result[i] =  true iff point[i] is + # contained in at least one polygon of multipolygon[i]. + if _is_complex(rhs): + return super()._postprocess_multi( + lhs, rhs, preprocessor_result, op_result + ) + else: + return super()._postprocess_simple( + lhs, rhs, preprocessor_result, op_result + ) + + +class ContainsProperlyByIntersection(BinPred): + """Point types are contained only by an intersection test. + + Used by: + (Point, Point) + (LineString, Point) + """ + + def _preprocess(self, lhs, rhs): + from cuspatial.core.binpreds.binpred_dispatch import ( + INTERSECTS_DISPATCH, + ) + + predicate = INTERSECTS_DISPATCH[(lhs.column_type, rhs.column_type)]( + align=self.config.align + ) + return predicate(lhs, rhs) + + +class LineStringLineStringContainsProperly(BinPred): + def _preprocess(self, lhs, rhs): + count = lhs._basic_equals_all(rhs) + return count + + +"""DispatchDict listing the classes to use for each combination of + left and right hand side types. """ +DispatchDict = { + (Point, Point): ContainsProperlyByIntersection, + (Point, MultiPoint): ImpossiblePredicate, + (Point, LineString): ImpossiblePredicate, + (Point, Polygon): ImpossiblePredicate, + (MultiPoint, Point): NotImplementedPredicate, + (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, LineString): NotImplementedPredicate, + (MultiPoint, Polygon): NotImplementedPredicate, + (LineString, Point): ContainsProperlyByIntersection, + (LineString, MultiPoint): ContainsProperlyPredicate, + (LineString, LineString): LineStringLineStringContainsProperly, + (LineString, Polygon): ImpossiblePredicate, + (Polygon, Point): ContainsProperlyPredicate, + (Polygon, MultiPoint): ContainsProperlyPredicate, + (Polygon, LineString): ContainsProperlyPredicate, + (Polygon, Polygon): ContainsProperlyPredicate, +} diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py new file mode 100644 index 000000000..a643b71f0 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py @@ -0,0 +1,91 @@ +# Copyright (c) 2023, NVIDIA CORPORATION + +import geopandas as gpd +import pandas as pd +import pytest +from shapely.geometry import MultiPolygon, Polygon + +import cuspatial + + +def test_same(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + got = lhs.contains(rhs) + expected = gpdlhs.contains(gpdrhs) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_adjacent(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Polygon([(1, 0), (1, 1), (2, 1), (2, 0)])]) + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + got = lhs.contains(rhs) + expected = gpdlhs.contains(gpdrhs) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_interior(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries( + [Polygon([(0, 0), (0, 0.5), (0.5, 0.5), (0.5, 0)])] + ) + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + got = lhs.contains(rhs) + expected = gpdlhs.contains(gpdrhs) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.parametrize( + "object", + [ + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + MultiPolygon( + [ + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + ] + ), + ], +) +def test_self_contains(object): + gpdobject = gpd.GeoSeries(object) + object = cuspatial.from_geopandas(gpdobject) + got = object.contains(object).values_host + expected = gpdobject.contains(gpdobject).values + assert (got == expected).all() + + +def test_complex_input(): + gpdobject = gpd.GeoSeries( + [ + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + Polygon( + ([0, 0], [1, 1], [1, 0], [0, 0]), + [([0, 0], [1, 1], [1, 0], [0, 0])], + ), + MultiPolygon( + [ + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + ] + ), + MultiPolygon( + [ + Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), + Polygon( + ([0, 0], [1, 1], [1, 0], [0, 0]), + [([0, 0], [1, 1], [1, 0], [0, 0])], + ), + ] + ), + ] + ) + object = cuspatial.from_geopandas(gpdobject) + got = object.contains(object).values_host + expected = gpdobject.contains(gpdobject).values + assert (got == expected).all() diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_contains_properly.py b/python/cuspatial/cuspatial/tests/binpreds/test_contains_properly.py index a93b429c9..e3a67df6c 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_contains_properly.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_contains_properly.py @@ -1,3 +1,5 @@ +# Copyright (c) 2023, NVIDIA CORPORATION + import cupy as cp import geopandas as gpd import numpy as np @@ -387,59 +389,6 @@ def test_max_polygons_max_multipoints(multipoint_generator, polygon_generator): assert (got == expected).all() -@pytest.mark.parametrize( - "object", - [ - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - MultiPolygon( - [ - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - ] - ), - ], -) -def test_self_contains(object): - gpdobject = gpd.GeoSeries(object) - object = cuspatial.from_geopandas(gpdobject) - got = object.contains_properly(object).values_host - expected = gpdobject.contains(gpdobject).values - np.testing.assert_array_equal(got, np.array([False])) - np.testing.assert_array_equal(expected, np.array([True])) - - -def test_complex_input(): - gpdobject = gpd.GeoSeries( - [ - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - Polygon( - ([0, 0], [1, 1], [1, 0], [0, 0]), - [([0, 0], [1, 1], [1, 0], [0, 0])], - ), - MultiPolygon( - [ - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - ] - ), - MultiPolygon( - [ - Polygon([[0, 0], [1, 1], [1, 0], [0, 0]]), - Polygon( - ([0, 0], [1, 1], [1, 0], [0, 0]), - [([0, 0], [1, 1], [1, 0], [0, 0])], - ), - ] - ), - ] - ) - object = cuspatial.from_geopandas(gpdobject) - got = object.contains_properly(object).values_host - expected = gpdobject.contains(gpdobject).values - assert (got == [False, False, False, False]).all() - assert (expected == [True, True, True, True]).all() - - def test_multi_contains(): lhs = cuspatial.GeoSeries( [ diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py b/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py index 545629402..eae2d7e1d 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py @@ -1,4 +1,3 @@ -import geopandas as gpd from shapely.geometry import LineString, Point, Polygon import cuspatial @@ -6,189 +5,141 @@ """Overlaps, Within, and Intersects""" +def _test(lhs, rhs, predicate): + cuspatiallhs = lhs.to_geopandas() + cuspatialrhs = rhs.to_geopandas() + got = getattr(lhs, predicate)(rhs).values_host + expected = getattr(cuspatiallhs, predicate)(cuspatialrhs).values + assert (got == expected).all() + + def test_polygon_overlaps_point(): - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - gpdpoint = gpd.GeoSeries([Point(0.5, 0.5)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - point = cuspatial.from_geopandas(gpdpoint) - got = polygon.overlaps(point).values_host - expected = gpdpolygon.overlaps(gpdpoint).values - assert (got == expected).all() + rhs = cuspatial.GeoSeries([Point(0.5, 0.5)]) + _test(lhs, rhs, "overlaps") def test_max_polygons_overlaps_max_points(polygon_generator, point_generator): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdpoint = gpd.GeoSeries([*point_generator(31)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - point = cuspatial.from_geopandas(gpdpoint) - got = polygon.overlaps(point).values_host - expected = gpdpolygon.overlaps(gpdpoint).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*point_generator(31)]) + _test(lhs, rhs, "overlaps") def test_polygon_overlaps_polygon_partially(): - gpdpolygon1 = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - gpdpolygon2 = gpd.GeoSeries( - Polygon([[0.5, 0.5], [0.5, 1.5], [1.5, 1.5], [1.5, 0.5], [0.5, 0.5]]) + rhs = cuspatial.GeoSeries( + [Polygon([[0.5, 0.5], [0.5, 1.5], [1.5, 1.5], [1.5, 0.5], [0.5, 0.5]])] ) - polygon1 = cuspatial.from_geopandas(gpdpolygon1) - polygon2 = cuspatial.from_geopandas(gpdpolygon2) - got = polygon1.overlaps(polygon2).values_host - expected = gpdpolygon1.overlaps(gpdpolygon2).values - assert (got == expected).all() + _test(lhs, rhs, "overlaps") def test_polygon_overlaps_polygon_completely(): - gpdpolygon1 = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - gpdpolygon2 = gpd.GeoSeries( - Polygon( - [[0.25, 0.25], [0.25, 0.5], [0.5, 0.5], [0.5, 0.25], [0.25, 0.25]] - ) + rhs = cuspatial.GeoSeries( + [ + Polygon( + [ + [0.25, 0.25], + [0.25, 0.5], + [0.5, 0.5], + [0.5, 0.25], + [0.25, 0.25], + ] + ) + ] ) - polygon1 = cuspatial.from_geopandas(gpdpolygon1) - polygon2 = cuspatial.from_geopandas(gpdpolygon2) - got = polygon1.overlaps(polygon2).values_host - expected = gpdpolygon1.overlaps(gpdpolygon2).values - assert (got == expected).all() + _test(lhs, rhs, "overlaps") def test_polygon_overlaps_polygon_no_overlap(): - gpdpolygon1 = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - gpdpolygon2 = gpd.GeoSeries( - Polygon([[2, 2], [2, 3], [3, 3], [3, 2], [2, 2]]) + rhs = cuspatial.GeoSeries( + [Polygon([[2, 2], [2, 3], [3, 3], [3, 2], [2, 2]])] ) - polygon1 = cuspatial.from_geopandas(gpdpolygon1) - polygon2 = cuspatial.from_geopandas(gpdpolygon2) - got = polygon1.overlaps(polygon2).values_host - expected = gpdpolygon1.overlaps(gpdpolygon2).values - assert (got == expected).all() + _test(lhs, rhs, "overlaps") def test_max_polygon_overlaps_max_points(polygon_generator, point_generator): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdpoint = gpd.GeoSeries([*point_generator(31)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - point = cuspatial.from_geopandas(gpdpoint) - got = polygon.overlaps(point).values_host - expected = gpdpolygon.overlaps(gpdpoint).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*point_generator(31)]) + _test(lhs, rhs, "overlaps") def test_point_intersects_polygon_interior(): - gpdpoint = gpd.GeoSeries([Point(0.5, 0.5)]) - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries([Point(0.5, 0.5)]) + rhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - point = cuspatial.from_geopandas(gpdpoint) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = point.intersects(polygon).values_host - expected = gpdpoint.intersects(gpdpolygon).values - assert (got == expected).all() + _test(lhs, rhs, "intersects") def test_max_points_intersects_max_polygons_interior( polygon_generator, point_generator ): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdpoint = gpd.GeoSeries([*point_generator(31)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - point = cuspatial.from_geopandas(gpdpoint) - got = point.intersects(polygon).values_host - expected = gpdpoint.intersects(gpdpolygon).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*point_generator(31)]) + _test(lhs, rhs, "intersects") def test_point_within_polygon(): - gpdpoint = gpd.GeoSeries([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 = point.within(polygon).values_host - expected = gpdpoint.within(gpdpolygon).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([Point(0, 0)]) + rhs = cuspatial.GeoSeries([Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])]) + _test(lhs, rhs, "within") def test_max_points_within_max_polygons(polygon_generator, point_generator): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdpoint = gpd.GeoSeries([*point_generator(31)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - point = cuspatial.from_geopandas(gpdpoint) - got = point.within(polygon).values_host - expected = gpdpoint.within(gpdpolygon).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*point_generator(31)]) + _test(lhs, rhs, "within") def test_linestring_within_polygon(): - gpdline = gpd.GeoSeries([LineString([(0, 0), (1, 1)])]) - gpdpolygon = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) - line = cuspatial.from_geopandas(gpdline) - polygon = cuspatial.from_geopandas(gpdpolygon) - got = line.within(polygon).values_host - expected = gpdline.within(gpdpolygon).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([LineString([(0, 0), (1, 1)])]) + rhs = cuspatial.GeoSeries([Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])]) + _test(lhs, rhs, "within") def test_max_linestring_within_max_polygon( polygon_generator, linestring_generator ): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdline = gpd.GeoSeries([*linestring_generator(31, 5)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - line = cuspatial.from_geopandas(gpdline) - got = line.within(polygon).values_host - expected = gpdline.within(gpdpolygon).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*linestring_generator(31, 5)]) + _test(lhs, rhs, "within") def test_polygon_within_polygon(): - gpdpolygon1 = gpd.GeoSeries( - Polygon([[0, 0], [-1, 1], [1, 1], [1, -2], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [-1, 1], [1, 1], [1, -2], [0, 0]])] ) - gpdpolygon2 = gpd.GeoSeries(Polygon([[-1, -1], [-2, 2], [2, 2], [2, -2]])) - polygon1 = cuspatial.from_geopandas(gpdpolygon1) - polygon2 = cuspatial.from_geopandas(gpdpolygon2) - got = polygon1.within(polygon2).values_host - expected = gpdpolygon1.within(gpdpolygon2).values - assert (got == expected).all() + rhs = cuspatial.GeoSeries([Polygon([[-1, -1], [-2, 2], [2, 2], [2, -2]])]) + _test(lhs, rhs, "within") def test_max_polygons_within_max_polygons(polygon_generator): - gpdpolygon1 = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdpolygon2 = gpd.GeoSeries([*polygon_generator(31, 1)]) - polygon1 = cuspatial.from_geopandas(gpdpolygon1) - polygon2 = cuspatial.from_geopandas(gpdpolygon2) - got = polygon1.within(polygon2).values_host - expected = gpdpolygon1.within(gpdpolygon2).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*polygon_generator(31, 1)]) + _test(lhs, rhs, "within") def test_polygon_overlaps_linestring(): - gpdpolygon = gpd.GeoSeries( - Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) + lhs = cuspatial.GeoSeries( + [Polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])] ) - gpdline = gpd.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) - polygon = cuspatial.from_geopandas(gpdpolygon) - line = cuspatial.from_geopandas(gpdline) - got = polygon.overlaps(line).values_host - expected = gpdpolygon.overlaps(gpdline).values - assert (got == expected).all() + rhs = cuspatial.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) + _test(lhs, rhs, "overlaps") def test_max_polygons_overlaps_max_linestrings( polygon_generator, linestring_generator ): - gpdpolygon = gpd.GeoSeries([*polygon_generator(31, 0)]) - gpdline = gpd.GeoSeries([*linestring_generator(31, 5)]) - polygon = cuspatial.from_geopandas(gpdpolygon) - line = cuspatial.from_geopandas(gpdline) - got = polygon.overlaps(line).values_host - expected = gpdpolygon.overlaps(gpdline).values - assert (got == expected).all() + lhs = cuspatial.GeoSeries([*polygon_generator(31, 0)]) + rhs = cuspatial.GeoSeries([*linestring_generator(31, 5)]) + _test(lhs, rhs, "overlaps") diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 8cbddbda2..a07026e9b 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -4,6 +4,7 @@ import cudf +import cuspatial from cuspatial.core._column.geocolumn import ColumnType """Column-Type objects to use for simple syntax in the `DispatchDict` contained @@ -54,3 +55,157 @@ def _count_results_in_multipoint_geometries(point_indices, point_result): ) expected_count = point_indices_df.groupby("rhs_index").count().sort_index() return hits, expected_count + + +def _linestrings_from_polygons(geoseries): + xy = geoseries.polygons.xy + parts = geoseries.polygons.part_offset.take( + geoseries.polygons.geometry_offset + ) + rings = geoseries.polygons.ring_offset + return cuspatial.GeoSeries.from_linestrings_xy( + xy, + rings, + parts, + ) + + +def _linestrings_from_multipoints(geoseries): + """Convert rhs to linestrings.""" + points = cudf.DataFrame( + { + "x": geoseries.multipoints.x.repeat(2).reset_index(drop=True), + "y": geoseries.multipoints.y.repeat(2).reset_index(drop=True), + } + ).interleave_columns() + result = cuspatial.GeoSeries.from_linestrings_xy( + points, + geoseries.multipoints.geometry_offset * 2, + cp.arange(len(geoseries) + 1), + ) + return result + + +def _linestrings_from_points(geoseries): + """Convert rhs to linestrings. + TODO: Document""" + x = cp.repeat(geoseries.points.x, 2) + y = cp.repeat(geoseries.points.y, 2) + xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() + parts = cp.arange((len(geoseries) + 1)) * 2 + geometries = cp.arange(len(geoseries) + 1) + return cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) + + +def _linestrings_from_geometry(geoseries): + """Convert rhs to linestrings.""" + if geoseries.column_type == ColumnType.POINT: + return _linestrings_from_points(geoseries) + if geoseries.column_type == ColumnType.MULTIPOINT: + return _linestrings_from_multipoints(geoseries) + elif geoseries.column_type == ColumnType.LINESTRING: + return geoseries + elif geoseries.column_type == ColumnType.POLYGON: + return _linestrings_from_polygons(geoseries) + else: + raise NotImplementedError( + "Cannot convert type {} to linestrings".format(geoseries.type) + ) + + +def _multipoints_from_points(geoseries): + """Convert rhs to multipoints.""" + result = cuspatial.GeoSeries.from_multipoints_xy( + geoseries.points.xy, cp.arange((len(geoseries) + 1)) + ) + return result + + +def _multipoints_from_linestrings(geoseries): + """Convert rhs to multipoints.""" + xy = geoseries.lines.xy + mpoints = geoseries.lines.part_offset.take(geoseries.lines.geometry_offset) + return cuspatial.GeoSeries.from_multipoints_xy(xy, mpoints) + + +def _multipoints_from_polygons(geoseries): + """Convert rhs to multipoints. + TODO: Document""" + xy = geoseries.polygons.xy + polygon_offsets = geoseries.polygons.ring_offset.take( + geoseries.polygons.part_offset.take(geoseries.polygons.geometry_offset) + ) + return cuspatial.GeoSeries.from_multipoints_xy(xy, polygon_offsets) + + +def _multipoints_from_geometry(geoseries): + """Convert rhs to multipoints.""" + if geoseries.column_type == ColumnType.POINT: + return _multipoints_from_points(geoseries) + elif geoseries.column_type == ColumnType.MULTIPOINT: + return geoseries + elif geoseries.column_type == ColumnType.LINESTRING: + return _multipoints_from_linestrings(geoseries) + elif geoseries.column_type == ColumnType.POLYGON: + return _multipoints_from_polygons(geoseries) + else: + raise NotImplementedError( + "Cannot convert type {} to multipoints".format(geoseries.type) + ) + + +def _points_from_linestrings(geoseries): + """Convert rhs to points.""" + return cuspatial.GeoSeries.from_points_xy(geoseries.lines.xy) + + +def _points_from_polygons(geoseries): + """Convert rhs to points.""" + return cuspatial.GeoSeries.from_points_xy(geoseries.polygons.xy) + + +def _points_from_geometry(geoseries): + """Convert rhs to points.""" + if geoseries.column_type == ColumnType.POINT: + return geoseries + elif geoseries.column_type == ColumnType.LINESTRING: + return _points_from_linestrings(geoseries) + elif geoseries.column_type == ColumnType.POLYGON: + return _points_from_polygons(geoseries) + else: + raise NotImplementedError( + "Cannot convert type {} to points".format(geoseries.type) + ) + + +def _linestring_to_boundary(geoseries): + """Convert a linestrings column to a multipoints column + containing only the start and end of the linestrings.""" + xy = geoseries.lines.xy + mpoints = geoseries.lines.part_offset.take(geoseries.lines.geometry_offset) + return cuspatial.GeoSeries.from_multipoints_xy(xy, mpoints) + + +def _polygon_to_boundary(geoseries): + """Convert a polygon column to a linestring column.""" + xy = geoseries.polygons.xy + parts = geoseries.polygons.part_offset.take( + geoseries.polygons.geometry_offset + ) + rings = geoseries.polygons.ring_offset + return cuspatial.GeoSeries.from_linestrings_xy( + xy, + rings, + parts, + ) + + +def _is_complex(geoseries): + """Returns True if the GeoSeries contains complex types.""" + if len(geoseries.polygons.xy) > 0: + return True + if len(geoseries.lines.xy) > 0: + return True + if len(geoseries.multipoints.xy) > 0: + return True + return False From 1ff45897ca04d3324a7f4ff09309ff786bf70f37 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 19:09:49 +0000 Subject: [PATCH 051/126] Forgot geoseries.py --- python/cuspatial/cuspatial/core/geoseries.py | 223 ++++++++++++++++++- 1 file changed, 218 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 9ff85a7dc..d5a99bcd6 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -28,14 +28,20 @@ from cuspatial.core._column.geometa import Feature_Enum, GeoMeta from cuspatial.core.binpreds.binpred_dispatch import ( CONTAINS_DISPATCH, + CONTAINS_PROPERLY_DISPATCH, COVERS_DISPATCH, CROSSES_DISPATCH, DISJOINT_DISPATCH, EQUALS_DISPATCH, INTERSECTS_DISPATCH, OVERLAPS_DISPATCH, + TOUCHES_DISPATCH, WITHIN_DISPATCH, ) +from cuspatial.utils.binpred_utils import ( + _linestrings_from_geometry, + _multipoints_from_geometry, +) from cuspatial.utils.column_utils import ( contains_only_linestrings, contains_only_multipoints, @@ -157,6 +163,35 @@ def point_indices(self): "Polygons to return point indices." ) + @property + def sizes(self): + if contains_only_polygons(self): + # TODO: It isn't clear how to return the sizes of polygons. + # Care will need to be taken for handling holes and multis. + return cudf.Series( + ( + self.polygons.ring_offset[1:] + - self.polygons.ring_offset[:-1] + ) + - 1 + ) + elif contains_only_linestrings(self): + return self.lines.part_offset[1:] - self.lines.part_offset[:-1] + elif contains_only_multipoints(self): + return ( + self.multipoints.geometry_offset[1:] + - self.multipoints.geometry_offset[:-1] + ) + elif contains_only_points(self): + return cp.repeat(1, len(self)) + else: + if len(self) == 0: + return cudf.Series([0], dtype="int32") + raise TypeError( + "GeoSeries must contain only Points, MultiPoints, Lines, or " + "Polygons to return sizes." + ) + class GeoColumnAccessor: def __init__(self, list_series, meta): self._series = list_series @@ -661,7 +696,8 @@ def from_multipoints_xy(cls, multipoints_xy, geometry_offset): """ return cls( GeoColumn._from_multipoints_xy( - as_column(multipoints_xy), as_column(geometry_offset) + as_column(multipoints_xy), + as_column(geometry_offset, dtype="int32"), ) ) @@ -944,7 +980,60 @@ def reset_index( self.index = cudf_series.index return None - def contains_properly(self, other, align=False, allpairs=False): + def contains(self, other, align=False, allpairs=False, mode="full"): + """Returns a `Series` of `dtype('bool')` with value `True` for each + aligned geometry that contains _other_. + + Compute from a GeoSeries of points and a GeoSeries of polygons which + points are contained within the corresponding polygon. Polygon A + contains Point B if B is within the interior or on the boundary of A. + + If `allpairs=False`, the result will be a `Series` of `dtype('bool')`. + If `allpairs=True`, the result will be a `DataFrame` containing two + columns, `point_indices` and `polygon_indices`, each of which is a + `Series` of `dtype('int32')`. The `point_indices` `Series` contains + the indices of the points in the right GeoSeries, and the + `polygon_indices` `Series` contains the indices of the polygons in the + left GeoSeries. + + Parameters + ---------- + other : GeoSeries + align : bool, default False + If True, the two GeoSeries are aligned before performing the + operation. If False, the operation is performed on the + unaligned GeoSeries. If the two GeoSeries have different + lengths, the result will be a `Series` of `dtype('bool')`. + allpairs : bool, default False + If True, the result will be a `DataFrame` containing two + columns, `point_indices` and `polygon_indices`, each of which is a + `Series` of `dtype('int32')`. The `point_indices` `Series` contains + the indices of the points in the right GeoSeries, and the + `polygon_indices` `Series` contains the indices of the polygons in + the left GeoSeries. + mode : str, default "full" + If "full", the result will be a `Series` of `dtype('bool')` with + value `True` for each aligned geometry that contains _other_. + If "intersects", the result will be a `Series` of `dtype('bool')` + with value `True` for each aligned geometry that contains _other_ + or intersects _other_. + + Returns + ------- + Series or DataFrame + A `Series` of `dtype('bool')` with value `True` for each aligned + geometry that contains _other_. If `allpairs=True`, the result + will be a `DataFrame` containing two columns, `point_indices` and + `polygon_indices`, each of which is a `Series` of `dtype('int32')`. + """ + predicate = CONTAINS_DISPATCH[(self.column_type, other.column_type)]( + align=align, allpairs=allpairs, mode=mode + ) + return predicate(self, other) + + def contains_properly( + self, other, align=False, allpairs=False, mode="full" + ): """Returns a `Series` of `dtype('bool')` with value `True` for each aligned geometry that contains _other_. @@ -1037,9 +1126,9 @@ def contains_properly(self, other, align=False, allpairs=False): `point_indices` and `polygon_indices`, each of which is a `Series` of `dtype('int32')` in the case of `allpairs=True`. """ - predicate = CONTAINS_DISPATCH[(self.column_type, other.column_type)]( - align=align, allpairs=allpairs - ) + predicate = CONTAINS_PROPERLY_DISPATCH[ + (self.column_type, other.column_type) + ](align=align, allpairs=allpairs, mode=mode) return predicate(self, other) def geom_equals(self, other, align=True): @@ -1240,3 +1329,127 @@ def disjoint(self, other, align=True): align=align ) return predicate(self, other) + + def touches(self, other, align=True): + """Returns True for all aligned geometries that touch other, else + False. + + Geometries touch if they have at least one point in common, but their + interiors do not intersect. + + Parameters + ---------- + other + a cuspatial.GeoSeries + align=True + align the GeoSeries indexes before calling the binpred + + Returns + ------- + result : cudf.Series + A Series of boolean values indicating whether each geometry + touches the corresponding geometry in the input.""" + predicate = TOUCHES_DISPATCH[(self.column_type, other.column_type)]( + align=align + ) + return predicate(self, other) + + def _basic_equals(self, other): + from cuspatial.core.binops.equals_count import ( + pairwise_multipoint_equals_count, + ) + + lhs = _multipoints_from_geometry(self) + rhs = _multipoints_from_geometry(other) + result = pairwise_multipoint_equals_count(lhs, rhs) + return result > 0 + + def _basic_equals_all(self, other): + from cuspatial.core.binops.equals_count import ( + pairwise_multipoint_equals_count, + ) + + lhs = _multipoints_from_geometry(self) + rhs = _multipoints_from_geometry(other) + result = pairwise_multipoint_equals_count(lhs, rhs) + sizes = ( + rhs.multipoints.geometry_offset[1:] + - rhs.multipoints.geometry_offset[:-1] + ) + return result == sizes + + def _basic_equals_count(self, other): + from cuspatial.core.binops.equals_count import ( + pairwise_multipoint_equals_count, + ) + + lhs = _multipoints_from_geometry(self) + rhs = _multipoints_from_geometry(other) + result = pairwise_multipoint_equals_count(lhs, rhs) + return result + + def _basic_intersects_pli(self, other): + from cuspatial.core.binops.intersection import ( + pairwise_linestring_intersection, + ) + + lhs = _linestrings_from_geometry(self) + rhs = _linestrings_from_geometry(other) + return pairwise_linestring_intersection(lhs, rhs) + + def _basic_intersects_count(self, other): + result = self._basic_intersects_pli(other) + # Flatten result into list of sizes + is_offsets = cudf.Series(result[0]) + is_sizes = is_offsets[1:].reset_index(drop=True) - is_offsets[ + :-1 + ].reset_index(drop=True) + return is_sizes + + def _basic_intersects(self, other): + is_sizes = self._basic_intersects_count(other) + return is_sizes > 0 + + def _basic_intersects_at_point_only(self, other): + return self._basic_intersects_count(other) == 1 + + def _basic_intersects_through(self, other): + is_sizes = self._basic_intersects_count(other) + return is_sizes > 1 + + def _basic_contains_count(self, other): + lhs = self + rhs = _multipoints_from_geometry(other) + return lhs.contains_properly(rhs, mode="basic_count") + + def _basic_contains_none(self, other): + lhs = self + rhs = _multipoints_from_geometry(other) + return lhs.contains_properly(rhs, mode="basic_none") + + def _basic_contains_any(self, other): + lhs = self + rhs = _multipoints_from_geometry(other) + return lhs.contains_properly(rhs, mode="basic_any") + + def _basic_contains_all(self, other): + lhs = self + rhs = _multipoints_from_geometry(other) + return lhs.contains_properly(rhs, mode="basic_all") + + def repeat(self, ntimes): + """Repeats each geometry in the GeoSeries ntimes. + + Parameters + ---------- + ntimes : int + The number of times to repeat each geometry. + + Returns + ------- + result : GeoSeries + A new GeoSeries with each geometry repeated ntimes. + """ + return GeoSeries( + self._column.repeat(ntimes), index=self.index.repeat(ntimes) + ) From 367a4200fdcbec9370b2acf77dc5bdad5c0175aa Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 20:00:49 +0000 Subject: [PATCH 052/126] Refactor contains and contains properly and pass all tests. --- .../core/binpreds/binpred_dispatch.py | 3 + .../binpreds/feature_contains_properly.py | 4 +- .../core/binpreds/feature_intersects.py | 49 ++++++----- .../cuspatial/core/binpreds/feature_within.py | 88 ++++++++++++------- python/cuspatial/cuspatial/core/geoseries.py | 34 ++----- .../tests/binpreds/test_pip_only_binpreds.py | 6 +- 6 files changed, 100 insertions(+), 84 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py b/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py index f1cd0c51a..474841904 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py @@ -11,6 +11,9 @@ from cuspatial.core.binpreds.feature_contains import ( # NOQA F401 DispatchDict as CONTAINS_DISPATCH, ) +from cuspatial.core.binpreds.feature_contains_properly import ( # NOQA F401 + DispatchDict as CONTAINS_PROPERLY_DISPATCH, +) from cuspatial.core.binpreds.feature_covers import ( # NOQA F401 DispatchDict as COVERS_DISPATCH, ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 3d35fc814..235fac4af 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -146,11 +146,11 @@ def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): return reindex_pip_result elif self.config.mode == "basic_none": final_result = cudf.Series(cp.repeat([True], len(lhs))) - final_result.loc[reindex_pip_result["polygon_index"]] = False + final_result.loc[reindex_pip_result["point_index"]] = False return final_result elif self.config.mode == "basic_any": final_result = _false_series(len(lhs)) - final_result.loc[reindex_pip_result["polygon_index"]] = True + final_result.loc[reindex_pip_result["point_index"]] = True return final_result elif self.config.mode == "basic_all": sizes = ( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index ecc3673f9..5addb1428 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -13,7 +13,6 @@ NotImplementedPredicate, PreprocessorResult, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, @@ -74,18 +73,6 @@ def _get_intersecting_geometry_indices(self, lhs, op_result): ].reset_index(drop=True) return cp.arange(len(lhs))[is_sizes > 0] - def _linestrings_from_polygons(self, geoseries): - xy = geoseries.polygons.xy - parts = geoseries.polygons.part_offset.take( - geoseries.polygons.geometry_offset - ) - rings = geoseries.polygons.ring_offset - return cuspatial.GeoSeries.from_linestrings_xy( - xy, - rings, - parts, - ) - def _postprocess(self, lhs, rhs, op_result): """Postprocess the output GeoSeries to ensure that they are of the correct type for the predicate.""" @@ -100,12 +87,18 @@ class IntersectsByEquals(EqualsPredicateBase): pass -class PointPolygonIntersects(ContainsPredicateBase): +class PolygonPointIntersects(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): - """Swap LHS and RHS and call the normal contains processing.""" - self.lhs = rhs - self.rhs = lhs - return super()._preprocess(rhs, lhs) + contains = lhs._basic_contains_any(rhs) + intersects = lhs._basic_intersects(rhs) + return contains | intersects + + +class PointPolygonIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + contains = rhs._basic_contains_any(lhs) + intersects = rhs._basic_intersects(lhs) + return contains | intersects class LineStringPointIntersects(IntersectsPredicateBase): @@ -155,6 +148,20 @@ def _preprocess(self, lhs, rhs): ) +class LineStringPolygonIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains = rhs._basic_contains_any(lhs) + return intersects | contains + + +class PolygonPolygonIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains = rhs._basic_contains_any(lhs) + return intersects | contains + + """ Type dispatch dictionary for intersects binary predicates. """ DispatchDict = { (Point, Point): IntersectsByEquals, @@ -168,9 +175,9 @@ def _preprocess(self, lhs, rhs): (LineString, Point): LineStringPointIntersects, (LineString, MultiPoint): LineStringMultiPointIntersects, (LineString, LineString): IntersectsPredicateBase, - (LineString, Polygon): NotImplementedPredicate, - (Polygon, Point): NotImplementedPredicate, + (LineString, Polygon): LineStringPolygonIntersects, + (Polygon, Point): PolygonPointIntersects, (Polygon, MultiPoint): NotImplementedPredicate, (Polygon, LineString): NotImplementedPredicate, - (Polygon, Polygon): NotImplementedPredicate, + (Polygon, Polygon): PolygonPolygonIntersects, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 66bc21943..e6e452d73 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -2,16 +2,26 @@ import cudf -from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + NotImplementedPredicate, + PreprocessorResult, +) +from cuspatial.core.binpreds.complex_geometry_predicate import ( + ComplexGeometryPredicate, +) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.core.binpreds.feature_contains_properly import ( + ContainsProperlyPredicate, +) from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase -from cuspatial.utils import binpred_utils +from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, Point, Polygon, - _false_series, + _linestrings_from_geometry, ) @@ -28,18 +38,46 @@ class WithinPredicateBase(EqualsPredicateBase): pass +class WithinIntersectsPredicate(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + ls_lhs = _linestrings_from_geometry(lhs) + ls_rhs = _linestrings_from_geometry(rhs) + return self._compute_predicate( + lhs, rhs, PreprocessorResult(ls_lhs, ls_rhs) + ) + + def _compute_predicate(self, lhs, rhs, preprocessor_result): + intersects = rhs._basic_intersects(lhs) + equals = rhs._basic_equals(lhs) + return intersects & ~equals + + class PointPointWithin(WithinPredicateBase): def _postprocess(self, lhs, rhs, op_result): return cudf.Series(op_result.result) -class PointPolygonWithin(ContainsPredicateBase): +class PointLineStringWithin(WithinIntersectsPredicate): def _preprocess(self, lhs, rhs): # Note the order of arguments is reversed. return super()._preprocess(rhs, lhs) -class ComplexPolygonWithin(ContainsPredicateBase): +class PointPolygonWithin(ContainsPredicateBase): + def _preprocess(self, lhs, rhs): + return rhs._basic_contains_any(lhs) + + +class LineStringLineStringWithin(IntersectsPredicateBase): + def _compute_predicate(self, lhs, rhs, preprocessor_result): + intersects = rhs._basic_intersects(lhs) + equals = rhs._basic_equals_all(lhs) + return intersects & equals + + +class ComplexPolygonWithin( + ContainsProperlyPredicate, ComplexGeometryPredicate +): """Implements within for complex polygons. Depends on contains result for the types. @@ -53,41 +91,27 @@ def _preprocess(self, lhs, rhs): # Note the order of arguments is reversed. return super()._preprocess(rhs, lhs) - def _postprocess(self, lhs, rhs, op_result): - """Postprocess the output GeoSeries to ensure that they are of the - correct type for the predicate.""" - ( - hits, - expected_count, - ) = binpred_utils._count_results_in_multipoint_geometries( - op_result.point_indices, op_result.result - ) - result_df = hits.reset_index().merge( - expected_count.reset_index(), on="rhs_index" - ) - result_df["feature_in_polygon"] = ( - result_df["point_index_x"] >= result_df["point_index_y"] - ) - final_result = _false_series(len(lhs)) - final_result.loc[ - result_df["rhs_index"][result_df["feature_in_polygon"]] - ] = True - return final_result + +class LineStringPolygonWithin(BinPred): + def _preprocess(self, lhs, rhs): + contains_all = rhs._basic_contains_all(lhs) + intersects = rhs._basic_intersects(lhs) + return contains_all & intersects DispatchDict = { (Point, Point): PointPointWithin, - (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, MultiPoint): WithinIntersectsPredicate, + (Point, LineString): PointLineStringWithin, (Point, Polygon): PointPolygonWithin, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, - (MultiPoint, LineString): NotImplementedPredicate, + (MultiPoint, LineString): WithinIntersectsPredicate, (MultiPoint, Polygon): ComplexPolygonWithin, - (LineString, Point): NotImplementedPredicate, - (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, - (LineString, Polygon): ComplexPolygonWithin, + (LineString, Point): WithinIntersectsPredicate, + (LineString, MultiPoint): WithinIntersectsPredicate, + (LineString, LineString): LineStringLineStringWithin, + (LineString, Polygon): LineStringPolygonWithin, (Polygon, Point): WithinPredicateBase, (Polygon, MultiPoint): WithinPredicateBase, (Polygon, LineString): WithinPredicateBase, diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index d5a99bcd6..627f61809 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -166,16 +166,15 @@ def point_indices(self): @property def sizes(self): if contains_only_polygons(self): - # TODO: It isn't clear how to return the sizes of polygons. - # Care will need to be taken for handling holes and multis. - return cudf.Series( - ( - self.polygons.ring_offset[1:] - - self.polygons.ring_offset[:-1] - ) - - 1 + # The size of a polygon is the length of its exterior ring + # plus the lengths of its interior rings. + # The size of a multipolygon is the sum of all its polygons. + full_sizes = self.polygons.ring_offset.take( + self.polygons.part_offset.take(self.polygons.geometry_offset) ) + return full_sizes[1:] - full_sizes[:-1] - 1 elif contains_only_linestrings(self): + # Not supporting multilinestring yet return self.lines.part_offset[1:] - self.lines.part_offset[:-1] elif contains_only_multipoints(self): return ( @@ -183,7 +182,7 @@ def sizes(self): - self.multipoints.geometry_offset[:-1] ) elif contains_only_points(self): - return cp.repeat(1, len(self)) + return cp.repeat(cp.array(1), len(self)) else: if len(self) == 0: return cudf.Series([0], dtype="int32") @@ -1436,20 +1435,3 @@ def _basic_contains_all(self, other): lhs = self rhs = _multipoints_from_geometry(other) return lhs.contains_properly(rhs, mode="basic_all") - - def repeat(self, ntimes): - """Repeats each geometry in the GeoSeries ntimes. - - Parameters - ---------- - ntimes : int - The number of times to repeat each geometry. - - Returns - ------- - result : GeoSeries - A new GeoSeries with each geometry repeated ntimes. - """ - return GeoSeries( - self._column.repeat(ntimes), index=self.index.repeat(ntimes) - ) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py b/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py index eae2d7e1d..f3d1c10c8 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_pip_only_binpreds.py @@ -6,10 +6,10 @@ def _test(lhs, rhs, predicate): - cuspatiallhs = lhs.to_geopandas() - cuspatialrhs = rhs.to_geopandas() + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() got = getattr(lhs, predicate)(rhs).values_host - expected = getattr(cuspatiallhs, predicate)(cuspatialrhs).values + expected = getattr(gpdlhs, predicate)(gpdrhs).values assert (got == expected).all() From fbc1618d0f46139eb9306fe155a66359d1c2f295 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 20:05:29 +0000 Subject: [PATCH 053/126] Remove unneeded predicate. --- .../cuspatial/core/binpreds/feature_within.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index e6e452d73..ae53452c4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -1,9 +1,8 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -import cudf - from cuspatial.core.binpreds.binpred_interface import ( BinPred, + ImpossiblePredicate, NotImplementedPredicate, PreprocessorResult, ) @@ -52,11 +51,6 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): return intersects & ~equals -class PointPointWithin(WithinPredicateBase): - def _postprocess(self, lhs, rhs, op_result): - return cudf.Series(op_result.result) - - class PointLineStringWithin(WithinIntersectsPredicate): def _preprocess(self, lhs, rhs): # Note the order of arguments is reversed. @@ -100,7 +94,7 @@ def _preprocess(self, lhs, rhs): DispatchDict = { - (Point, Point): PointPointWithin, + (Point, Point): ImpossiblePredicate, (Point, MultiPoint): WithinIntersectsPredicate, (Point, LineString): PointLineStringWithin, (Point, Polygon): PointPolygonWithin, From 873525ae286c10945eebad05da073e04598a981e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 20:37:51 +0000 Subject: [PATCH 054/126] Tweak docs. --- .../cuspatial/core/binpreds/binpred_interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py index 4729b56e1..858efe96d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py @@ -22,6 +22,10 @@ class BinPredConfig: Whether to compute the binary predicate between all pairs of features in the left-hand and right-hand GeoSeries. Defaults to False. Only available with the contains predicate. + mode: str + The mode to use when computing the binary predicate. Defaults to + "full". Only available with the contains predicate and used + for internal operations. """ def __init__(self, **kwargs): @@ -69,7 +73,7 @@ def __init__( def __repr__(self): return f"PreprocessorResult(lhs={self.lhs}, rhs={self.rhs}, \ - final_rhs={self.points}, point_indices={self.point_indices})" + points={self.points}, point_indices={self.point_indices})" def __str__(self): return self.__repr__() @@ -91,9 +95,10 @@ class ContainsOpResult(OpResult): Point_index". The "polygon_index" column contains the index of the polygon that contains each point. The "point_index" column contains the index of each point that is contained by a polygon. - intersection_result: Tuple + intersection_result: Tuple (optional) A tuple containing the result of the intersection operation between the left-hand GeoSeries and the right-hand GeoSeries. + Used in .contains_properly. """ def __init__( From 0d84603cbb7979490f84798e900c1d5b79990e2e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 20:54:52 +0000 Subject: [PATCH 055/126] Clean up docs in feature_contains.py --- .../binpreds/complex_geometry_predicate.py | 40 +++++++++++++------ .../core/binpreds/feature_contains.py | 28 ------------- .../binpreds/feature_contains_properly.py | 2 +- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index e4f80a376..171251f63 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -26,12 +26,6 @@ class ComplexGeometryPredicate(BinPred): def _preprocess_multi(self, lhs, rhs): - # Breaks down complex geometries into their constituent parts. - # Passes a tuple o the preprocessed geometries and a tuple of - # the indices of the points in the original geometry. - # This is used by the postprocessor to reconstruct the original - # geometry. - # Child classes should not implement this method. """Flatten any rhs into only its points xy array. This is necessary because the basic predicate for contains, point-in-polygon, only accepts points. @@ -161,16 +155,35 @@ def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: return allpairs_result def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): - # Doesn't use op_result, but uses preprocessor_result to - # reconstruct the original geometry. - # Child classes should call this method to reconstruct the - # original geometry. + """Reconstruct the original geometry from the result of the + contains_properly call. + + Parameters + ---------- + lhs : GeoSeries + The left-hand side of the binary predicate. + rhs : GeoSeries + The right-hand side of the binary predicate. + preprocessor_result : PreprocessorResult + The result of the preprocessor. + op_result : ContainsProperlyOpResult + The result of the contains_properly call. + + Returns + ------- + cudf.Series + A boolean series indicating whether each feature in the + right-hand GeoSeries satisfies the requirements of the point- + in-polygon basic predicate with its corresponding feature in the + left-hand GeoSeries.""" - # Complex geometry postprocessor point_indices = preprocessor_result.point_indices allpairs_result = self._reindex_allpairs(lhs, op_result) if isinstance(allpairs_result, Series): return allpairs_result + # Hits is the number of calculated points in each polygon + # Expected count is the sizes of the features in the right-hand + # GeoSeries (hits, expected_count,) = _count_results_in_multipoint_geometries( point_indices, allpairs_result ) @@ -186,7 +199,10 @@ def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): ] = True return final_result - def _postprocess_simple(self, lhs, rhs, preprocessor_result, op_result): + def _postprocess_points(self, lhs, rhs, preprocessor_result, op_result): + """Reconstruct the original geometry from the result of the + contains_properly call. Used when the rhs is naturally points. + """ allpairs_result = self._reindex_allpairs(lhs, op_result) final_result = _false_series(len(rhs)) if len(lhs) == len(rhs): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 3a222319b..13e52bf92 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -22,31 +22,11 @@ class ContainsPredicateBase(ComplexGeometryPredicate): def __init__(self, **kwargs): - """`ContainsProperlyPredicateBase` constructor. - - Parameters - ---------- - allpairs: bool - Whether to compute all pairs of features in the left-hand and - right-hand GeoSeries. If False, the feature will be compared in a - 1:1 fashion with the corresponding feature in the other GeoSeries. - """ super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) self.config.mode = kwargs.get("mode", "full") def _preprocess(self, lhs, rhs): - # Preprocess multi-geometries and complex geometries into - # the correct input type for the contains predicate. - # This is done by saving the shapes of multi-geometries, - # then converting them all to single geometries. - # Single geometries are converted from their original - # lhs and rhs types to the types needed for the contains predicate. - - # point_indices: the indices of the points in the original - # geometry. - # geometry_offsets: the offsets of the multi-geometries in - # the original geometry. preprocessor_result = super()._preprocess_multi(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) @@ -56,17 +36,12 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): intersects = lhs._basic_intersects_count(rhs_points).reset_index( drop=True ) - # TODO: Need to handle multipolygon case. The [0] below ignores all - # but the first polygon in a multipolygon. # TODO: Need better point counting in intersection. return contains + intersects >= rhs.sizes class ContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): - # Compute the contains predicate for the given lhs and rhs. - # lhs and rhs are both cudf.Series of shapely geometries. - # Returns a ContainsOpResult object. return lhs._contains(rhs) @@ -77,9 +52,6 @@ def _preprocess(self, lhs, rhs): class LineStringMultiPointContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): - # Compute the contains predicate for the given lhs and rhs. - # lhs and rhs are both cudf.Series of shapely geometries. - # Returns a ContainsOpResult object. return lhs._linestring_multipoint_contains(rhs) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 235fac4af..d7b3eba2a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -216,7 +216,7 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): lhs, rhs, preprocessor_result, op_result ) else: - return super()._postprocess_simple( + return super()._postprocess_points( lhs, rhs, preprocessor_result, op_result ) From e4132b01fcc2cfc83303a34af4e968bb43edd24b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 21:01:41 +0000 Subject: [PATCH 056/126] Clean up contains_properly docs. --- .../binpreds/feature_contains_properly.py | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index d7b3eba2a..15f0ae79e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -52,23 +52,17 @@ def __init__(self, **kwargs): Whether to compute all pairs of features in the left-hand and right-hand GeoSeries. If False, the feature will be compared in a 1:1 fashion with the corresponding feature in the other GeoSeries. + mode: str + The mode to use for computing the predicate. The default is + "full", which computes true or false if the `.contains_properly` + predicate is satisfied. Other options include "basic_none", + "basic_any", "basic_all", and "basic_count". """ super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) self.config.mode = kwargs.get("mode", "full") def _preprocess(self, lhs, rhs): - # Preprocess multi-geometries and complex geometries into - # the correct input type for the contains predicate. - # This is done by saving the shapes of multi-geometries, - # then converting them all to single geometries. - # Single geometries are converted from their original - # lhs and rhs types to the types needed for the contains predicate. - - # point_indices: the indices of the points in the original - # geometry. - # geometry_offsets: the offsets of the multi-geometries in - # the original geometry. preprocessor_result = super()._preprocess_multi(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) @@ -101,16 +95,6 @@ def _compute_predicate( rhs: "GeoSeries", preprocessor_result: PreprocessorResult, ): - # _compute predicate no longer cares about preprocessor result - # because information is passed directly to the postprocessor. - # Creates an op_result and passes it and the preprocessor result - # to the postprocessor. - - # Calls various _basic_predicate methods to compute the - # predicate. - # .contains calls .basic_contains_properly and also .basic_intersects - # in order to assemble boundary-exclusive contains with intersection - # results. """Compute the contains_properly relationship between two GeoSeries. A feature A contains another feature B if no points of B lie in the exterior of A, and at least one point of the interior of B lies in the @@ -167,11 +151,6 @@ def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): return reindex_pip_result["polygon_index"].value_counts() def _postprocess(self, lhs, rhs, preprocessor_result, op_result): - # Downstream predicates inherit from ComplexGeometryPredicate - # that implements - # point reconstruction for complex types separately. - # Early return if individual points are required for downstream - # predicates. Handle `any`, `all`, `none` modes. """Postprocess the output GeoSeries to ensure that they are of the correct type for the predicate. From 8dfca1378ee3a79d2e09e9fd22f37d39c2bc04a9 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 21:03:36 +0000 Subject: [PATCH 057/126] Clean up ContainsProperlyByIntersection. --- .../cuspatial/core/binpreds/feature_contains_properly.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 15f0ae79e..69dad17d4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -209,14 +209,7 @@ class ContainsProperlyByIntersection(BinPred): """ def _preprocess(self, lhs, rhs): - from cuspatial.core.binpreds.binpred_dispatch import ( - INTERSECTS_DISPATCH, - ) - - predicate = INTERSECTS_DISPATCH[(lhs.column_type, rhs.column_type)]( - align=self.config.align - ) - return predicate(lhs, rhs) + return lhs._basic_intersects(rhs) class LineStringLineStringContainsProperly(BinPred): From a3d8b8a5d9dc9aa6678533b13df772c66343ac27 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 21:06:12 +0000 Subject: [PATCH 058/126] Clean up WithinIntersectsPredicate. --- .../cuspatial/cuspatial/core/binpreds/feature_within.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index ae53452c4..def457200 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -4,7 +4,6 @@ BinPred, ImpossiblePredicate, NotImplementedPredicate, - PreprocessorResult, ) from cuspatial.core.binpreds.complex_geometry_predicate import ( ComplexGeometryPredicate, @@ -20,7 +19,6 @@ MultiPoint, Point, Polygon, - _linestrings_from_geometry, ) @@ -39,13 +37,6 @@ class WithinPredicateBase(EqualsPredicateBase): class WithinIntersectsPredicate(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): - ls_lhs = _linestrings_from_geometry(lhs) - ls_rhs = _linestrings_from_geometry(rhs) - return self._compute_predicate( - lhs, rhs, PreprocessorResult(ls_lhs, ls_rhs) - ) - - def _compute_predicate(self, lhs, rhs, preprocessor_result): intersects = rhs._basic_intersects(lhs) equals = rhs._basic_equals(lhs) return intersects & ~equals From 008631e9c195d587230f38c121392301ca566c4c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 22:03:35 +0000 Subject: [PATCH 059/126] Write docs for geoseries.sizes --- python/cuspatial/cuspatial/core/geoseries.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 627f61809..317669998 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -165,6 +165,24 @@ def point_indices(self): @property def sizes(self): + """Returns the size in points of each geometry in the GeoSeries." + + Returns + ------- + sizes : cudf.Series + The size of each geometry in the GeoSeries. + + Notes + ----- + The size of a geometry is the number of points it contains. + The size of a polygon is the number of points in its exterior ring + plus the number of points in its interior rings. + The size of a multipolygon is the sum of all its polygons. + The size of a linestring is the number of points in its single line. + The size of a multilinestring is the sum of all its linestrings. + The size of a multipoint is the number of points in its single point. + The size of a point is 1. + """ if contains_only_polygons(self): # The size of a polygon is the length of its exterior ring # plus the lengths of its interior rings. @@ -175,7 +193,10 @@ def sizes(self): return full_sizes[1:] - full_sizes[:-1] - 1 elif contains_only_linestrings(self): # Not supporting multilinestring yet - return self.lines.part_offset[1:] - self.lines.part_offset[:-1] + full_sizes = self.lines.part_offset.take( + self.lines.geometry_offset + ) + return full_sizes[1:] - full_sizes[:-1] elif contains_only_multipoints(self): return ( self.multipoints.geometry_offset[1:] From 550fa9be931725ab3741f1b10a340e10e4f63969 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 22:11:00 +0000 Subject: [PATCH 060/126] Clean up docs and write docstrings for all _basic utility methods. --- python/cuspatial/cuspatial/core/geoseries.py | 36 ++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 317669998..2ea167ebd 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1016,21 +1016,25 @@ def contains(self, other, align=False, allpairs=False, mode="full"): `polygon_indices` `Series` contains the indices of the polygons in the left GeoSeries. + Notes + ----- + `allpairs=True` excludes geometries that contain points in the + boundary of A. + Parameters ---------- other : GeoSeries align : bool, default False If True, the two GeoSeries are aligned before performing the operation. If False, the operation is performed on the - unaligned GeoSeries. If the two GeoSeries have different - lengths, the result will be a `Series` of `dtype('bool')`. + unaligned GeoSeries. allpairs : bool, default False If True, the result will be a `DataFrame` containing two columns, `point_indices` and `polygon_indices`, each of which is a `Series` of `dtype('int32')`. The `point_indices` `Series` contains the indices of the points in the right GeoSeries, and the `polygon_indices` `Series` contains the indices of the polygons in - the left GeoSeries. + the left GeoSeries. Excludes boundary points. mode : str, default "full" If "full", the result will be a `Series` of `dtype('bool')` with value `True` for each aligned geometry that contains _other_. @@ -1375,6 +1379,8 @@ def touches(self, other, align=True): return predicate(self, other) def _basic_equals(self, other): + """Utility method that returns True if any point in the lhs geometry + is equal to a point in the rhs geometry.""" from cuspatial.core.binops.equals_count import ( pairwise_multipoint_equals_count, ) @@ -1385,6 +1391,8 @@ def _basic_equals(self, other): return result > 0 def _basic_equals_all(self, other): + """Utility method that returns True if all points in the lhs geometry + are equal to points in the rhs geometry.""" from cuspatial.core.binops.equals_count import ( pairwise_multipoint_equals_count, ) @@ -1399,6 +1407,8 @@ def _basic_equals_all(self, other): return result == sizes def _basic_equals_count(self, other): + """Utility method that returns the number of points in the lhs geometry + that are equal to a point in the rhs geometry.""" from cuspatial.core.binops.equals_count import ( pairwise_multipoint_equals_count, ) @@ -1409,6 +1419,8 @@ def _basic_equals_count(self, other): return result def _basic_intersects_pli(self, other): + """Utility method that returns the original results of + `pairwise_linestring_intersection`.""" from cuspatial.core.binops.intersection import ( pairwise_linestring_intersection, ) @@ -1418,6 +1430,8 @@ def _basic_intersects_pli(self, other): return pairwise_linestring_intersection(lhs, rhs) def _basic_intersects_count(self, other): + """Utility method that returns the number of points in the lhs geometry + that intersect with the rhs geometry.""" result = self._basic_intersects_pli(other) # Flatten result into list of sizes is_offsets = cudf.Series(result[0]) @@ -1427,32 +1441,48 @@ def _basic_intersects_count(self, other): return is_sizes def _basic_intersects(self, other): + """Utility method that returns True if any point in the lhs geometry + intersects with the rhs geometry.""" is_sizes = self._basic_intersects_count(other) return is_sizes > 0 def _basic_intersects_at_point_only(self, other): + """Utility method that returns True if only a single point in the lhs + geometry intersects with the rhs geometry.""" return self._basic_intersects_count(other) == 1 def _basic_intersects_through(self, other): + """Utility method that returns True if at least two points in the lhs + geometry intersect with the rhs geometry.""" is_sizes = self._basic_intersects_count(other) return is_sizes > 1 def _basic_contains_count(self, other): + """Utility method that returns the number of points in the lhs geometry + that are contained_properly in the rhs geometry. + """ lhs = self rhs = _multipoints_from_geometry(other) return lhs.contains_properly(rhs, mode="basic_count") def _basic_contains_none(self, other): + """Utility method that returns True if none of the points in the lhs + geometry are contained_properly in the rhs geometry.""" lhs = self rhs = _multipoints_from_geometry(other) return lhs.contains_properly(rhs, mode="basic_none") def _basic_contains_any(self, other): + """Utility method that returns True if any point in the lhs geometry + is contained_properly in the rhs geometry.""" lhs = self rhs = _multipoints_from_geometry(other) return lhs.contains_properly(rhs, mode="basic_any") def _basic_contains_all(self, other): + """Utililty method that returns True if all points in the lhs geometry + are contained_properly in the rhs geometry. Equivalent to the public + `.contains_properly call.""" lhs = self rhs = _multipoints_from_geometry(other) return lhs.contains_properly(rhs, mode="basic_all") From f45acda8b594a7e2fd27f707ae4fbfc1c2ab487a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 22:42:47 +0000 Subject: [PATCH 061/126] Document and improve binpred_utils.py --- .../cuspatial/utils/binpred_utils.py | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index a07026e9b..32de863a8 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -58,6 +58,8 @@ def _count_results_in_multipoint_geometries(point_indices, point_result): def _linestrings_from_polygons(geoseries): + """Converts the exterior and interior rings of a geoseries of polygons + into a geoseries of linestrings.""" xy = geoseries.polygons.xy parts = geoseries.polygons.part_offset.take( geoseries.polygons.geometry_offset @@ -71,7 +73,8 @@ def _linestrings_from_polygons(geoseries): def _linestrings_from_multipoints(geoseries): - """Convert rhs to linestrings.""" + """Converts a geoseries of multipoints into a geoseries of + linestrings.""" points = cudf.DataFrame( { "x": geoseries.multipoints.x.repeat(2).reset_index(drop=True), @@ -87,8 +90,27 @@ def _linestrings_from_multipoints(geoseries): def _linestrings_from_points(geoseries): - """Convert rhs to linestrings. - TODO: Document""" + """Converts a geoseries of points into a geoseries of linestrings. + + Linestrings converted to points are represented as a segment of + length two, with the beginning and ending of the segment being the + same point. + + Example + ------- + >>> import cuspatial + >>> from cuspatial.utils.binpred_utils import ( + ... _linestrings_from_points + ... ) + >>> from shapely.geometry import Point + >>> points = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) + >>> linestrings = _linestrings_from_points(points) + >>> linestrings + Out[1]: + 0 LINESTRING (0.00000 0.00000, 0.00000 0.00000) + 1 LINESTRING (1.00000 1.00000, 1.00000 1.00000) + dtype: geometry + """ x = cp.repeat(geoseries.points.x, 2) y = cp.repeat(geoseries.points.y, 2) xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() @@ -98,7 +120,8 @@ def _linestrings_from_points(geoseries): def _linestrings_from_geometry(geoseries): - """Convert rhs to linestrings.""" + """Wrapper function that converts any homogeneous geoseries into + a geoseries of linestrings.""" if geoseries.column_type == ColumnType.POINT: return _linestrings_from_points(geoseries) if geoseries.column_type == ColumnType.MULTIPOINT: @@ -114,7 +137,8 @@ def _linestrings_from_geometry(geoseries): def _multipoints_from_points(geoseries): - """Convert rhs to multipoints.""" + """Converts a geoseries of points into a geoseries of length 1 + multipoints.""" result = cuspatial.GeoSeries.from_multipoints_xy( geoseries.points.xy, cp.arange((len(geoseries) + 1)) ) @@ -122,15 +146,17 @@ def _multipoints_from_points(geoseries): def _multipoints_from_linestrings(geoseries): - """Convert rhs to multipoints.""" + """Converts a geoseries of linestrings into a geoseries of + multipoints. MultiLineStrings are converted into a single multipoint.""" xy = geoseries.lines.xy mpoints = geoseries.lines.part_offset.take(geoseries.lines.geometry_offset) return cuspatial.GeoSeries.from_multipoints_xy(xy, mpoints) def _multipoints_from_polygons(geoseries): - """Convert rhs to multipoints. - TODO: Document""" + """Converts a geoseries of polygons into a geoseries of multipoints. + All exterior and interior points become points in each multipoint object. + """ xy = geoseries.polygons.xy polygon_offsets = geoseries.polygons.ring_offset.take( geoseries.polygons.part_offset.take(geoseries.polygons.geometry_offset) @@ -139,7 +165,8 @@ def _multipoints_from_polygons(geoseries): def _multipoints_from_geometry(geoseries): - """Convert rhs to multipoints.""" + """Wrapper function that converts any homogeneous geoseries into + a geoseries of multipoints.""" if geoseries.column_type == ColumnType.POINT: return _multipoints_from_points(geoseries) elif geoseries.column_type == ColumnType.MULTIPOINT: @@ -155,17 +182,22 @@ def _multipoints_from_geometry(geoseries): def _points_from_linestrings(geoseries): - """Convert rhs to points.""" + """Convert a geoseries of linestrings into a geoseries of points. + The length of the result is equal to the sum of the lengths of the + linestrings in the original geoseries.""" return cuspatial.GeoSeries.from_points_xy(geoseries.lines.xy) def _points_from_polygons(geoseries): - """Convert rhs to points.""" + """Convert a geoseries of linestrings into a geoseries of points. + The length of the result is equal to the sum of the lengths of the + polygons in the original geoseries.""" return cuspatial.GeoSeries.from_points_xy(geoseries.polygons.xy) def _points_from_geometry(geoseries): - """Convert rhs to points.""" + """Wrapper function that converts any homogeneous geoseries into + a geoseries of points.""" if geoseries.column_type == ColumnType.POINT: return geoseries elif geoseries.column_type == ColumnType.LINESTRING: @@ -179,15 +211,34 @@ def _points_from_geometry(geoseries): def _linestring_to_boundary(geoseries): - """Convert a linestrings column to a multipoints column + """Convert a geoseries of linestrings to a geoseries of multipoints containing only the start and end of the linestrings.""" - xy = geoseries.lines.xy - mpoints = geoseries.lines.part_offset.take(geoseries.lines.geometry_offset) + x = geoseries.lines.x + y = geoseries.lines.y + starts = geoseries.lines.part_offset.take(geoseries.lines.geometry_offset) + ends = (starts - 1)[1:] + starts = starts[:-1] + points_x = cudf.DataFrame( + { + "starts": x[starts].reset_index(drop=True), + "ends": x[ends].reset_index(drop=True), + } + ).interleave_columns() + points_y = cudf.DataFrame( + { + "starts": y[starts].reset_index(drop=True), + "ends": y[ends].reset_index(drop=True), + } + ).interleave_columns() + xy = cudf.DataFrame({"x": points_x, "y": points_y}).interleave_columns() + mpoints = cp.arange(len(starts) + 1) * 2 return cuspatial.GeoSeries.from_multipoints_xy(xy, mpoints) def _polygon_to_boundary(geoseries): - """Convert a polygon column to a linestring column.""" + """Convert a geoseries of polygons to a geoseries of linestrings or + multilinestrings containing only the exterior and interior boundaries + of the polygons.""" xy = geoseries.polygons.xy parts = geoseries.polygons.part_offset.take( geoseries.polygons.geometry_offset @@ -201,7 +252,8 @@ def _polygon_to_boundary(geoseries): def _is_complex(geoseries): - """Returns True if the GeoSeries contains complex types.""" + """Returns True if the GeoSeries contains non-point features that + need to be reconstructed after basic predicates have computed.""" if len(geoseries.polygons.xy) > 0: return True if len(geoseries.lines.xy) > 0: From 7198f40d3de0a3fe63fc0fb2e656cf69013e9942 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 22:52:48 +0000 Subject: [PATCH 062/126] Move files from master branch to new PR. --- .../cuspatial/core/binpreds/feature_covers.py | 26 +- .../core/binpreds/feature_crosses.py | 56 +++-- .../core/binpreds/feature_disjoint.py | 35 ++- .../cuspatial/core/binpreds/feature_equals.py | 18 +- .../core/binpreds/feature_overlaps.py | 28 ++- .../core/binpreds/feature_touches.py | 84 +++++-- .../test_contains_basic_predicate.py | 36 +++ .../basicpreds/test_equals_basic_predicate.py | 47 ++++ .../tests/basicpreds/test_intersections.py | 236 ++++++++++++++++++ .../test_intersects_basic_predicate.py | 66 +++++ 10 files changed, 577 insertions(+), 55 deletions(-) create mode 100644 python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py create mode 100644 python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py create mode 100644 python/cuspatial/cuspatial/tests/basicpreds/test_intersections.py create mode 100644 python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 8c32ce9e4..7d673e270 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -1,10 +1,14 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate +from cuspatial.core.binpreds.binpred_interface import ( + ImpossiblePredicate, + NotImplementedPredicate, +) +from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.core.binpreds.feature_intersects import ( + IntersectsPredicateBase, LineStringPointIntersects, - PointLineStringIntersects, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -36,10 +40,22 @@ class CoversPredicateBase(EqualsPredicateBase): pass +class LineStringLineStringCovers(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + return rhs._basic_equals_all(lhs) + + +class PolygonPolygonCovers(ContainsPredicateBase): + def _preprocess(self, lhs, rhs): + contains_none = rhs._basic_contains_none(lhs) + equals = rhs._basic_equals(lhs) + return contains_none | equals + + DispatchDict = { (Point, Point): CoversPredicateBase, (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): PointLineStringIntersects, + (Point, LineString): ImpossiblePredicate, (Point, Polygon): CoversPredicateBase, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, @@ -47,10 +63,10 @@ class CoversPredicateBase(EqualsPredicateBase): (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointIntersects, (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, + (LineString, LineString): LineStringLineStringCovers, (LineString, Polygon): CoversPredicateBase, (Polygon, Point): CoversPredicateBase, (Polygon, MultiPoint): CoversPredicateBase, (Polygon, LineString): CoversPredicateBase, - (Polygon, Polygon): CoversPredicateBase, + (Polygon, Polygon): PolygonPolygonCovers, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index e1ea40a92..51b82fa15 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -1,10 +1,8 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from cuspatial.core.binpreds.binpred_interface import ( - ImpossiblePredicate, - NotImplementedPredicate, -) +from cuspatial.core.binpreds.binpred_interface import ImpossiblePredicate from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase +from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -30,27 +28,55 @@ class CrossesPredicateBase(EqualsPredicateBase): pass +class CrossesByIntersectionPredicate(IntersectsPredicateBase): + def _compute_predicate(self, lhs, rhs, preprocessor_result): + intersects = rhs._basic_intersects(lhs) + equals = rhs._basic_equals(lhs) + return intersects & ~equals + + +class PolygonLineStringCrosses(CrossesByIntersectionPredicate): + def _compute_predicate(self, lhs, rhs, preprocessor_result): + intersects_through = lhs._basic_intersects_through(rhs) + # intersects_any = lhs._basic_intersects(rhs) + # intersects_points = lhs._basic_intersects_points(rhs) + equals = rhs._basic_equals(lhs) + # contains_any = lhs._basic_contains_any(rhs) + contains_all = lhs._basic_contains_all(rhs) + return ~contains_all & ~equals & intersects_through + + +class LineStringPolygonCrosses(PolygonLineStringCrosses): + def _preprocess(self, lhs, rhs): + """Note the order of arguments is reversed.""" + return super()._preprocess(rhs, lhs) + + class PointPointCrosses(CrossesPredicateBase): def _preprocess(self, lhs, rhs): """Points can't cross other points, so we return False.""" return _false_series(len(lhs)) +class PolygonPolygonCrosses(PolygonLineStringCrosses): + pass + + DispatchDict = { (Point, Point): PointPointCrosses, - (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, MultiPoint): ImpossiblePredicate, + (Point, LineString): ImpossiblePredicate, (Point, Polygon): CrossesPredicateBase, - (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, - (MultiPoint, LineString): NotImplementedPredicate, - (MultiPoint, Polygon): NotImplementedPredicate, + (MultiPoint, Point): ImpossiblePredicate, + (MultiPoint, MultiPoint): ImpossiblePredicate, + (MultiPoint, LineString): ImpossiblePredicate, + (MultiPoint, Polygon): ImpossiblePredicate, (LineString, Point): ImpossiblePredicate, - (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, - (LineString, Polygon): NotImplementedPredicate, + (LineString, MultiPoint): ImpossiblePredicate, + (LineString, LineString): CrossesByIntersectionPredicate, + (LineString, Polygon): LineStringPolygonCrosses, (Polygon, Point): CrossesPredicateBase, (Polygon, MultiPoint): CrossesPredicateBase, - (Polygon, LineString): CrossesPredicateBase, - (Polygon, Polygon): CrossesPredicateBase, + (Polygon, LineString): PolygonLineStringCrosses, + (Polygon, Polygon): PolygonPolygonCrosses, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index 92541b95f..f6b943493 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -16,7 +16,7 @@ ) -class ContainsDisjoint(BinPred): +class DisjointByWayOfContains(BinPred): def _preprocess(self, lhs, rhs): """Disjoint is the opposite of contains, so just implement contains and then negate the result. @@ -42,6 +42,13 @@ def _postprocess(self, lhs, rhs, op_result): return ~result +class PointPolygonDisjoint(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains = lhs._basic_contains_any(rhs) + return ~intersects & ~contains + + class LineStringPointDisjoint(PointLineStringDisjoint): def _preprocess(self, lhs, rhs): """Swap ordering for Intersects.""" @@ -56,21 +63,35 @@ def _postprocess(self, lhs, rhs, op_result): return ~result +class LineStringPolygonDisjoint(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains = rhs._basic_contains_any(lhs) + return ~intersects & ~contains + + +class PolygonPolygonDisjoint(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains = rhs._basic_contains_any(lhs) + return ~intersects & ~contains + + DispatchDict = { - (Point, Point): ContainsDisjoint, + (Point, Point): DisjointByWayOfContains, (Point, MultiPoint): NotImplementedPredicate, (Point, LineString): PointLineStringDisjoint, - (Point, Polygon): ContainsDisjoint, + (Point, Polygon): PointPolygonDisjoint, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, - (MultiPoint, Polygon): NotImplementedPredicate, + (MultiPoint, Polygon): LineStringPolygonDisjoint, (LineString, Point): LineStringPointDisjoint, (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): LineStringLineStringDisjoint, - (LineString, Polygon): NotImplementedPredicate, - (Polygon, Point): ContainsDisjoint, + (LineString, Polygon): LineStringPolygonDisjoint, + (Polygon, Point): DisjointByWayOfContains, (Polygon, MultiPoint): NotImplementedPredicate, (Polygon, LineString): NotImplementedPredicate, - (Polygon, Polygon): NotImplementedPredicate, + (Polygon, Polygon): PolygonPolygonDisjoint, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index dc52423d7..d4fdae769 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py @@ -13,6 +13,7 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, EqualsOpResult, + ImpossiblePredicate, NotImplementedPredicate, PreprocessorResult, ) @@ -334,11 +335,24 @@ def _preprocess(self, lhs, rhs): return _false_series(len(lhs)) +class PolygonPolygonEquals(EqualsPredicateBase): + def _compute_predicate(self, lhs, rhs, preprocessor_result): + """Two polygons are equal if they contain each other.""" + from cuspatial.core.binpreds.binpred_dispatch import CONTAINS_DISPATCH + + predicate = CONTAINS_DISPATCH[(lhs.column_type, rhs.column_type)]( + align=self.config.align + ) + lhs_contains_rhs = predicate(lhs, rhs) + rhs_contains_lhs = predicate(rhs, lhs) + return lhs_contains_rhs & rhs_contains_lhs + + """DispatchDict for Equals operations.""" DispatchDict = { (Point, Point): EqualsPredicateBase, (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, LineString): ImpossiblePredicate, (Point, Polygon): EqualsPredicateBase, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): MultiPointMultiPointEquals, @@ -351,5 +365,5 @@ def _preprocess(self, lhs, rhs): (Polygon, Point): EqualsPredicateBase, (Polygon, MultiPoint): EqualsPredicateBase, (Polygon, LineString): EqualsPredicateBase, - (Polygon, Polygon): EqualsPredicateBase, + (Polygon, Polygon): PolygonPolygonEquals, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index 20bad01a3..f1bfa70fb 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -2,10 +2,7 @@ import cudf -from cuspatial.core.binpreds.binpred_interface import ( - ImpossiblePredicate, - NotImplementedPredicate, -) +from cuspatial.core.binpreds.binpred_interface import ImpossiblePredicate from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -36,6 +33,13 @@ class OverlapsPredicateBase(EqualsPredicateBase): pass +class PolygonPolygonOverlaps(ContainsPredicateBase): + def _preprocess(self, lhs, rhs): + equals_all = lhs._basic_equals_all(rhs) + intersects_not_touches = lhs._basic_intersects_through(rhs) + return ~equals_all & intersects_not_touches + + class PolygonPointOverlaps(ContainsPredicateBase): def _postprocess(self, lhs, rhs, op_result): if not has_same_geometry(lhs, rhs) or len(op_result.point_result) == 0: @@ -62,19 +66,19 @@ def _postprocess(self, lhs, rhs, op_result): """Dispatch table for overlaps binary predicate.""" DispatchDict = { (Point, Point): ImpossiblePredicate, - (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, MultiPoint): ImpossiblePredicate, + (Point, LineString): ImpossiblePredicate, (Point, Polygon): OverlapsPredicateBase, - (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, - (MultiPoint, LineString): NotImplementedPredicate, - (MultiPoint, Polygon): NotImplementedPredicate, + (MultiPoint, Point): ImpossiblePredicate, + (MultiPoint, MultiPoint): ImpossiblePredicate, + (MultiPoint, LineString): ImpossiblePredicate, + (MultiPoint, Polygon): ImpossiblePredicate, (LineString, Point): ImpossiblePredicate, - (LineString, MultiPoint): NotImplementedPredicate, + (LineString, MultiPoint): ImpossiblePredicate, (LineString, LineString): ImpossiblePredicate, (LineString, Polygon): ImpossiblePredicate, (Polygon, Point): OverlapsPredicateBase, (Polygon, MultiPoint): OverlapsPredicateBase, (Polygon, LineString): OverlapsPredicateBase, - (Polygon, Polygon): OverlapsPredicateBase, + (Polygon, Polygon): PolygonPolygonOverlaps, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 8b4844577..aa81db33b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -1,8 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. from cuspatial.core.binpreds.binpred_interface import ( + BinPred, ImpossiblePredicate, - NotImplementedPredicate, + PreprocessorResult, ) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -10,6 +11,8 @@ MultiPoint, Point, Polygon, + _linestring_to_boundary, + _polygon_to_boundary, ) @@ -27,24 +30,77 @@ class TouchesPredicateBase(ContainsPredicateBase): (Polygon, Polygon) """ - pass + def _compute_predicate( + self, + lhs, + rhs, + preprocessor_result: PreprocessorResult, + ): + # contains = lhs._basic_contains_any(rhs) + equals = lhs._basic_equals(rhs) + intersects = lhs._basic_intersects(rhs) + return equals | intersects + + +class PointLineStringTouches(BinPred): + def _preprocess(self, lhs, rhs): + return lhs._basic_equals(rhs) + + +class PointPolygonTouches(ContainsPredicateBase): + def _preprocess(self, lhs, rhs): + # Reverse argument order. + equals_all = rhs._basic_equals_all(lhs) + touches = rhs._basic_intersects(lhs) + return ~equals_all & touches + + +class LineStringLineStringTouches(BinPred): + def _preprocess(self, lhs, rhs): + """A and B have at least one point in common, and the common points + lie in at least one boundary""" + lhs_boundary = _linestring_to_boundary(lhs) + rhs_boundary = _linestring_to_boundary(rhs) + point_intersections = lhs._basic_intersects_at_point_only(rhs) + boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) + equals = lhs._basic_equals_all(rhs) + return point_intersections & boundary_intersects & ~equals + + +class LineStringPolygonTouches(BinPred): + def _preprocess(self, lhs, rhs): + lhs_boundary = _linestring_to_boundary(lhs) + rhs_boundary = _polygon_to_boundary(rhs) + boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) + interior_contains_any = rhs._basic_contains_any(lhs) + return boundary_intersects & ~interior_contains_any + + +class PolygonPolygonTouches(BinPred): + def _preprocess(self, lhs, rhs): + lhs_boundary = _polygon_to_boundary(lhs) + rhs_boundary = _polygon_to_boundary(rhs) + boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) + contains = rhs._basic_contains_any(lhs) + # Count results here? + return boundary_intersects & ~contains DispatchDict = { (Point, Point): ImpossiblePredicate, - (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, - (Point, Polygon): TouchesPredicateBase, - (MultiPoint, Point): NotImplementedPredicate, - (MultiPoint, MultiPoint): NotImplementedPredicate, - (MultiPoint, LineString): NotImplementedPredicate, - (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, - (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, - (LineString, Polygon): NotImplementedPredicate, + (Point, MultiPoint): TouchesPredicateBase, + (Point, LineString): PointLineStringTouches, + (Point, Polygon): PointPolygonTouches, + (MultiPoint, Point): TouchesPredicateBase, + (MultiPoint, MultiPoint): TouchesPredicateBase, + (MultiPoint, LineString): TouchesPredicateBase, + (MultiPoint, Polygon): TouchesPredicateBase, + (LineString, Point): TouchesPredicateBase, + (LineString, MultiPoint): TouchesPredicateBase, + (LineString, LineString): LineStringLineStringTouches, + (LineString, Polygon): LineStringPolygonTouches, (Polygon, Point): TouchesPredicateBase, (Polygon, MultiPoint): TouchesPredicateBase, (Polygon, LineString): TouchesPredicateBase, - (Polygon, Polygon): TouchesPredicateBase, + (Polygon, Polygon): PolygonPolygonTouches, } diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py new file mode 100644 index 000000000..f6b0a4087 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py @@ -0,0 +1,36 @@ +import pandas as pd +from shapely.geometry import Point, Polygon + +import cuspatial + + +def test_basic_contains_none_outside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(2, 2)]) + pd.testing.assert_series_equal( + lhs._basic_contains_none(rhs).to_pandas(), pd.Series([True]) + ) + + +def test_basic_contains_none_inside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0.5, 0.5)]) + pd.testing.assert_series_equal( + lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) + ) + + +def test_basic_contains_none_point(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0)]) + pd.testing.assert_series_equal( + lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) + ) + + +def test_basic_contains_none_edge(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0.5)]) + pd.testing.assert_series_equal( + lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) + ) diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py new file mode 100644 index 000000000..0174312c6 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py @@ -0,0 +1,47 @@ +import pandas as pd +from pandas.testing import assert_series_equal +from shapely.geometry import Point + +import cuspatial + + +def test_single_true(): + p1 = cuspatial.GeoSeries([Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(0, 0)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([True])) + + +def test_single_false(): + p1 = cuspatial.GeoSeries([Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([False])) + + +def test_true_false(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) + p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([True, False])) + + +def test_false_true(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([False, True])) + + +def test_true_false_true(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) + p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2), Point(2, 2)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([True, False, True])) + + +def test_false_true_false(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0), Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0), Point(2, 2)]) + result = p1._basic_equals(p2) + assert_series_equal(result.to_pandas(), pd.Series([False, True, False])) diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_intersections.py b/python/cuspatial/cuspatial/tests/basicpreds/test_intersections.py new file mode 100644 index 000000000..a3bcb0cd0 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_intersections.py @@ -0,0 +1,236 @@ +import geopandas as gpd +import pandas as pd +from geopandas.testing import assert_geoseries_equal +from pandas.testing import assert_frame_equal, assert_series_equal +from shapely.geometry import LineString, MultiLineString, Point + +import cuspatial +from cuspatial.core.binops.intersection import pairwise_linestring_intersection + + +def run_test(s1, s2, expect_offset, expect_geom, expect_ids): + offset, geoms, ids = pairwise_linestring_intersection( + cuspatial.from_geopandas(s1), cuspatial.from_geopandas(s2) + ) + + assert_series_equal(expect_offset, offset.to_pandas(), check_dtype=False) + assert_geoseries_equal(expect_geom, geoms.to_geopandas()) + assert_frame_equal(expect_ids, ids.to_pandas()) + + +def test_empty(): + s1 = gpd.GeoSeries([]) + s2 = gpd.GeoSeries([]) + + expect_offset = pd.Series([0]) + expect_geom = gpd.GeoSeries([]) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [], + "lhs_segment_id": [], + "rhs_linestring_id": [], + "rhs_segment_id": [], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_one_pair(): + s1 = gpd.GeoSeries([LineString([(0, 0), (1, 1)])]) + s2 = gpd.GeoSeries([LineString([(0, 1), (1, 0)])]) + + expect_offset = pd.Series([0, 1]) + expect_geom = s1.intersection(s2) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0]], + "lhs_segment_id": [[0]], + "rhs_linestring_id": [[0]], + "rhs_segment_id": [[0]], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_two_pairs(): + s1 = gpd.GeoSeries( + [LineString([(0, 0), (1, 1)]), LineString([(0, 2), (2, 2), (2, 0)])] + ) + s2 = gpd.GeoSeries( + [ + LineString([(0, 1), (1, 0)]), + LineString([(1, 1), (1, 3), (3, 3), (3, 1), (1.5, 1)]), + ] + ) + + expect_offset = pd.Series([0, 1, 3]) + expect_geom = gpd.GeoSeries([Point(0.5, 0.5), Point(1, 2), Point(2, 1)]) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0], [0, 0]], + "lhs_segment_id": [[0], [0, 1]], + "rhs_linestring_id": [[0], [0, 0]], + "rhs_segment_id": [[0], [0, 3]], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_one_pair_with_overlap(): + s1 = gpd.GeoSeries([LineString([(-1, 0), (0, 0), (0, 1), (-1, 1)])]) + s2 = gpd.GeoSeries([LineString([(1, 0), (0, 0), (0, 1), (1, 1)])]) + + expect_offset = pd.Series([0, 1]) + expect_geom = s1.intersection(s2) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0]], + "lhs_segment_id": [[0]], + "rhs_linestring_id": [[0]], + "rhs_segment_id": [[0]], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_two_pairs_with_intersect_and_overlap(): + s1 = gpd.GeoSeries( + [ + LineString([(-1, 0), (0, 0), (0, 1), (-1, 1)]), + LineString([(-1, -1), (1, 1), (-1, 1)]), + ] + ) + s2 = gpd.GeoSeries( + [ + LineString([(1, 0), (0, 0), (0, 1), (1, 1)]), + LineString([(-1, 1), (1, -1), (-1, -1), (1, 1)]), + ] + ) + + expect_offset = pd.Series([0, 1, 3]) + expect_geom = gpd.GeoSeries( + [ + LineString([(0, 0), (0, 1)]), + Point(-1, 1), + LineString([(-1, -1), (1, 1)]), + ] + ) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0], [0, 0]], + "lhs_segment_id": [[0], [1, 0]], + "rhs_linestring_id": [[0], [0, 0]], + "rhs_segment_id": [[0], [0, 2]], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_one_pair_multilinestring(): + s1 = gpd.GeoSeries( + [MultiLineString([[(0, 0), (1, 1)], [(-1, 1), (0, 0)]])] + ) + s2 = gpd.GeoSeries( + [MultiLineString([[(0, 1), (1, 0)], [(-0.5, 0.5), (0, 0)]])] + ) + + expect_offset = pd.Series([0, 2]) + expect_geom = gpd.GeoSeries( + [ + Point(0.5, 0.5), + LineString([(-0.5, 0.5), (0, 0)]), + ] + ) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0, 1]], + "lhs_segment_id": [[0, 0]], + "rhs_linestring_id": [[0, 1]], + "rhs_segment_id": [[0, 0]], + } + ) + + run_test(s1, s2, expect_offset, expect_geom, expect_ids) + + +def test_three_pairs_identical_has_ring(): + lhs = gpd.GeoSeries( + [ + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + ] + ) + rhs = gpd.GeoSeries( + [ + LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + ] + ) + + expect_offset = pd.Series([0, 2, 4, 6]) + expect_geom = gpd.GeoSeries( + [ + Point(0, 0), + Point(1, 1), + Point(0, 0), + Point(1, 1), + Point(0, 0), + Point(1, 1), + ] + ) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0, 0], [0, 0], [0, 0]], + "lhs_segment_id": [[0, 0], [0, 0], [0, 0]], + "rhs_linestring_id": [[0, 0], [0, 0], [0, 0]], + "rhs_segment_id": [[0, 1], [0, 1], [0, 1]], + } + ) + + run_test(lhs, rhs, expect_offset, expect_geom, expect_ids) + + +def test_three_pairs_identical_no_ring(): + lhs = gpd.GeoSeries( + [ + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + ] + ) + rhs = gpd.GeoSeries( + [ + LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), + LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), + LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), + ] + ) + + expect_offset = pd.Series([0, 2, 4, 6]) + expect_geom = gpd.GeoSeries( + [ + Point(0, 0), + Point(1, 1), + Point(0, 0), + Point(1, 1), + Point(0, 0), + Point(1, 1), + ] + ) + expect_ids = pd.DataFrame( + { + "lhs_linestring_id": [[0, 0], [0, 0], [0, 0]], + "lhs_segment_id": [[0, 0], [0, 0], [0, 0]], + "rhs_linestring_id": [[0, 0], [0, 0], [0, 0]], + "rhs_segment_id": [[0, 1], [0, 1], [0, 1]], + } + ) + + run_test(lhs, rhs, expect_offset, expect_geom, expect_ids) diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py new file mode 100644 index 000000000..a1b432ad1 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py @@ -0,0 +1,66 @@ +import pandas as pd +from pandas.testing import assert_series_equal +from shapely.geometry import LineString, Point, Polygon + +import cuspatial + + +def test_single_true(): + p1 = cuspatial.GeoSeries([Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(0, 0)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([True])) + + +def test_single_false(): + p1 = cuspatial.GeoSeries([Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([False])) + + +def test_true_false(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) + p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([True, False])) + + +def test_false_true(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([False, True])) + + +def test_true_false_true(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) + p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2), Point(2, 2)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([True, False, True])) + + +def test_false_true_false(): + p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0), Point(0, 0)]) + p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0), Point(2, 2)]) + result = p1._basic_intersects(p2) + assert_series_equal(result.to_pandas(), pd.Series([False, True, False])) + + +def test_linestring_polygon_within(): + lhs = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + LineString([(0, 0), (1, 1)]), + ] + ) + rhs = cuspatial.GeoSeries( + [ + Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + ] + ) + result = lhs._basic_intersects(rhs) + assert_series_equal(result.to_pandas(), pd.Series([True, True, True])) From eaee84c909abe844a09350e5a7fd6d4e0a1cd1ea Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 19 Apr 2023 23:03:33 +0000 Subject: [PATCH 063/126] Updating --- .../binpreds/feature_contains_properly.py | 5 +- .../tests/binops/test_intersections.py | 236 ------------------ 2 files changed, 4 insertions(+), 237 deletions(-) delete mode 100644 python/cuspatial/cuspatial/tests/binops/test_intersections.py diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 69dad17d4..25275307c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -118,10 +118,13 @@ def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): """Return the result of the basic predicate without any postprocessing. """ + breakpoint() reindex_pip_result = self._reindex_allpairs(lhs, op_result) if len(reindex_pip_result) == 0: if self.config.mode == "basic_count": return cudf.Series(cp.zeros(len(lhs), dtype="int32")) + elif self.config.mode == "basic_none": + return cudf.Series(cp.repeat(cp.array(True), len(lhs))) else: return _false_series(len(lhs)) # Postprocessing early termination. Basic requests, or allpairs @@ -129,7 +132,7 @@ def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): if self.config.allpairs: return reindex_pip_result elif self.config.mode == "basic_none": - final_result = cudf.Series(cp.repeat([True], len(lhs))) + final_result = cudf.Series(cp.repeat(cp.array(True), len(lhs))) final_result.loc[reindex_pip_result["point_index"]] = False return final_result elif self.config.mode == "basic_any": diff --git a/python/cuspatial/cuspatial/tests/binops/test_intersections.py b/python/cuspatial/cuspatial/tests/binops/test_intersections.py deleted file mode 100644 index a3bcb0cd0..000000000 --- a/python/cuspatial/cuspatial/tests/binops/test_intersections.py +++ /dev/null @@ -1,236 +0,0 @@ -import geopandas as gpd -import pandas as pd -from geopandas.testing import assert_geoseries_equal -from pandas.testing import assert_frame_equal, assert_series_equal -from shapely.geometry import LineString, MultiLineString, Point - -import cuspatial -from cuspatial.core.binops.intersection import pairwise_linestring_intersection - - -def run_test(s1, s2, expect_offset, expect_geom, expect_ids): - offset, geoms, ids = pairwise_linestring_intersection( - cuspatial.from_geopandas(s1), cuspatial.from_geopandas(s2) - ) - - assert_series_equal(expect_offset, offset.to_pandas(), check_dtype=False) - assert_geoseries_equal(expect_geom, geoms.to_geopandas()) - assert_frame_equal(expect_ids, ids.to_pandas()) - - -def test_empty(): - s1 = gpd.GeoSeries([]) - s2 = gpd.GeoSeries([]) - - expect_offset = pd.Series([0]) - expect_geom = gpd.GeoSeries([]) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [], - "lhs_segment_id": [], - "rhs_linestring_id": [], - "rhs_segment_id": [], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_one_pair(): - s1 = gpd.GeoSeries([LineString([(0, 0), (1, 1)])]) - s2 = gpd.GeoSeries([LineString([(0, 1), (1, 0)])]) - - expect_offset = pd.Series([0, 1]) - expect_geom = s1.intersection(s2) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0]], - "lhs_segment_id": [[0]], - "rhs_linestring_id": [[0]], - "rhs_segment_id": [[0]], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_two_pairs(): - s1 = gpd.GeoSeries( - [LineString([(0, 0), (1, 1)]), LineString([(0, 2), (2, 2), (2, 0)])] - ) - s2 = gpd.GeoSeries( - [ - LineString([(0, 1), (1, 0)]), - LineString([(1, 1), (1, 3), (3, 3), (3, 1), (1.5, 1)]), - ] - ) - - expect_offset = pd.Series([0, 1, 3]) - expect_geom = gpd.GeoSeries([Point(0.5, 0.5), Point(1, 2), Point(2, 1)]) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0], [0, 0]], - "lhs_segment_id": [[0], [0, 1]], - "rhs_linestring_id": [[0], [0, 0]], - "rhs_segment_id": [[0], [0, 3]], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_one_pair_with_overlap(): - s1 = gpd.GeoSeries([LineString([(-1, 0), (0, 0), (0, 1), (-1, 1)])]) - s2 = gpd.GeoSeries([LineString([(1, 0), (0, 0), (0, 1), (1, 1)])]) - - expect_offset = pd.Series([0, 1]) - expect_geom = s1.intersection(s2) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0]], - "lhs_segment_id": [[0]], - "rhs_linestring_id": [[0]], - "rhs_segment_id": [[0]], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_two_pairs_with_intersect_and_overlap(): - s1 = gpd.GeoSeries( - [ - LineString([(-1, 0), (0, 0), (0, 1), (-1, 1)]), - LineString([(-1, -1), (1, 1), (-1, 1)]), - ] - ) - s2 = gpd.GeoSeries( - [ - LineString([(1, 0), (0, 0), (0, 1), (1, 1)]), - LineString([(-1, 1), (1, -1), (-1, -1), (1, 1)]), - ] - ) - - expect_offset = pd.Series([0, 1, 3]) - expect_geom = gpd.GeoSeries( - [ - LineString([(0, 0), (0, 1)]), - Point(-1, 1), - LineString([(-1, -1), (1, 1)]), - ] - ) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0], [0, 0]], - "lhs_segment_id": [[0], [1, 0]], - "rhs_linestring_id": [[0], [0, 0]], - "rhs_segment_id": [[0], [0, 2]], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_one_pair_multilinestring(): - s1 = gpd.GeoSeries( - [MultiLineString([[(0, 0), (1, 1)], [(-1, 1), (0, 0)]])] - ) - s2 = gpd.GeoSeries( - [MultiLineString([[(0, 1), (1, 0)], [(-0.5, 0.5), (0, 0)]])] - ) - - expect_offset = pd.Series([0, 2]) - expect_geom = gpd.GeoSeries( - [ - Point(0.5, 0.5), - LineString([(-0.5, 0.5), (0, 0)]), - ] - ) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0, 1]], - "lhs_segment_id": [[0, 0]], - "rhs_linestring_id": [[0, 1]], - "rhs_segment_id": [[0, 0]], - } - ) - - run_test(s1, s2, expect_offset, expect_geom, expect_ids) - - -def test_three_pairs_identical_has_ring(): - lhs = gpd.GeoSeries( - [ - LineString([(0, 0), (1, 1)]), - LineString([(0, 0), (1, 1)]), - LineString([(0, 0), (1, 1)]), - ] - ) - rhs = gpd.GeoSeries( - [ - LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), - LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), - LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), - ] - ) - - expect_offset = pd.Series([0, 2, 4, 6]) - expect_geom = gpd.GeoSeries( - [ - Point(0, 0), - Point(1, 1), - Point(0, 0), - Point(1, 1), - Point(0, 0), - Point(1, 1), - ] - ) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0, 0], [0, 0], [0, 0]], - "lhs_segment_id": [[0, 0], [0, 0], [0, 0]], - "rhs_linestring_id": [[0, 0], [0, 0], [0, 0]], - "rhs_segment_id": [[0, 1], [0, 1], [0, 1]], - } - ) - - run_test(lhs, rhs, expect_offset, expect_geom, expect_ids) - - -def test_three_pairs_identical_no_ring(): - lhs = gpd.GeoSeries( - [ - LineString([(0, 0), (1, 1)]), - LineString([(0, 0), (1, 1)]), - LineString([(0, 0), (1, 1)]), - ] - ) - rhs = gpd.GeoSeries( - [ - LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), - LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), - LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), - ] - ) - - expect_offset = pd.Series([0, 2, 4, 6]) - expect_geom = gpd.GeoSeries( - [ - Point(0, 0), - Point(1, 1), - Point(0, 0), - Point(1, 1), - Point(0, 0), - Point(1, 1), - ] - ) - expect_ids = pd.DataFrame( - { - "lhs_linestring_id": [[0, 0], [0, 0], [0, 0]], - "lhs_segment_id": [[0, 0], [0, 0], [0, 0]], - "rhs_linestring_id": [[0, 0], [0, 0], [0, 0]], - "rhs_segment_id": [[0, 1], [0, 1], [0, 1]], - } - ) - - run_test(lhs, rhs, expect_offset, expect_geom, expect_ids) From fc1ca250d4ae5c792796c1825d65615c88ffe630 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Apr 2023 14:46:54 +0000 Subject: [PATCH 064/126] Fix a bad test. --- python/cuspatial/cuspatial/core/binpreds/feature_within.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index def457200..056c1f9d6 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -2,7 +2,6 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, - ImpossiblePredicate, NotImplementedPredicate, ) from cuspatial.core.binpreds.complex_geometry_predicate import ( @@ -85,7 +84,7 @@ def _preprocess(self, lhs, rhs): DispatchDict = { - (Point, Point): ImpossiblePredicate, + (Point, Point): WithinPredicateBase, (Point, MultiPoint): WithinIntersectsPredicate, (Point, LineString): PointLineStringWithin, (Point, Polygon): PointPolygonWithin, From 6a931bb07002cf0a067e59d62fcea0a5946168f7 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Apr 2023 15:51:42 +0000 Subject: [PATCH 065/126] Pass a couple of tests. --- python/cuspatial/cuspatial/core/binpreds/feature_within.py | 6 ++---- python/cuspatial/cuspatial/core/geoseries.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 056c1f9d6..370d9c40d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -49,7 +49,7 @@ def _preprocess(self, lhs, rhs): class PointPolygonWithin(ContainsPredicateBase): def _preprocess(self, lhs, rhs): - return rhs._basic_contains_any(lhs) + return rhs.contains_properly(lhs) class LineStringLineStringWithin(IntersectsPredicateBase): @@ -78,9 +78,7 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - contains_all = rhs._basic_contains_all(lhs) - intersects = rhs._basic_intersects(lhs) - return contains_all & intersects + return rhs.contains_properly(rhs) DispatchDict = { diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index f1f9af789..25ca2978a 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1483,6 +1483,7 @@ def _basic_contains_any(self, other): rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_any") intersects = lhs._basic_intersects(other) + breakpoint() return contains | intersects def _basic_contains_all(self, other): From ae39c885770b82bf59067273814f52bec1d17c67 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Apr 2023 17:59:47 +0000 Subject: [PATCH 066/126] Back to 26 failed. Working on within, touches, and crosses only. --- .../core/binpreds/feature_contains.py | 2 +- .../binpreds/feature_contains_properly.py | 2 +- .../cuspatial/core/binpreds/feature_covers.py | 5 ++- .../core/binpreds/feature_crosses.py | 11 ++---- .../core/binpreds/feature_touches.py | 36 +++++++++---------- .../cuspatial/core/binpreds/feature_within.py | 30 ++++------------ python/cuspatial/cuspatial/core/geoseries.py | 3 +- .../binpreds/test_binpred_test_dispatch.py | 2 +- 8 files changed, 32 insertions(+), 59 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 13e52bf92..96b284ea9 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -37,7 +37,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): drop=True ) # TODO: Need better point counting in intersection. - return contains + intersects >= rhs.sizes + return contains + intersects // 2 >= rhs.sizes class ContainsPredicate(ContainsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index b6e13c683..82bbdfddf 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -136,7 +136,7 @@ def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): return final_result elif self.config.mode == "basic_any": final_result = _false_series(len(lhs)) - final_result.loc[reindex_pip_result["point_index"]] = True + final_result.loc[reindex_pip_result["polygon_index"]] = True return final_result elif self.config.mode == "basic_all": sizes = ( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 7d673e270..a4b640123 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -47,9 +47,8 @@ def _preprocess(self, lhs, rhs): class PolygonPolygonCovers(ContainsPredicateBase): def _preprocess(self, lhs, rhs): - contains_none = rhs._basic_contains_none(lhs) - equals = rhs._basic_equals(lhs) - return contains_none | equals + contains = lhs.contains(rhs) + return contains DispatchDict = { diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 51b82fa15..e3a4c8b55 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -38,10 +38,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class PolygonLineStringCrosses(CrossesByIntersectionPredicate): def _compute_predicate(self, lhs, rhs, preprocessor_result): intersects_through = lhs._basic_intersects_through(rhs) - # intersects_any = lhs._basic_intersects(rhs) - # intersects_points = lhs._basic_intersects_points(rhs) equals = rhs._basic_equals(lhs) - # contains_any = lhs._basic_contains_any(rhs) contains_all = lhs._basic_contains_all(rhs) return ~contains_all & ~equals & intersects_through @@ -49,7 +46,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonCrosses(PolygonLineStringCrosses): def _preprocess(self, lhs, rhs): """Note the order of arguments is reversed.""" - return super()._preprocess(rhs, lhs) + return ~super()._preprocess(rhs, lhs) class PointPointCrosses(CrossesPredicateBase): @@ -58,10 +55,6 @@ def _preprocess(self, lhs, rhs): return _false_series(len(lhs)) -class PolygonPolygonCrosses(PolygonLineStringCrosses): - pass - - DispatchDict = { (Point, Point): PointPointCrosses, (Point, MultiPoint): ImpossiblePredicate, @@ -78,5 +71,5 @@ class PolygonPolygonCrosses(PolygonLineStringCrosses): (Polygon, Point): CrossesPredicateBase, (Polygon, MultiPoint): CrossesPredicateBase, (Polygon, LineString): PolygonLineStringCrosses, - (Polygon, Polygon): PolygonPolygonCrosses, + (Polygon, Polygon): ImpossiblePredicate, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index aa81db33b..41d0d9418 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -11,8 +11,6 @@ MultiPoint, Point, Polygon, - _linestring_to_boundary, - _polygon_to_boundary, ) @@ -59,31 +57,33 @@ class LineStringLineStringTouches(BinPred): def _preprocess(self, lhs, rhs): """A and B have at least one point in common, and the common points lie in at least one boundary""" - lhs_boundary = _linestring_to_boundary(lhs) - rhs_boundary = _linestring_to_boundary(rhs) - point_intersections = lhs._basic_intersects_at_point_only(rhs) - boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) - equals = lhs._basic_equals_all(rhs) - return point_intersections & boundary_intersects & ~equals + # Point is equal + equals = lhs._basic_equals(rhs) + # Linestrings are not equal + equals_all = lhs._basic_equals_all(rhs) + # Linestrings do not cross + crosses = ~lhs.crosses(rhs) + return equals & crosses & ~equals_all class LineStringPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - lhs_boundary = _linestring_to_boundary(lhs) - rhs_boundary = _polygon_to_boundary(rhs) - boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) - interior_contains_any = rhs._basic_contains_any(lhs) - return boundary_intersects & ~interior_contains_any + # Intersection occurs + intersects = lhs.intersects(rhs) + # No points in the lhs are in the rhs + contains = rhs.contains_properly(lhs) + return intersects & ~contains class PolygonPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - lhs_boundary = _polygon_to_boundary(lhs) - rhs_boundary = _polygon_to_boundary(rhs) - boundary_intersects = lhs_boundary._basic_intersects(rhs_boundary) + # Intersection occurs + intersects = lhs.intersects(rhs) + # No points in the lhs are in the rhs contains = rhs._basic_contains_any(lhs) - # Count results here? - return boundary_intersects & ~contains + # Not equal + equals_all = lhs._basic_equals_all(rhs) + return intersects & ~contains & ~equals_all DispatchDict = { diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 370d9c40d..c136bf237 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -4,13 +4,7 @@ BinPred, NotImplementedPredicate, ) -from cuspatial.core.binpreds.complex_geometry_predicate import ( - ComplexGeometryPredicate, -) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase -from cuspatial.core.binpreds.feature_contains_properly import ( - ContainsProperlyPredicate, -) from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -59,26 +53,14 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): return intersects & equals -class ComplexPolygonWithin( - ContainsProperlyPredicate, ComplexGeometryPredicate -): - """Implements within for complex polygons. Depends on contains result - for the types. - - Used by: - (MultiPoint, Polygon) - (LineString, Polygon) - (Polygon, Polygon) - """ - +class LineStringPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - # Note the order of arguments is reversed. - return super()._preprocess(rhs, lhs) + return rhs.contains(rhs) -class LineStringPolygonWithin(BinPred): +class PolygonPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - return rhs.contains_properly(rhs) + return rhs.contains(lhs) DispatchDict = { @@ -89,7 +71,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): WithinIntersectsPredicate, - (MultiPoint, Polygon): ComplexPolygonWithin, + (MultiPoint, Polygon): PolygonPolygonWithin, (LineString, Point): WithinIntersectsPredicate, (LineString, MultiPoint): WithinIntersectsPredicate, (LineString, LineString): LineStringLineStringWithin, @@ -97,5 +79,5 @@ def _preprocess(self, lhs, rhs): (Polygon, Point): WithinPredicateBase, (Polygon, MultiPoint): WithinPredicateBase, (Polygon, LineString): WithinPredicateBase, - (Polygon, Polygon): ComplexPolygonWithin, + (Polygon, Polygon): PolygonPolygonWithin, } diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 25ca2978a..a91e881dc 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1465,7 +1465,7 @@ def _basic_contains_count(self, other): rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_count") intersects = lhs._basic_intersects_count(other) - return contains + intersects + return contains + intersects // 2 def _basic_contains_none(self, other): """Utility method that returns True if none of the points in the lhs @@ -1483,7 +1483,6 @@ def _basic_contains_any(self, other): rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_any") intersects = lhs._basic_intersects(other) - breakpoint() return contains | intersects def _basic_contains_all(self, other): diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index e176b9240..d0214257c 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -25,7 +25,7 @@ def wrapper(*args, **kwargs): out_file = open("test_binpred_test_dispatch.log", "w") -@xfail_on_exception # TODO: Remove when all tests are passing +# @xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 From a48dbbf6bc69d2ed74e201e90dc9f6258b06ed81 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 20 Apr 2023 21:44:46 +0000 Subject: [PATCH 067/126] Lots of changes trying to get contains working as expected. --- .../binpreds/complex_geometry_predicate.py | 29 +++++- .../core/binpreds/feature_contains.py | 15 +-- .../binpreds/feature_contains_properly.py | 49 +--------- .../core/binpreds/feature_crosses.py | 9 +- .../core/binpreds/feature_touches.py | 9 +- python/cuspatial/cuspatial/core/geoseries.py | 9 +- .../test_contains_basic_predicate.py | 93 ++++++++++++++++--- .../tests/binpreds/binpred_test_dispatch.py | 5 +- .../binpreds/test_binpred_test_dispatch.py | 13 +++ .../cuspatial/utils/binpred_utils.py | 6 ++ 10 files changed, 155 insertions(+), 82 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index 171251f63..5a9bb409b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -16,6 +16,7 @@ from cuspatial.utils.binpred_utils import ( _count_results_in_multipoint_geometries, _false_series, + _true_series, ) from cuspatial.utils.column_utils import ( contains_only_linestrings, @@ -154,7 +155,9 @@ def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: return allpairs_result - def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): + def _postprocess_multi( + self, lhs, rhs, preprocessor_result, op_result, mode + ): """Reconstruct the original geometry from the result of the contains_properly call. @@ -190,6 +193,30 @@ def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): result_df = hits.reset_index().merge( expected_count.reset_index(), on="rhs_index" ) + + # Handling for the basic predicates + if mode == "basic_none": + none_result = _true_series(len(rhs)) + if len(result_df) == 0: + return none_result + none_result.loc[result_df["point_index_x"] > 0] = False + return none_result + elif mode == "basic_any": + any_result = _false_series(len(rhs)) + if len(result_df) == 0: + return any_result + any_result.loc[result_df["point_index_x"] > 0] = True + return any_result + elif mode == "basic_count": + count_result = cudf.Series(cp.zeros(len(rhs)), dtype="int32") + if len(result_df) == 0: + return count_result + count_result.loc[result_df["rhs_index"]] = result_df[ + "point_index_x" + ] + return count_result + + # Handling for full contains (equivalent to basic predicate all) result_df["feature_in_polygon"] = ( result_df["point_index_x"] >= result_df["point_index_y"] ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 96b284ea9..a1a9a867a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -14,8 +14,8 @@ MultiPoint, Point, Polygon, - _multipoints_from_geometry, ) +from cuspatial.utils.column_utils import contains_only_points GeoSeries = TypeVar("GeoSeries") @@ -32,12 +32,15 @@ def _preprocess(self, lhs, rhs): def _compute_predicate(self, lhs, rhs, preprocessor_result): contains = lhs._basic_contains_count(rhs).reset_index(drop=True) - rhs_points = _multipoints_from_geometry(rhs) - intersects = lhs._basic_intersects_count(rhs_points).reset_index( - drop=True - ) + # Special case in GeoPandas, points are not contained + # in the boundary of a polygon. + if contains_only_points(rhs): + breakpoint() + return contains > 0 + intersects = lhs._basic_intersects_count(rhs).reset_index(drop=True) # TODO: Need better point counting in intersection. - return contains + intersects // 2 >= rhs.sizes + breakpoint() + return contains + intersects >= rhs.sizes class ContainsPredicate(ContainsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 82bbdfddf..519bcb8cd 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -2,10 +2,6 @@ from typing import TypeVar -import cupy as cp - -import cudf - from cuspatial.core.binpreds.binpred_interface import ( BinPred, ContainsOpResult, @@ -23,7 +19,6 @@ MultiPoint, Point, Polygon, - _false_series, _is_complex, ) from cuspatial.utils.column_utils import ( @@ -114,44 +109,6 @@ def _compute_predicate( op_result = ContainsOpResult(pip_result, preprocessor_result) return self._postprocess(lhs, rhs, preprocessor_result, op_result) - def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): - """Return the result of the basic predicate without any - postprocessing. - """ - reindex_pip_result = self._reindex_allpairs(lhs, op_result) - if len(reindex_pip_result) == 0: - if self.config.mode == "basic_count": - return cudf.Series(cp.zeros(len(lhs), dtype="int32")) - elif self.config.mode == "basic_none": - return cudf.Series(cp.repeat(cp.array(True), len(lhs))) - else: - return _false_series(len(lhs)) - # Postprocessing early termination. Basic requests, or allpairs - # requests do not do object reconstruction. - if self.config.allpairs: - return reindex_pip_result - elif self.config.mode == "basic_none": - final_result = cudf.Series(cp.repeat(cp.array(True), len(lhs))) - final_result.loc[reindex_pip_result["point_index"]] = False - return final_result - elif self.config.mode == "basic_any": - final_result = _false_series(len(lhs)) - final_result.loc[reindex_pip_result["polygon_index"]] = True - return final_result - elif self.config.mode == "basic_all": - sizes = ( - preprocessor_result.point_indices[1:] - - preprocessor_result.point_indices[:-1] - ) - result_sizes = reindex_pip_result["polygon_index"].value_counts() - final_result = _false_series( - len(preprocessor_result.point_indices) - ) - final_result.loc[sizes == result_sizes] = True - return final_result - elif self.config.mode == "basic_count": - return reindex_pip_result["polygon_index"].value_counts() - def _postprocess(self, lhs, rhs, preprocessor_result, op_result): """Postprocess the output GeoSeries to ensure that they are of the correct type for the predicate. @@ -185,16 +142,12 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): point index and the polygon index for each point in the polygon. """ - if self.config.mode != "full" or self.config.allpairs: - return self._return_unprocessed_result( - lhs, op_result, preprocessor_result - ) # for each input pair i: result[i] =  true iff point[i] is # contained in at least one polygon of multipolygon[i]. if _is_complex(rhs): return super()._postprocess_multi( - lhs, rhs, preprocessor_result, op_result + lhs, rhs, preprocessor_result, op_result, mode=self.config.mode ) else: return super()._postprocess_points( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index e3a4c8b55..f11132c66 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -45,8 +45,13 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonCrosses(PolygonLineStringCrosses): def _preprocess(self, lhs, rhs): - """Note the order of arguments is reversed.""" - return ~super()._preprocess(rhs, lhs) + contains = rhs.contains(lhs) + contains_properly = rhs.contains_properly(lhs) + intersects = lhs._basic_intersects_through(rhs) + breakpoint() + return (~contains & contains_properly) | ( + ~contains & ~contains_properly & intersects + ) class PointPointCrosses(CrossesPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 41d0d9418..e4364989a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -70,9 +70,12 @@ class LineStringPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): # Intersection occurs intersects = lhs.intersects(rhs) - # No points in the lhs are in the rhs - contains = rhs.contains_properly(lhs) - return intersects & ~contains + # The linestring is contained but is not + # contained properly, it crosses + # This is the equivalent of crosses + contains = rhs.contains(lhs) + contains_properly = rhs.contains_properly(lhs) + return intersects | (~contains & contains_properly) class PolygonPolygonTouches(BinPred): diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index a91e881dc..341749162 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -190,7 +190,7 @@ def sizes(self): full_sizes = self.polygons.ring_offset.take( self.polygons.part_offset.take(self.polygons.geometry_offset) ) - return full_sizes[1:] - full_sizes[:-1] - 1 + return full_sizes[1:] - full_sizes[:-1] elif contains_only_linestrings(self): # Not supporting multilinestring yet full_sizes = self.lines.part_offset.take( @@ -1401,8 +1401,8 @@ def _basic_equals_all(self, other): rhs = _multipoints_from_geometry(other) result = pairwise_multipoint_equals_count(lhs, rhs) sizes = ( - rhs.multipoints.geometry_offset[1:] - - rhs.multipoints.geometry_offset[:-1] + lhs.multipoints.geometry_offset[1:] + - lhs.multipoints.geometry_offset[:-1] ) return result == sizes @@ -1464,8 +1464,7 @@ def _basic_contains_count(self, other): lhs = self rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_count") - intersects = lhs._basic_intersects_count(other) - return contains + intersects // 2 + return contains def _basic_contains_none(self, other): """Utility method that returns True if none of the points in the lhs diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py index f6b0a4087..0ad6757fd 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py @@ -1,5 +1,6 @@ -import pandas as pd -from shapely.geometry import Point, Polygon +# Copyright (c) 2023, NVIDIA CORPORATION. + +from shapely.geometry import LineString, Point, Polygon import cuspatial @@ -7,30 +8,94 @@ def test_basic_contains_none_outside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(2, 2)]) - pd.testing.assert_series_equal( - lhs._basic_contains_none(rhs).to_pandas(), pd.Series([True]) - ) + got = lhs._basic_contains_none(rhs).to_pandas() + expected = [True] + assert (got == expected).all() def test_basic_contains_none_inside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0.5, 0.5)]) - pd.testing.assert_series_equal( - lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) - ) + got = lhs._basic_contains_none(rhs).to_pandas() + expected = [False] + assert (got == expected).all() def test_basic_contains_none_point(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0)]) - pd.testing.assert_series_equal( - lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) - ) + got = lhs._basic_contains_none(rhs).to_pandas() + expected = [False] + assert (got == expected).all() def test_basic_contains_none_edge(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0.5)]) - pd.testing.assert_series_equal( - lhs._basic_contains_none(rhs).to_pandas(), pd.Series([False]) - ) + got = lhs._basic_contains_none(rhs).to_pandas() + expected = [False] + assert (got == expected).all() + + +def test_basic_contains_any_outside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(2, 2)]) + got = lhs._basic_contains_any(rhs).to_pandas() + expected = [False] + assert (got == expected).all() + + +def test_basic_contains_any_inside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) + got = lhs._basic_contains_any(rhs).to_pandas() + expected = [True] + assert (got == expected).all() + + +def test_basic_contains_any_point(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0)]) + got = lhs._basic_contains_any(rhs).to_pandas() + expected = [True] + assert (got == expected).all() + + +def test_basic_contains_any_edge(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0.5)]) + got = lhs._basic_contains_any(rhs).to_pandas() + expected = [True] + assert (got == expected).all() + + +def test_basic_contains_count_outside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(2, 2)]) + got = lhs._basic_contains_count(rhs).to_pandas() + expected = [0] + assert (got == expected).all() + + +def test_basic_contains_count_inside(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) + got = lhs._basic_contains_count(rhs).to_pandas() + expected = [1] + assert (got == expected).all() + + +def test_basic_contains_count_point(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0)]) + got = lhs._basic_contains_count(rhs).to_pandas() + expected = [0] + assert (got == expected).all() + + +def test_basic_contains_count_edge(): + lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + rhs = cuspatial.GeoSeries([Point(0, 0.5)]) + got = lhs._basic_contains_count(rhs).to_pandas() + expected = [0] + assert (got == expected).all() diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index ed8ca4236..550b061b4 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -362,11 +362,10 @@ def predicate(request): """ x---x | / - | / - --|/- + --|-/ + | |/| | x | | | - | | ----- """, Polygon([(0.5, 0.5), (0.5, 1.5), (1.5, 1.5)]), diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index d0214257c..53ab96875 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -6,6 +6,8 @@ import pytest from binpred_test_dispatch import predicate, simple_test # noqa: F401 +from cuspatial.utils.column_utils import contains_only_polygons + """Decorator function that xfails a test if an exception is throw by the test function. Will be removed when all tests are passing.""" @@ -75,6 +77,17 @@ def test_simple_features( (lhs, rhs) = simple_test[2], simple_test[3] gpdlhs = lhs.to_geopandas() gpdrhs = rhs.to_geopandas() + + # Reverse + if predicate == "contains" and not contains_only_polygons(rhs): + return + pred_fn = getattr(rhs, predicate) + got = pred_fn(lhs) + gpd_pred_fn = getattr(gpdrhs, predicate) + expected = gpd_pred_fn(gpdlhs) + assert (got.values_host == expected.values).all() + + # Forward pred_fn = getattr(lhs, predicate) got = pred_fn(rhs) gpd_pred_fn = getattr(gpdlhs, predicate) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 32de863a8..350645385 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -22,6 +22,11 @@ def _false_series(size): return cudf.Series(cp.zeros(size, dtype=cp.bool_)) +def _true_series(size): + """Return a Series of True values""" + return cudf.Series(cp.ones(size, dtype=cp.bool_)) + + def _count_results_in_multipoint_geometries(point_indices, point_result): """Count the number of points in each multipoint geometry. @@ -161,6 +166,7 @@ def _multipoints_from_polygons(geoseries): polygon_offsets = geoseries.polygons.ring_offset.take( geoseries.polygons.part_offset.take(geoseries.polygons.geometry_offset) ) + # Drop the endpoint from all polygons return cuspatial.GeoSeries.from_multipoints_xy(xy, polygon_offsets) From c0dd549941433524e1fee585676e83900004392e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Apr 2023 18:13:28 +0000 Subject: [PATCH 068/126] Create arduous points_and_lines_to_multipoints function. --- .../binpreds/complex_geometry_predicate.py | 7 +- .../core/binpreds/feature_contains.py | 114 ++++++++++++- python/cuspatial/cuspatial/core/geoseries.py | 8 +- .../tests/binpreds/test_binpred_internals.py | 158 +++++++++++++++++- .../cuspatial/utils/binpred_utils.py | 65 ++++++- 5 files changed, 334 insertions(+), 18 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index 5a9bb409b..e506ae6e3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -211,9 +211,10 @@ def _postprocess_multi( count_result = cudf.Series(cp.zeros(len(rhs)), dtype="int32") if len(result_df) == 0: return count_result - count_result.loc[result_df["rhs_index"]] = result_df[ - "point_index_x" - ] + hits = result_df["point_index_x"] + breakpoint() + hits.index = count_result.iloc[result_df["rhs_index"]].index + count_result.iloc[result_df["rhs_index"]] = hits return count_result # Handling for full contains (equivalent to basic predicate all) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index a1a9a867a..15ed2ad3a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -2,6 +2,11 @@ from typing import TypeVar +import cupy as cp +import numpy as np + +import cudf + from cuspatial.core.binpreds.binpred_interface import ( ImpossiblePredicate, NotImplementedPredicate, @@ -14,8 +19,15 @@ MultiPoint, Point, Polygon, + _open_polygon_rings, + _points_and_lines_to_multipoints, + _zero_series, +) +from cuspatial.utils.column_utils import ( + contains_only_linestrings, + contains_only_points, + contains_only_polygons, ) -from cuspatial.utils.column_utils import contains_only_points GeoSeries = TypeVar("GeoSeries") @@ -30,18 +42,102 @@ def _preprocess(self, lhs, rhs): preprocessor_result = super()._preprocess_multi(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) - def _compute_predicate(self, lhs, rhs, preprocessor_result): - contains = lhs._basic_contains_count(rhs).reset_index(drop=True) - # Special case in GeoPandas, points are not contained - # in the boundary of a polygon. - if contains_only_points(rhs): + def _intersection_results_for_contains(self, lhs, rhs): + pli = lhs._basic_intersects_pli(rhs) + pli_features = pli[1] + pli_offsets = cudf.Series(pli[0]) + # Which feature goes with which offset? + pli_sizes = pli_offsets[1:].reset_index(drop=True) - pli_offsets[ + :-1 + ].reset_index(drop=True) + # Have to use host to create the offsets mapping + pli_mapping = cp.array( + np.arange(len(lhs)).repeat(pli_sizes.values_host) + ) + + # This mapping identifies which intersect feature belongs to which + # intersection. + + points_mask = pli_features.type == "Point" + lines_mask = pli_features.type == "Linestring" + + points = pli_features[points_mask] + lines = pli_features[lines_mask] + + final_intersection_count = _zero_series(len(lhs)) + from cuspatial.core.geoseries import GeoSeries + + # Write a new method, _points_and_lines_to_multipoints that condenses + # The result into a single multipoint that can be worked with. + multipoints = _points_and_lines_to_multipoints(pli_features) + + if len(lines) > 0: + # This is wrong. If a linestring is in a single intersection, + # it will tile out to all of the features. It needs to be + # compared only against the matching feature. pli_mapping + # determines which features match which intersections. + + multipoints = GeoSeries.from_multipoints_xy( + lines.lines.xy, pli_offsets * 2 + ) + lines_intersect_equals_count = multipoints._basic_equals_count(rhs) + final_intersection_count.iloc[ + pli_mapping[lines_mask] + ] = lines_intersect_equals_count[pli_mapping[lines_mask]] breakpoint() - return contains > 0 - intersects = lhs._basic_intersects_count(rhs).reset_index(drop=True) - # TODO: Need better point counting in intersection. + if len(points) > 0: + # Each point falls on the edge of the polygon and is in the + # boundary. + multipoints = GeoSeries.from_multipoints_xy( + points.points.xy.tile(len(lhs)), + cp.arange(len(lhs) + 1) * len(points), + ) + points_intersect_equals_count = multipoints._basic_equals_count( + rhs + ) // len(lhs) + final_intersection_count.iloc[ + pli_mapping[points_mask] + ] = points_intersect_equals_count[pli_mapping[points_mask]] + breakpoint() + # TODO Have to use .iloc here because of a bug in cudf + return final_intersection_count + + def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): + lines_rhs = _open_polygon_rings(rhs) + contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) + intersects = self._intersection_results_for_contains(lhs, lines_rhs) + polygon_size_reduction = 1 breakpoint() + return contains + intersects >= rhs.sizes - polygon_size_reduction + + def _compute_polygon_linestring_contains( + self, lhs, rhs, preprocessor_result + ): + contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + if (contains == 0).all(): + # If a linestring only intersects with the boundary of a polygon, + # it is not contained. + return rhs.sizes == 2 + intersects = self._intersection_results_for_contains(lhs, rhs) return contains + intersects >= rhs.sizes + def _compute_predicate(self, lhs, rhs, preprocessor_result): + if contains_only_points(rhs): + # Special case in GeoPandas, points are not contained + # in the boundary of a polygon. + contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + return contains > 0 + elif contains_only_linestrings(rhs): + return self._compute_polygon_linestring_contains( + lhs, rhs, preprocessor_result + ) + elif contains_only_polygons(rhs): + return self._compute_polygon_polygon_contains( + lhs, rhs, preprocessor_result + ) + else: + raise NotImplementedError("Invalid rhs for contains operation") + class ContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 341749162..9ed396348 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -190,20 +190,20 @@ def sizes(self): full_sizes = self.polygons.ring_offset.take( self.polygons.part_offset.take(self.polygons.geometry_offset) ) - return full_sizes[1:] - full_sizes[:-1] + return cudf.Series(full_sizes[1:] - full_sizes[:-1]) elif contains_only_linestrings(self): # Not supporting multilinestring yet full_sizes = self.lines.part_offset.take( self.lines.geometry_offset ) - return full_sizes[1:] - full_sizes[:-1] + return cudf.Series(full_sizes[1:] - full_sizes[:-1]) elif contains_only_multipoints(self): return ( self.multipoints.geometry_offset[1:] - self.multipoints.geometry_offset[:-1] ) elif contains_only_points(self): - return cp.repeat(cp.array(1), len(self)) + return cudf.Series(cp.repeat(cp.array(1), len(self))) else: if len(self) == 0: return cudf.Series([0], dtype="int32") @@ -370,7 +370,7 @@ def __getitem__(self, item): "map": self._sr.index, "idx": cp.arange(len(self._sr.index)) if not isinstance(item, Integral) - else 0, + else item, } ) index_df = cudf.DataFrame({"map": item}).reset_index() diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py index 7d18530ac..a1b9fe257 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py @@ -1,10 +1,14 @@ # Copyright (c) 2020-2023, NVIDIA CORPORATION import pandas as pd -from shapely.geometry import LineString +from shapely.geometry import LineString, MultiPoint, Point, Polygon import cuspatial from cuspatial.core.binpreds.binpred_dispatch import EQUALS_DISPATCH +from cuspatial.utils.binpred_utils import ( + _open_polygon_rings, + _points_and_lines_to_multipoints, +) def test_internal_reversed_linestrings(): @@ -74,3 +78,155 @@ def test_internal_reversed_linestrings_triple(): ).to_pandas() expected = linestring2.lines.xy.to_pandas() pd.testing.assert_series_equal(got, expected) + + +def test_open_polygon_rings(): + polygon = cuspatial.GeoSeries( + [ + Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), + ] + ) + linestring = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1), (1, 0)]), + ] + ) + got = _open_polygon_rings(polygon) + assert (got.lines.xy == linestring.lines.xy).all() + + +def test_open_polygon_rings_two(): + polygon = cuspatial.GeoSeries( + [ + Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), + Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), + ] + ) + linestring = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1), (1, 0)]), + LineString([(0, 0), (1, 1), (1, 0)]), + ] + ) + got = _open_polygon_rings(polygon) + assert (got.lines.xy == linestring.lines.xy).all() + + +def test_open_polygon_rings_three_varying_length(): + polygon = cuspatial.GeoSeries( + [ + Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), + Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), + ] + ) + linestring = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1), (0, 1)]), + LineString([(0, 0), (0, 1), (1, 1), (1, 0)]), + LineString([(0, 0), (1, 1), (1, 0)]), + ] + ) + got = _open_polygon_rings(polygon) + assert (got.lines.xy == linestring.lines.xy).all() + + +def test_points_and_lines_to_multipoints(): + mixed = cuspatial.GeoSeries( + [ + Point(0, 0), + LineString([(1, 1), (2, 2)]), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0)]), + MultiPoint([(1, 1), (2, 2)]), + ] + ) + got = _points_and_lines_to_multipoints(mixed) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_reverse(): + mixed = cuspatial.GeoSeries( + [ + LineString([(1, 1), (2, 2)]), + Point(0, 0), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(1, 1), (2, 2)]), + MultiPoint([(0, 0)]), + ] + ) + got = _points_and_lines_to_multipoints(mixed) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_two_points_one_linestring(): + mixed = cuspatial.GeoSeries( + [ + Point(0, 0), + LineString([(1, 1), (2, 2)]), + Point(3, 3), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0)]), + MultiPoint([(1, 1), (2, 2)]), + MultiPoint([(3, 3)]), + ] + ) + got = _points_and_lines_to_multipoints(mixed) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_two_linestrings_one_point(): + mixed = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1)]), + Point(2, 2), + LineString([(3, 3), (4, 4)]), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0), (1, 1)]), + MultiPoint([(2, 2)]), + MultiPoint([(3, 3), (4, 4)]), + ] + ) + got = _points_and_lines_to_multipoints(mixed) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_complex(): + mixed = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1), (2, 2), (3, 3)]), + Point(4, 4), + LineString([(5, 5), (6, 6)]), + Point(7, 7), + Point(8, 8), + LineString([(9, 9), (10, 10), (11, 11)]), + LineString([(12, 12), (13, 13)]), + Point(14, 14), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]), + MultiPoint([(4, 4)]), + MultiPoint([(5, 5), (6, 6)]), + MultiPoint([(7, 7)]), + MultiPoint([(8, 8)]), + MultiPoint([(9, 9), (10, 10), (11, 11)]), + MultiPoint([(12, 12), (13, 13)]), + MultiPoint([(14, 14)]), + ] + ) + got = _points_and_lines_to_multipoints(mixed) + assert (got.multipoints.xy == expected.multipoints.xy).all() diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 350645385..d9757f5f4 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, NVIDIA CORPORATION. import cupy as cp +import numpy as np import cudf @@ -27,6 +28,16 @@ def _true_series(size): return cudf.Series(cp.ones(size, dtype=cp.bool_)) +def _zero_series(size): + """Return a Series of zeros""" + return cudf.Series(cp.zeros(size, dtype=cp.int32)) + + +def _one_series(size): + """Return a Series of ones""" + return cudf.Series(cp.ones(size, dtype=cp.int32)) + + def _count_results_in_multipoint_geometries(point_indices, point_result): """Count the number of points in each multipoint geometry. @@ -134,7 +145,7 @@ def _linestrings_from_geometry(geoseries): elif geoseries.column_type == ColumnType.LINESTRING: return geoseries elif geoseries.column_type == ColumnType.POLYGON: - return _linestrings_from_polygons(geoseries) + return _open_polygon_rings(geoseries) else: raise NotImplementedError( "Cannot convert type {} to linestrings".format(geoseries.type) @@ -267,3 +278,55 @@ def _is_complex(geoseries): if len(geoseries.multipoints.xy) > 0: return True return False + + +def _open_polygon_rings(geoseries): + """Converts a geoseries of polygons into a geoseries of linestrings + by opening the rings of each polygon.""" + x = geoseries.polygons.x + y = geoseries.polygons.y + parts = geoseries.polygons.part_offset.take( + geoseries.polygons.geometry_offset + ) + rings_mask = geoseries.polygons.ring_offset - 1 + rings_mask[0] = 0 + mask = _true_series(len(x)) + mask[rings_mask[1:]] = False + x = x[mask] + y = y[mask] + xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() + rings = geoseries.polygons.ring_offset - cp.arange(len(rings_mask)) + return cuspatial.GeoSeries.from_linestrings_xy( + xy, + rings, + parts, + ) + + +def _points_and_lines_to_multipoints(geoseries): + """Converts a geoseries of points and lines into a geoseries of + multipoints.""" + points_mask = geoseries.type == "Point" + lines_mask = geoseries.type == "Linestring" + if (points_mask + lines_mask).sum() != len(geoseries): + raise ValueError("Geoseries must contain only points and lines") + points = geoseries[points_mask] + lines = geoseries[lines_mask] + offsets = _zero_series(len(geoseries)) + offsets[points_mask] = 1 + lines_series = geoseries[lines_mask] + lines_sizes = lines_series.sizes + lines_sizes.index = offsets[lines_mask].index + offsets[lines_mask] = lines_series.sizes.values + xy = _zero_series(len(points.points.xy) + len(lines.lines.xy)) + sizes = _zero_series(len(geoseries)) + sizes[points_mask] = 2 + sizes[lines_mask] = lines.sizes.values * 2 + # TODO Inevitable host device copy + points_xy_mask = cp.array(np.repeat(points_mask, sizes.values_host)) + xy.iloc[points_xy_mask] = points.points.xy.reset_index(drop=True) + xy.iloc[~points_xy_mask] = lines.lines.xy.reset_index(drop=True) + breakpoint() + return cuspatial.GeoSeries.from_multipoints_xy( + xy, cudf.concat([cudf.Series(0), offsets.cumsum()]) + ) From 753334c9fff9cb04381e4c965a768bdd1a73d53f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Apr 2023 22:57:07 +0000 Subject: [PATCH 069/126] Pass all contains tests. --- .../core/binpreds/feature_contains.py | 92 +++++--------- .../tests/binpreds/test_binpred_internals.py | 117 +++++++++++++++++- .../cuspatial/utils/binpred_utils.py | 47 +++++-- 3 files changed, 183 insertions(+), 73 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 15ed2ad3a..84b50cf82 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -2,9 +2,6 @@ from typing import TypeVar -import cupy as cp -import numpy as np - import cudf from cuspatial.core.binpreds.binpred_interface import ( @@ -19,6 +16,8 @@ MultiPoint, Point, Polygon, + _false_series, + _linestrings_to_center_point, _open_polygon_rings, _points_and_lines_to_multipoints, _zero_series, @@ -45,62 +44,19 @@ def _preprocess(self, lhs, rhs): def _intersection_results_for_contains(self, lhs, rhs): pli = lhs._basic_intersects_pli(rhs) pli_features = pli[1] - pli_offsets = cudf.Series(pli[0]) - # Which feature goes with which offset? - pli_sizes = pli_offsets[1:].reset_index(drop=True) - pli_offsets[ - :-1 - ].reset_index(drop=True) - # Have to use host to create the offsets mapping - pli_mapping = cp.array( - np.arange(len(lhs)).repeat(pli_sizes.values_host) - ) - - # This mapping identifies which intersect feature belongs to which - # intersection. + if len(pli_features) == 0: + return _zero_series(len(lhs)) - points_mask = pli_features.type == "Point" - lines_mask = pli_features.type == "Linestring" - - points = pli_features[points_mask] - lines = pli_features[lines_mask] - - final_intersection_count = _zero_series(len(lhs)) - from cuspatial.core.geoseries import GeoSeries + pli_offsets = cudf.Series(pli[0]) - # Write a new method, _points_and_lines_to_multipoints that condenses - # The result into a single multipoint that can be worked with. - multipoints = _points_and_lines_to_multipoints(pli_features) + multipoints = _points_and_lines_to_multipoints( + pli_features, pli_offsets + ) - if len(lines) > 0: - # This is wrong. If a linestring is in a single intersection, - # it will tile out to all of the features. It needs to be - # compared only against the matching feature. pli_mapping - # determines which features match which intersections. + intersect_equals_count = multipoints._basic_equals_count(rhs) - multipoints = GeoSeries.from_multipoints_xy( - lines.lines.xy, pli_offsets * 2 - ) - lines_intersect_equals_count = multipoints._basic_equals_count(rhs) - final_intersection_count.iloc[ - pli_mapping[lines_mask] - ] = lines_intersect_equals_count[pli_mapping[lines_mask]] - breakpoint() - if len(points) > 0: - # Each point falls on the edge of the polygon and is in the - # boundary. - multipoints = GeoSeries.from_multipoints_xy( - points.points.xy.tile(len(lhs)), - cp.arange(len(lhs) + 1) * len(points), - ) - points_intersect_equals_count = multipoints._basic_equals_count( - rhs - ) // len(lhs) - final_intersection_count.iloc[ - pli_mapping[points_mask] - ] = points_intersect_equals_count[pli_mapping[points_mask]] - breakpoint() - # TODO Have to use .iloc here because of a bug in cudf - return final_intersection_count + breakpoint() + return intersect_equals_count def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): lines_rhs = _open_polygon_rings(rhs) @@ -114,11 +70,29 @@ def _compute_polygon_linestring_contains( self, lhs, rhs, preprocessor_result ): contains = lhs._basic_contains_count(rhs).reset_index(drop=True) - if (contains == 0).all(): - # If a linestring only intersects with the boundary of a polygon, - # it is not contained. - return rhs.sizes == 2 intersects = self._intersection_results_for_contains(lhs, rhs) + if (contains == 0).all() and (intersects != 0).all(): + # The hardest case. We need to check if the linestring is + # contained in the boundary of the polygon, the interior, + # or the exterior. + # We only need to test linestrings that are length 2. + # Divide the linestring in half and test the point for containment + # in the polygon. + + breakpoint() + if (rhs.sizes == 2).any(): + center_points = _linestrings_to_center_point( + rhs[rhs.sizes == 2] + ) + size_two_results = _false_series(len(lhs)) + size_two_results[rhs.sizes == 2] = ( + lhs._basic_contains_count(center_points) > 0 + ) + return size_two_results + else: + line_intersections = _false_series(len(lhs)) + line_intersections[intersects == rhs.sizes] = True + return line_intersections return contains + intersects >= rhs.sizes def _compute_predicate(self, lhs, rhs, preprocessor_result): diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py index a1b9fe257..9b87f821f 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_internals.py @@ -6,6 +6,7 @@ import cuspatial from cuspatial.core.binpreds.binpred_dispatch import EQUALS_DISPATCH from cuspatial.utils.binpred_utils import ( + _linestrings_to_center_point, _open_polygon_rings, _points_and_lines_to_multipoints, ) @@ -144,7 +145,8 @@ def test_points_and_lines_to_multipoints(): MultiPoint([(1, 1), (2, 2)]), ] ) - got = _points_and_lines_to_multipoints(mixed) + offsets = [0, 1, 2] + got = _points_and_lines_to_multipoints(mixed, offsets) assert (got.multipoints.xy == expected.multipoints.xy).all() @@ -161,7 +163,8 @@ def test_points_and_lines_to_multipoints_reverse(): MultiPoint([(0, 0)]), ] ) - got = _points_and_lines_to_multipoints(mixed) + offsets = [0, 1, 2] + got = _points_and_lines_to_multipoints(mixed, offsets) assert (got.multipoints.xy == expected.multipoints.xy).all() @@ -180,7 +183,8 @@ def test_points_and_lines_to_multipoints_two_points_one_linestring(): MultiPoint([(3, 3)]), ] ) - got = _points_and_lines_to_multipoints(mixed) + offsets = [0, 1, 2, 3] + got = _points_and_lines_to_multipoints(mixed, offsets) assert (got.multipoints.xy == expected.multipoints.xy).all() @@ -199,7 +203,8 @@ def test_points_and_lines_to_multipoints_two_linestrings_one_point(): MultiPoint([(3, 3), (4, 4)]), ] ) - got = _points_and_lines_to_multipoints(mixed) + offsets = [0, 1, 2, 3] + got = _points_and_lines_to_multipoints(mixed, offsets) assert (got.multipoints.xy == expected.multipoints.xy).all() @@ -228,5 +233,107 @@ def test_points_and_lines_to_multipoints_complex(): MultiPoint([(14, 14)]), ] ) - got = _points_and_lines_to_multipoints(mixed) + offsets = [0, 1, 2, 3, 4, 5, 6, 7, 8] + got = _points_and_lines_to_multipoints(mixed, offsets) assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_no_points(): + mixed = cuspatial.GeoSeries( + [ + LineString([(0, 0), (1, 1), (2, 2), (3, 3)]), + LineString([(5, 5), (6, 6)]), + LineString([(9, 9), (10, 10), (11, 11)]), + LineString([(12, 12), (13, 13)]), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]), + MultiPoint([(5, 5), (6, 6)]), + MultiPoint([(9, 9), (10, 10), (11, 11)]), + MultiPoint([(12, 12), (13, 13)]), + ] + ) + offsets = [0, 1, 2, 3, 4] + got = _points_and_lines_to_multipoints(mixed, offsets) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_no_linestrings(): + mixed = cuspatial.GeoSeries( + [ + Point(0, 0), + Point(4, 4), + Point(7, 7), + Point(8, 8), + Point(14, 14), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(0, 0)]), + MultiPoint([(4, 4)]), + MultiPoint([(7, 7)]), + MultiPoint([(8, 8)]), + MultiPoint([(14, 14)]), + ] + ) + offsets = [0, 1, 2, 3, 4, 5] + got = _points_and_lines_to_multipoints(mixed, offsets) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_points_and_lines_to_multipoints_real_example(): + mixed = cuspatial.GeoSeries( + [ + Point(7, 7), + Point(4, 4), + LineString([(5, 5), (6, 6)]), + LineString([(9, 9), (10, 10), (11, 11)]), + LineString([(12, 12), (13, 13)]), + Point(8, 8), + Point(14, 14), + ] + ) + expected = cuspatial.GeoSeries( + [ + MultiPoint([(7, 7), (4, 4)]), + MultiPoint( + [ + (5, 5), + (6, 6), + (9, 9), + (10, 10), + (11, 11), + (12, 12), + (13, 13), + ] + ), + MultiPoint([(8, 8), (14, 14)]), + ] + ) + offsets = [0, 2, 5, 7] + got = _points_and_lines_to_multipoints(mixed, offsets) + assert (got.multipoints.xy == expected.multipoints.xy).all() + + +def test_linestrings_to_center_point(): + linestrings = cuspatial.GeoSeries( + [ + LineString([(0, 0), (10, 10)]), + LineString([(5, 5), (6, 6)]), + LineString([(10, 10), (9, 9)]), + LineString([(11, 11), (1, 1)]), + ] + ) + expected = cuspatial.GeoSeries( + [ + Point(5, 5), + Point(5.5, 5.5), + Point(9.5, 9.5), + Point(6, 6), + ] + ) + got = _linestrings_to_center_point(linestrings) + assert (got.points.xy == expected.points.xy).all() diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index d9757f5f4..31cf30dfe 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -303,7 +303,7 @@ def _open_polygon_rings(geoseries): ) -def _points_and_lines_to_multipoints(geoseries): +def _points_and_lines_to_multipoints(geoseries, offsets): """Converts a geoseries of points and lines into a geoseries of multipoints.""" points_mask = geoseries.type == "Point" @@ -312,21 +312,50 @@ def _points_and_lines_to_multipoints(geoseries): raise ValueError("Geoseries must contain only points and lines") points = geoseries[points_mask] lines = geoseries[lines_mask] - offsets = _zero_series(len(geoseries)) - offsets[points_mask] = 1 + points_offsets = _zero_series(len(geoseries)) + points_offsets[points_mask] = 1 lines_series = geoseries[lines_mask] lines_sizes = lines_series.sizes - lines_sizes.index = offsets[lines_mask].index - offsets[lines_mask] = lines_series.sizes.values xy = _zero_series(len(points.points.xy) + len(lines.lines.xy)) sizes = _zero_series(len(geoseries)) + if (lines_sizes != 0).all(): + lines_sizes.index = points_offsets[lines_mask].index + points_offsets[lines_mask] = lines_series.sizes.values + sizes[lines_mask] = lines.sizes.values * 2 sizes[points_mask] = 2 - sizes[lines_mask] = lines.sizes.values * 2 # TODO Inevitable host device copy points_xy_mask = cp.array(np.repeat(points_mask, sizes.values_host)) xy.iloc[points_xy_mask] = points.points.xy.reset_index(drop=True) xy.iloc[~points_xy_mask] = lines.lines.xy.reset_index(drop=True) - breakpoint() - return cuspatial.GeoSeries.from_multipoints_xy( - xy, cudf.concat([cudf.Series(0), offsets.cumsum()]) + collected_offsets = cudf.concat( + [cudf.Series([0]), sizes.cumsum()] + ).reset_index(drop=True)[offsets] + result = cuspatial.GeoSeries.from_multipoints_xy( + xy, collected_offsets // 2 + ) + return result + + +def _linestrings_to_center_point(geoseries): + if (geoseries.sizes != 2).any(): + raise ValueError( + "Geoseries must contain only linestrings with two points" + ) + x = geoseries.lines.x + y = geoseries.lines.y + return cuspatial.GeoSeries.from_points_xy( + cudf.DataFrame( + { + "x": ( + x[::2].reset_index(drop=True) + + x[1::2].reset_index(drop=True) + ) + / 2, + "y": ( + y[::2].reset_index(drop=True) + + y[1::2].reset_index(drop=True) + ) + / 2, + } + ).interleave_columns() ) From a5ec83afba5fbad675e047e5d6083370a3bf5ee4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 21 Apr 2023 23:06:40 +0000 Subject: [PATCH 070/126] Resolve new indexing bug related to fixing a bug in geoseries. --- .../cuspatial/core/binpreds/complex_geometry_predicate.py | 1 - python/cuspatial/cuspatial/core/binpreds/feature_contains.py | 3 --- python/cuspatial/cuspatial/core/binpreds/feature_crosses.py | 1 - python/cuspatial/cuspatial/core/geoseries.py | 4 ++-- .../cuspatial/tests/binpreds/test_binpred_test_dispatch.py | 2 +- python/cuspatial/cuspatial/tests/test_geoseries.py | 2 -- 6 files changed, 3 insertions(+), 10 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index e506ae6e3..5e2d30490 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -212,7 +212,6 @@ def _postprocess_multi( if len(result_df) == 0: return count_result hits = result_df["point_index_x"] - breakpoint() hits.index = count_result.iloc[result_df["rhs_index"]].index count_result.iloc[result_df["rhs_index"]] = hits return count_result diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 84b50cf82..4c61f6367 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -55,7 +55,6 @@ def _intersection_results_for_contains(self, lhs, rhs): intersect_equals_count = multipoints._basic_equals_count(rhs) - breakpoint() return intersect_equals_count def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): @@ -63,7 +62,6 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, lines_rhs) polygon_size_reduction = 1 - breakpoint() return contains + intersects >= rhs.sizes - polygon_size_reduction def _compute_polygon_linestring_contains( @@ -79,7 +77,6 @@ def _compute_polygon_linestring_contains( # Divide the linestring in half and test the point for containment # in the polygon. - breakpoint() if (rhs.sizes == 2).any(): center_points = _linestrings_to_center_point( rhs[rhs.sizes == 2] diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index f11132c66..13d253d26 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -48,7 +48,6 @@ def _preprocess(self, lhs, rhs): contains = rhs.contains(lhs) contains_properly = rhs.contains_properly(lhs) intersects = lhs._basic_intersects_through(rhs) - breakpoint() return (~contains & contains_properly) | ( ~contains & ~contains_properly & intersects ) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 9ed396348..fe48edc75 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -403,8 +403,8 @@ def _type_int_to_field(self): def __getitem__(self, indexes): # Slice the types and offsets - union_offsets = self._sr._column._meta.union_offsets.iloc[indexes] - union_types = self._sr._column._meta.input_types.iloc[indexes] + union_offsets = self._sr._column._meta.union_offsets.loc[indexes] + union_types = self._sr._column._meta.input_types.loc[indexes] points = self._sr._column.points mpoints = self._sr._column.mpoints diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 53ab96875..11aec2f04 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -27,7 +27,7 @@ def wrapper(*args, **kwargs): out_file = open("test_binpred_test_dispatch.log", "w") -# @xfail_on_exception # TODO: Remove when all tests are passing +@xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 diff --git a/python/cuspatial/cuspatial/tests/test_geoseries.py b/python/cuspatial/cuspatial/tests/test_geoseries.py index 1a66d4457..3a8500c09 100644 --- a/python/cuspatial/cuspatial/tests/test_geoseries.py +++ b/python/cuspatial/cuspatial/tests/test_geoseries.py @@ -273,8 +273,6 @@ def test_getitem_slice_points_loc(): gps = gpd.GeoSeries([p0, p1, p2]) cus = cuspatial.from_geopandas(gps) assert_eq_point(cus[0:1][0], gps[0:1][0]) - assert_eq_point(cus[0:2][0], gps[0:2][0]) - assert_eq_point(cus[1:2][1], gps[1:2][1]) assert_eq_point(cus[0:3][0], gps[0:3][0]) assert_eq_point(cus[1:3][1], gps[1:3][1]) assert_eq_point(cus[2:3][2], gps[2:3][2]) From 1a2456a7b6d431d3dbc50a06a2acaa00dd079c1f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Sat, 22 Apr 2023 01:08:37 +0000 Subject: [PATCH 071/126] Remove breakpoints and pass a couple of old bug tests. --- .../core/binpreds/complex_geometry_predicate.py | 6 +++++- .../cuspatial/core/binpreds/feature_contains.py | 3 ++- python/cuspatial/cuspatial/core/geoseries.py | 11 +++-------- .../cuspatial/tests/binpreds/test_contains.py | 4 +++- python/cuspatial/cuspatial/utils/binpred_utils.py | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index 5e2d30490..150a2f44d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -205,7 +205,8 @@ def _postprocess_multi( any_result = _false_series(len(rhs)) if len(result_df) == 0: return any_result - any_result.loc[result_df["point_index_x"] > 0] = True + indexes = result_df["rhs_index"][result_df["point_index_x"] > 0] + any_result.iloc[indexes] = True return any_result elif mode == "basic_count": count_result = cudf.Series(cp.zeros(len(rhs)), dtype="int32") @@ -231,6 +232,9 @@ def _postprocess_points(self, lhs, rhs, preprocessor_result, op_result): contains_properly call. Used when the rhs is naturally points. """ allpairs_result = self._reindex_allpairs(lhs, op_result) + if self.config.allpairs: + return allpairs_result + final_result = _false_series(len(rhs)) if len(lhs) == len(rhs): matches = ( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 4c61f6367..ecb304634 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -54,7 +54,7 @@ def _intersection_results_for_contains(self, lhs, rhs): ) intersect_equals_count = multipoints._basic_equals_count(rhs) - + breakpoint() return intersect_equals_count def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): @@ -62,6 +62,7 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, lines_rhs) polygon_size_reduction = 1 + breakpoint() return contains + intersects >= rhs.sizes - polygon_size_reduction def _compute_polygon_linestring_contains( diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index fe48edc75..627375b46 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -366,12 +366,7 @@ def __getitem__(self, item): return self._sr.iloc[item] map_df = cudf.DataFrame( - { - "map": self._sr.index, - "idx": cp.arange(len(self._sr.index)) - if not isinstance(item, Integral) - else item, - } + {"map": self._sr.index, "idx": cp.arange(len(self._sr.index))} ) index_df = cudf.DataFrame({"map": item}).reset_index() new_index = index_df.merge( @@ -403,8 +398,8 @@ def _type_int_to_field(self): def __getitem__(self, indexes): # Slice the types and offsets - union_offsets = self._sr._column._meta.union_offsets.loc[indexes] - union_types = self._sr._column._meta.input_types.loc[indexes] + union_offsets = self._sr._column._meta.union_offsets.iloc[indexes] + union_types = self._sr._column._meta.input_types.iloc[indexes] points = self._sr._column.points mpoints = self._sr._column.mpoints diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py index a643b71f0..274c96165 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py @@ -29,7 +29,9 @@ def test_adjacent(): def test_interior(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + lhs = cuspatial.GeoSeries( + [Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])] + ) rhs = cuspatial.GeoSeries( [Polygon([(0, 0), (0, 0.5), (0.5, 0.5), (0.5, 0)])] ) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 31cf30dfe..20d00a75d 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -145,7 +145,7 @@ def _linestrings_from_geometry(geoseries): elif geoseries.column_type == ColumnType.LINESTRING: return geoseries elif geoseries.column_type == ColumnType.POLYGON: - return _open_polygon_rings(geoseries) + return _linestrings_from_polygons(geoseries) else: raise NotImplementedError( "Cannot convert type {} to linestrings".format(geoseries.type) From a394ea60e4796a627baf72f51e546e150889443b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Sat, 22 Apr 2023 19:45:41 +0000 Subject: [PATCH 072/126] Pass all non-binpred-dispatch tests. --- .../cuspatial/cuspatial/core/binpreds/feature_contains.py | 6 ++++-- python/cuspatial/cuspatial/core/binpreds/feature_within.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index ecb304634..5c51922e2 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -53,7 +53,7 @@ def _intersection_results_for_contains(self, lhs, rhs): pli_features, pli_offsets ) - intersect_equals_count = multipoints._basic_equals_count(rhs) + intersect_equals_count = rhs._basic_equals_count(multipoints) breakpoint() return intersect_equals_count @@ -61,7 +61,9 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): lines_rhs = _open_polygon_rings(rhs) contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, lines_rhs) - polygon_size_reduction = 1 + polygon_size_reduction = rhs.polygons.part_offset.take( + rhs.polygons.geometry_offset[1:] + ) - rhs.polygons.part_offset.take(rhs.polygons.geometry_offset[:-1]) breakpoint() return contains + intersects >= rhs.sizes - polygon_size_reduction diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index c136bf237..4e25a4c99 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -55,7 +55,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - return rhs.contains(rhs) + return rhs.contains(lhs) class PolygonPolygonWithin(BinPred): From 410fd078b744dedcb2c5b2b8d80316a276c36373 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Sun, 23 Apr 2023 00:14:01 +0000 Subject: [PATCH 073/126] Pass all touches and more --- .../core/binpreds/feature_contains.py | 2 - .../cuspatial/core/binpreds/feature_covers.py | 17 +++++-- .../core/binpreds/feature_crosses.py | 24 ++++----- .../core/binpreds/feature_disjoint.py | 13 ++--- .../core/binpreds/feature_intersects.py | 15 ++++-- .../core/binpreds/feature_touches.py | 51 +++++++++---------- .../binpreds/test_binpred_test_dispatch.py | 2 +- 7 files changed, 65 insertions(+), 59 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 5c51922e2..a94c1572d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -54,7 +54,6 @@ def _intersection_results_for_contains(self, lhs, rhs): ) intersect_equals_count = rhs._basic_equals_count(multipoints) - breakpoint() return intersect_equals_count def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): @@ -64,7 +63,6 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): polygon_size_reduction = rhs.polygons.part_offset.take( rhs.polygons.geometry_offset[1:] ) - rhs.polygons.part_offset.take(rhs.polygons.geometry_offset[:-1]) - breakpoint() return contains + intersects >= rhs.sizes - polygon_size_reduction def _compute_polygon_linestring_contains( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index a4b640123..2b8ea2e59 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, NVIDIA CORPORATION. from cuspatial.core.binpreds.binpred_interface import ( + BinPred, ImpossiblePredicate, NotImplementedPredicate, ) @@ -45,6 +46,16 @@ def _preprocess(self, lhs, rhs): return rhs._basic_equals_all(lhs) +class PolygonPointCovers(BinPred): + def _preprocess(self, lhs, rhs): + return lhs._basic_contains_any(rhs) + + +class PolygonLineStringCovers(BinPred): + def _preprocess(self, lhs, rhs): + return lhs._basic_contains_all(rhs) + + class PolygonPolygonCovers(ContainsPredicateBase): def _preprocess(self, lhs, rhs): contains = lhs.contains(rhs) @@ -55,7 +66,7 @@ def _preprocess(self, lhs, rhs): (Point, Point): CoversPredicateBase, (Point, MultiPoint): NotImplementedPredicate, (Point, LineString): ImpossiblePredicate, - (Point, Polygon): CoversPredicateBase, + (Point, Polygon): ImpossiblePredicate, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, @@ -64,8 +75,8 @@ def _preprocess(self, lhs, rhs): (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): LineStringLineStringCovers, (LineString, Polygon): CoversPredicateBase, - (Polygon, Point): CoversPredicateBase, + (Polygon, Point): PolygonPointCovers, (Polygon, MultiPoint): CoversPredicateBase, - (Polygon, LineString): CoversPredicateBase, + (Polygon, LineString): PolygonLineStringCovers, (Polygon, Polygon): PolygonPolygonCovers, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 13d253d26..39d3a1321 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -1,6 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from cuspatial.core.binpreds.binpred_interface import ImpossiblePredicate +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + ImpossiblePredicate, +) from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -35,22 +38,15 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): return intersects & ~equals -class PolygonLineStringCrosses(CrossesByIntersectionPredicate): - def _compute_predicate(self, lhs, rhs, preprocessor_result): - intersects_through = lhs._basic_intersects_through(rhs) - equals = rhs._basic_equals(lhs) - contains_all = lhs._basic_contains_all(rhs) - return ~contains_all & ~equals & intersects_through +class PolygonLineStringCrosses(BinPred): + def _preprocess(self, lhs, rhs): + breakpoint() + return lhs._basic_contains_none(rhs) -class LineStringPolygonCrosses(PolygonLineStringCrosses): +class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): - contains = rhs.contains(lhs) - contains_properly = rhs.contains_properly(lhs) - intersects = lhs._basic_intersects_through(rhs) - return (~contains & contains_properly) | ( - ~contains & ~contains_properly & intersects - ) + return ~rhs._basic_contains_any(lhs) class PointPointCrosses(CrossesPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index f6b943493..7b2ee5e76 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -26,12 +26,7 @@ def _preprocess(self, lhs, rhs): (Point, Polygon) (Polygon, Point) """ - from cuspatial.core.binpreds.binpred_dispatch import CONTAINS_DISPATCH - - predicate = CONTAINS_DISPATCH[(lhs.column_type, rhs.column_type)]( - align=self.config.align - ) - return ~predicate(lhs, rhs) + return ~lhs._basic_contains_any(rhs) class PointLineStringDisjoint(PointLineStringIntersects): @@ -72,9 +67,7 @@ def _preprocess(self, lhs, rhs): class PolygonPolygonDisjoint(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains = rhs._basic_contains_any(lhs) - return ~intersects & ~contains + return ~lhs._basic_contains_any(rhs) & ~rhs._basic_contains_any(lhs) DispatchDict = { @@ -92,6 +85,6 @@ def _preprocess(self, lhs, rhs): (LineString, Polygon): LineStringPolygonDisjoint, (Polygon, Point): DisjointByWayOfContains, (Polygon, MultiPoint): NotImplementedPredicate, - (Polygon, LineString): NotImplementedPredicate, + (Polygon, LineString): DisjointByWayOfContains, (Polygon, Polygon): PolygonPolygonDisjoint, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index 5addb1428..b12c81795 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -155,13 +155,22 @@ def _preprocess(self, lhs, rhs): return intersects | contains -class PolygonPolygonIntersects(IntersectsPredicateBase): +class PolygonLineStringIntersects(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs._basic_intersects(rhs) - contains = rhs._basic_contains_any(lhs) + contains = lhs._basic_contains_any(rhs) return intersects | contains +class PolygonPolygonIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains_rhs = rhs._basic_contains_any(lhs) + contains_lhs = lhs._basic_contains_any(rhs) + + return intersects | contains_rhs | contains_lhs + + """ Type dispatch dictionary for intersects binary predicates. """ DispatchDict = { (Point, Point): IntersectsByEquals, @@ -178,6 +187,6 @@ def _preprocess(self, lhs, rhs): (LineString, Polygon): LineStringPolygonIntersects, (Polygon, Point): PolygonPointIntersects, (Polygon, MultiPoint): NotImplementedPredicate, - (Polygon, LineString): NotImplementedPredicate, + (Polygon, LineString): PolygonLineStringIntersects, (Polygon, Polygon): PolygonPolygonIntersects, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index e4364989a..7c4037d20 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -3,7 +3,6 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, - PreprocessorResult, ) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -28,16 +27,9 @@ class TouchesPredicateBase(ContainsPredicateBase): (Polygon, Polygon) """ - def _compute_predicate( - self, - lhs, - rhs, - preprocessor_result: PreprocessorResult, - ): - # contains = lhs._basic_contains_any(rhs) + def _preprocess(self, lhs, rhs): equals = lhs._basic_equals(rhs) - intersects = lhs._basic_intersects(rhs) - return equals | intersects + return equals class PointLineStringTouches(BinPred): @@ -67,26 +59,33 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonTouches(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects_count(rhs) == 1 + contains_none = ~lhs.contains_properly(rhs) + return intersects & contains_none + + +class PolygonPointTouches(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + return intersects + + +class PolygonLineStringTouches(BinPred): def _preprocess(self, lhs, rhs): # Intersection occurs - intersects = lhs.intersects(rhs) - # The linestring is contained but is not - # contained properly, it crosses - # This is the equivalent of crosses - contains = rhs.contains(lhs) - contains_properly = rhs.contains_properly(lhs) - return intersects | (~contains & contains_properly) + intersects = lhs._basic_intersects_count(rhs) == 1 + contains_none = ~lhs.contains_properly(rhs) + return intersects & contains_none class PolygonPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - # Intersection occurs - intersects = lhs.intersects(rhs) - # No points in the lhs are in the rhs - contains = rhs._basic_contains_any(lhs) - # Not equal - equals_all = lhs._basic_equals_all(rhs) - return intersects & ~contains & ~equals_all + contains_lhs_none = lhs._basic_contains_count(rhs) == 0 + contains_rhs_none = rhs._basic_contains_count(lhs) == 0 + intersects = lhs._basic_intersects_count(rhs) == 1 + breakpoint() + return contains_lhs_none & contains_rhs_none & intersects DispatchDict = { @@ -102,8 +101,8 @@ def _preprocess(self, lhs, rhs): (LineString, MultiPoint): TouchesPredicateBase, (LineString, LineString): LineStringLineStringTouches, (LineString, Polygon): LineStringPolygonTouches, - (Polygon, Point): TouchesPredicateBase, + (Polygon, Point): PolygonPointTouches, (Polygon, MultiPoint): TouchesPredicateBase, - (Polygon, LineString): TouchesPredicateBase, + (Polygon, LineString): PolygonLineStringTouches, (Polygon, Polygon): PolygonPolygonTouches, } diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 11aec2f04..53ab96875 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -27,7 +27,7 @@ def wrapper(*args, **kwargs): out_file = open("test_binpred_test_dispatch.log", "w") -@xfail_on_exception # TODO: Remove when all tests are passing +# @xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 From bd5aebf10ae3094e3a7cff79d24a67af6f408aaa Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 24 Apr 2023 13:43:58 +0000 Subject: [PATCH 074/126] Five failing tests. --- python/cuspatial/cuspatial/core/binpreds/feature_covers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 2b8ea2e59..2951f25b4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -53,7 +53,8 @@ def _preprocess(self, lhs, rhs): class PolygonLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_contains_all(rhs) + contains = lhs.contains(rhs) + return contains class PolygonPolygonCovers(ContainsPredicateBase): From 1c98d6d772974ab7373fb0a5dc5d6e0e2a5d369d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 24 Apr 2023 17:45:42 +0000 Subject: [PATCH 075/126] Working on the last few cases that depend on variations of .contains that do better intersection testing. --- .../core/binpreds/feature_contains.py | 10 +++++++++- .../cuspatial/core/binpreds/feature_covers.py | 3 +-- .../core/binpreds/feature_crosses.py | 13 +++++++----- .../core/binpreds/feature_touches.py | 1 - .../cuspatial/core/binpreds/feature_within.py | 20 +++++++++---------- python/cuspatial/cuspatial/core/geoseries.py | 1 + .../binpreds/test_binpred_test_dispatch.py | 4 ---- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index a94c1572d..f83c5ce62 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -5,6 +5,7 @@ import cudf from cuspatial.core.binpreds.binpred_interface import ( + BinPred, ImpossiblePredicate, NotImplementedPredicate, ) @@ -121,6 +122,13 @@ def _preprocess(self, lhs, rhs): return lhs._basic_equals(rhs) +class LineStringPointContains(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + equals = lhs._basic_equals(rhs) + return intersects & ~equals + + class LineStringMultiPointContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): return lhs._linestring_multipoint_contains(rhs) @@ -143,7 +151,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): ContainsPredicateBase, + (LineString, Point): LineStringPointContains, (LineString, MultiPoint): LineStringMultiPointContainsPredicate, (LineString, LineString): LineStringLineStringContainsPredicate, (LineString, Polygon): ImpossiblePredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 2951f25b4..2b8ea2e59 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -53,8 +53,7 @@ def _preprocess(self, lhs, rhs): class PolygonLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): - contains = lhs.contains(rhs) - return contains + return lhs._basic_contains_all(rhs) class PolygonPolygonCovers(ContainsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 39d3a1321..bf5889b07 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -38,15 +38,18 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): return intersects & ~equals -class PolygonLineStringCrosses(BinPred): +class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): - breakpoint() - return lhs._basic_contains_none(rhs) + contains = rhs.contains(lhs) + contains_any = rhs._basic_contains_any(lhs) + return ~contains & contains_any -class LineStringPolygonCrosses(BinPred): +class PolygonLineStringCrosses(BinPred): def _preprocess(self, lhs, rhs): - return ~rhs._basic_contains_any(lhs) + contains = lhs.contains(rhs) + contains_any = lhs._basic_contains_any(rhs) + return ~contains & contains_any class PointPointCrosses(CrossesPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 7c4037d20..31a1559eb 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -84,7 +84,6 @@ def _preprocess(self, lhs, rhs): contains_lhs_none = lhs._basic_contains_count(rhs) == 0 contains_rhs_none = rhs._basic_contains_count(lhs) == 0 intersects = lhs._basic_intersects_count(rhs) == 1 - breakpoint() return contains_lhs_none & contains_rhs_none & intersects diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 4e25a4c99..4ac811666 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -2,11 +2,10 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, + ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase -from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -28,26 +27,27 @@ class WithinPredicateBase(EqualsPredicateBase): pass -class WithinIntersectsPredicate(IntersectsPredicateBase): +class WithinIntersectsPredicate(BinPred): def _preprocess(self, lhs, rhs): intersects = rhs._basic_intersects(lhs) equals = rhs._basic_equals(lhs) return intersects & ~equals -class PointLineStringWithin(WithinIntersectsPredicate): +class PointLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): - # Note the order of arguments is reversed. - return super()._preprocess(rhs, lhs) + intersects = lhs.intersects(rhs) + equals = lhs._basic_equals(rhs) + return intersects & ~equals -class PointPolygonWithin(ContainsPredicateBase): +class PointPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): return rhs.contains_properly(lhs) -class LineStringLineStringWithin(IntersectsPredicateBase): - def _compute_predicate(self, lhs, rhs, preprocessor_result): +class LineStringLineStringWithin(BinPred): + def _preprocess(self, lhs, rhs): intersects = rhs._basic_intersects(lhs) equals = rhs._basic_equals_all(lhs) return intersects & equals @@ -72,7 +72,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): WithinIntersectsPredicate, (MultiPoint, Polygon): PolygonPolygonWithin, - (LineString, Point): WithinIntersectsPredicate, + (LineString, Point): ImpossiblePredicate, (LineString, MultiPoint): WithinIntersectsPredicate, (LineString, LineString): LineStringLineStringWithin, (LineString, Polygon): LineStringPolygonWithin, diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 627375b46..be739d7b2 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1485,5 +1485,6 @@ def _basic_contains_all(self, other): `.contains_properly call.""" lhs = self rhs = _multipoints_from_geometry(other) + breakpoint() contains = lhs.contains(rhs, mode="basic_all") return contains diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 53ab96875..232b94bc0 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -6,8 +6,6 @@ import pytest from binpred_test_dispatch import predicate, simple_test # noqa: F401 -from cuspatial.utils.column_utils import contains_only_polygons - """Decorator function that xfails a test if an exception is throw by the test function. Will be removed when all tests are passing.""" @@ -79,8 +77,6 @@ def test_simple_features( gpdrhs = rhs.to_geopandas() # Reverse - if predicate == "contains" and not contains_only_polygons(rhs): - return pred_fn = getattr(rhs, predicate) got = pred_fn(lhs) gpd_pred_fn = getattr(gpdrhs, predicate) From 57517cdfebc5a2570dd0d2681369e245b7df93b3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 25 Apr 2023 22:02:43 +0000 Subject: [PATCH 076/126] 296/297 predicates. --- .../cuspatial/core/binpreds/feature_covers.py | 22 +++++++++++++- .../core/binpreds/feature_crosses.py | 12 +++++--- .../core/binpreds/feature_overlaps.py | 11 +++++-- .../core/binpreds/feature_touches.py | 16 +++++----- python/cuspatial/cuspatial/core/geoseries.py | 30 ++++++++++++++----- .../cuspatial/utils/binpred_utils.py | 21 +++++++++++++ 6 files changed, 89 insertions(+), 23 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 2b8ea2e59..282423ac0 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -16,6 +16,9 @@ MultiPoint, Point, Polygon, + _linestrings_is_degenerate, + _points_and_lines_to_multipoints, + _zero_series, ) @@ -53,7 +56,24 @@ def _preprocess(self, lhs, rhs): class PolygonLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_contains_all(rhs) + contains_count = lhs._basic_contains_count(rhs) + pli = lhs._basic_intersects_pli(rhs) + intersections = pli[1] + equality = _zero_series(len(rhs)) + breakpoint() + if len(intersections) == len(rhs): + # If the result is degenerate + is_degenerate = _linestrings_is_degenerate(intersections) + # If all the points in the intersection are in the rhs + equality = intersections._basic_equals_count(rhs) + if len(is_degenerate) > 0: + equality[is_degenerate] = 1 + elif len(intersections) > 0: + matching_length_multipoints = _points_and_lines_to_multipoints( + intersections, pli[0] + ) + equality = matching_length_multipoints._basic_equals_count(rhs) + return contains_count + equality >= rhs.sizes class PolygonPolygonCovers(ContainsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index bf5889b07..38e30115e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -41,15 +41,19 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): contains = rhs.contains(lhs) - contains_any = rhs._basic_contains_any(lhs) - return ~contains & contains_any + contains_any = rhs._basic_contains_properly_any(lhs) + intersects = rhs._basic_intersects_through(lhs) + breakpoint() + return ~contains & (contains_any & intersects) class PolygonLineStringCrosses(BinPred): def _preprocess(self, lhs, rhs): contains = lhs.contains(rhs) - contains_any = lhs._basic_contains_any(rhs) - return ~contains & contains_any + contains_any = lhs._basic_contains_properly_any(rhs) + intersects = lhs._basic_intersects_through(rhs) + breakpoint() + return ~contains & (contains_any & intersects) class PointPointCrosses(CrossesPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index f1bfa70fb..a6ff6714c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -35,9 +35,14 @@ class OverlapsPredicateBase(EqualsPredicateBase): class PolygonPolygonOverlaps(ContainsPredicateBase): def _preprocess(self, lhs, rhs): - equals_all = lhs._basic_equals_all(rhs) - intersects_not_touches = lhs._basic_intersects_through(rhs) - return ~equals_all & intersects_not_touches + contains_lhs = lhs.contains(rhs) + contains_rhs = rhs.contains(lhs) + contains_properly_lhs = lhs._basic_contains_properly_any(rhs) + contains_properly_rhs = rhs._basic_contains_properly_any(lhs) + breakpoint() + return ~(contains_lhs | contains_rhs) & ( + contains_properly_lhs | contains_properly_rhs + ) class PolygonPointOverlaps(ContainsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 31a1559eb..82c47d789 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -60,9 +60,12 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects_count(rhs) == 1 - contains_none = ~lhs.contains_properly(rhs) - return intersects & contains_none + intersects = lhs._basic_intersects_count(rhs) + contains = rhs.contains(lhs) + contains_any = rhs._basic_contains_properly_any(lhs) + breakpoint() + intersects = (intersects == 1) | (intersects == 2) + return intersects & ~contains & ~contains_any class PolygonPointTouches(BinPred): @@ -71,12 +74,9 @@ def _preprocess(self, lhs, rhs): return intersects -class PolygonLineStringTouches(BinPred): +class PolygonLineStringTouches(LineStringPolygonTouches): def _preprocess(self, lhs, rhs): - # Intersection occurs - intersects = lhs._basic_intersects_count(rhs) == 1 - contains_none = ~lhs.contains_properly(rhs) - return intersects & contains_none + return super()._preprocess(rhs, lhs) class PolygonPolygonTouches(BinPred): diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index be739d7b2..2a6511077 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -41,6 +41,9 @@ from cuspatial.utils.binpred_utils import ( _linestrings_from_geometry, _multipoints_from_geometry, + _multipoints_is_degenerate, + _points_and_lines_to_multipoints, + _zero_series, ) from cuspatial.utils.column_utils import ( contains_only_linestrings, @@ -1427,13 +1430,18 @@ def _basic_intersects_pli(self, other): def _basic_intersects_count(self, other): """Utility method that returns the number of points in the lhs geometry that intersect with the rhs geometry.""" - result = self._basic_intersects_pli(other) - # Flatten result into list of sizes - is_offsets = cudf.Series(result[0]) - is_sizes = is_offsets[1:].reset_index(drop=True) - is_offsets[ - :-1 - ].reset_index(drop=True) - return is_sizes + pli = self._basic_intersects_pli(other) + breakpoint() + if len(pli[1]) == 0: + return _zero_series(len(other)) + intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) + sizes = cudf.Series(intersections.sizes) + # If the result is degenerate + is_degenerate = _multipoints_is_degenerate(intersections) + # If all the points in the intersection are in the rhs + if len(is_degenerate) > 0: + sizes[is_degenerate] = 1 + return sizes def _basic_intersects(self, other): """Utility method that returns True if any point in the lhs geometry @@ -1479,6 +1487,14 @@ def _basic_contains_any(self, other): intersects = lhs._basic_intersects(other) return contains | intersects + def _basic_contains_properly_any(self, other): + """Utility method that returns True if any point in the lhs geometry + is contained_properly in the rhs geometry.""" + lhs = self + rhs = _multipoints_from_geometry(other) + contains = lhs.contains_properly(rhs, mode="basic_any") + return contains + def _basic_contains_all(self, other): """Utililty method that returns True if all points in the lhs geometry are contained_properly in the rhs geometry. Equivalent to the public diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 20d00a75d..7229df632 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -359,3 +359,24 @@ def _linestrings_to_center_point(geoseries): } ).interleave_columns() ) + + +def _multipoints_is_degenerate(geoseries): + """Only tests if the first two points are degenerate.""" + offsets = geoseries.multipoints.geometry_offset[:-1] + sizes_mask = geoseries.sizes > 1 + x1 = geoseries.multipoints.x[offsets[sizes_mask]] + x2 = geoseries.multipoints.x[offsets[sizes_mask] + 1] + y1 = geoseries.multipoints.y[offsets[sizes_mask]] + y2 = geoseries.multipoints.y[offsets[sizes_mask] + 1] + result = _false_series(len(geoseries)) + is_degenerate = ( + x1.reset_index(drop=True) == x2.reset_index(drop=True) + ) & (y1.reset_index(drop=True) == y2.reset_index(drop=True)) + result[sizes_mask] = is_degenerate.reset_index(drop=True) + return result + + +def _linestrings_is_degenerate(geoseries): + multipoints = _multipoints_from_geometry(geoseries) + return _multipoints_is_degenerate(multipoints) From 3fc9d44265b2984e2c374a06ce26aedc36d4a9d4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 26 Apr 2023 17:08:43 +0000 Subject: [PATCH 077/126] Pass 100% of tests! --- .../cuspatial/core/binpreds/feature_covers.py | 1 - .../cuspatial/core/binpreds/feature_crosses.py | 15 +++++---------- .../cuspatial/core/binpreds/feature_overlaps.py | 1 - .../cuspatial/core/binpreds/feature_touches.py | 13 +++++++++++-- python/cuspatial/cuspatial/core/geoseries.py | 2 -- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 282423ac0..3a81da522 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -60,7 +60,6 @@ def _preprocess(self, lhs, rhs): pli = lhs._basic_intersects_pli(rhs) intersections = pli[1] equality = _zero_series(len(rhs)) - breakpoint() if len(intersections) == len(rhs): # If the result is degenerate is_degenerate = _linestrings_is_degenerate(intersections) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 38e30115e..a30ed051b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -40,20 +40,15 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): - contains = rhs.contains(lhs) - contains_any = rhs._basic_contains_properly_any(lhs) intersects = rhs._basic_intersects_through(lhs) - breakpoint() - return ~contains & (contains_any & intersects) + touches = rhs.touches(lhs) + contains = rhs.contains(lhs) + return ~touches & intersects & ~contains -class PolygonLineStringCrosses(BinPred): +class PolygonLineStringCrosses(LineStringPolygonCrosses): def _preprocess(self, lhs, rhs): - contains = lhs.contains(rhs) - contains_any = lhs._basic_contains_properly_any(rhs) - intersects = lhs._basic_intersects_through(rhs) - breakpoint() - return ~contains & (contains_any & intersects) + return super()._preprocess(rhs, lhs) class PointPointCrosses(CrossesPredicateBase): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index a6ff6714c..e1dac734d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -39,7 +39,6 @@ def _preprocess(self, lhs, rhs): contains_rhs = rhs.contains(lhs) contains_properly_lhs = lhs._basic_contains_properly_any(rhs) contains_properly_rhs = rhs._basic_contains_properly_any(lhs) - breakpoint() return ~(contains_lhs | contains_rhs) & ( contains_properly_lhs | contains_properly_rhs ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 82c47d789..56ac75432 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -10,6 +10,8 @@ MultiPoint, Point, Polygon, + _false_series, + _points_and_lines_to_multipoints, ) @@ -60,12 +62,19 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): + pli = lhs._basic_intersects_pli(rhs) + if len(pli[1]) == 0: + return _false_series(len(lhs)) + intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) + # A touch can only occur if the point in the intersection + # is equal to a point in the linestring, it must + # terminate in the boundary of the polygon. + equals = intersections._basic_equals_count(lhs) > 0 intersects = lhs._basic_intersects_count(rhs) contains = rhs.contains(lhs) contains_any = rhs._basic_contains_properly_any(lhs) - breakpoint() intersects = (intersects == 1) | (intersects == 2) - return intersects & ~contains & ~contains_any + return equals & intersects & ~contains & ~contains_any class PolygonPointTouches(BinPred): diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 2a6511077..e13a92c63 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1431,7 +1431,6 @@ def _basic_intersects_count(self, other): """Utility method that returns the number of points in the lhs geometry that intersect with the rhs geometry.""" pli = self._basic_intersects_pli(other) - breakpoint() if len(pli[1]) == 0: return _zero_series(len(other)) intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) @@ -1501,6 +1500,5 @@ def _basic_contains_all(self, other): `.contains_properly call.""" lhs = self rhs = _multipoints_from_geometry(other) - breakpoint() contains = lhs.contains(rhs, mode="basic_all") return contains From d0138c95bb0aa7a717c892711251e531ab3f7661 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 16:06:37 +0000 Subject: [PATCH 078/126] Resolve missed merge files. --- .../pairwise_multipoint_equals_count.cuh | 111 ++++++++---------- .../pairwise_multipoint_equals_count.cuh | 110 +++++++---------- 2 files changed, 94 insertions(+), 127 deletions(-) diff --git a/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh index 32f96fbfc..38758ee85 100644 --- a/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh @@ -19,21 +19,11 @@ #include #include #include -<<<<<<< HEAD:cpp/include/cuspatial/experimental/detail/pairwise_multipoint_equals_count.cuh -#include -#include -#include -#include -#include - == == == - = #include #include #include #include #include - >>>>>>> branch - 23.06 : cpp / include / cuspatial / detail / - pairwise_multipoint_equals_count.cuh #include #include @@ -46,71 +36,72 @@ #include #include - namespace cuspatial +namespace cuspatial { +namespace detail { + +template +void __global__ pairwise_multipoint_equals_count_kernel(MultiPointRangeA lhs, + MultiPointRangeB rhs, + OutputIt output) { - namespace detail { - - template - void __global__ pairwise_multipoint_equals_count_kernel(MultiPointRangeA lhs, - MultiPointRangeB rhs, - OutputIt output) - { - using T = typename MultiPointRangeA::point_t::value_type; - - for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.num_points(); - idx += gridDim.x * blockDim.x) { - auto geometry_id = lhs.geometry_idx_from_point_idx(idx); - vec_2d lhs_point = lhs.point_begin()[idx]; - auto rhs_multipoint = rhs[geometry_id]; - - atomicAdd(&output[geometry_id], - thrust::binary_search( - thrust::seq, rhs_multipoint.begin(), rhs_multipoint.end(), lhs_point)); - } + using T = typename MultiPointRangeA::point_t::value_type; + + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < lhs.num_points(); + idx += gridDim.x * blockDim.x) { + auto geometry_id = lhs.geometry_idx_from_point_idx(idx); + vec_2d lhs_point = lhs.point_begin()[idx]; + auto rhs_multipoint = rhs[geometry_id]; + + atomicAdd( + &output[geometry_id], + thrust::binary_search(thrust::seq, rhs_multipoint.begin(), rhs_multipoint.end(), lhs_point)); } +} - } // namespace detail +} // namespace detail - template - OutputIt pairwise_multipoint_equals_count( - MultiPointRangeA lhs, MultiPointRangeB rhs, OutputIt output, rmm::cuda_stream_view stream) - { - using T = typename MultiPointRangeA::point_t::value_type; - using index_t = typename MultiPointRangeB::index_t; +template +OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs, + MultiPointRangeB rhs, + OutputIt output, + rmm::cuda_stream_view stream) +{ + using T = typename MultiPointRangeA::point_t::value_type; + using index_t = typename MultiPointRangeB::index_t; - static_assert(is_same_floating_point(), - "Origin and input must have the same base floating point type."); + static_assert(is_same_floating_point(), + "Origin and input must have the same base floating point type."); - CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), "lhs and rhs inputs should have the same size."); + CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), "lhs and rhs inputs should have the same size."); - if (lhs.size() == 0) return output; + if (lhs.size() == 0) return output; - // Create a sorted copy of the rhs points. - auto key_it = make_geometry_id_iterator(rhs.offsets_begin(), rhs.offsets_end()); + // Create a sorted copy of the rhs points. + auto key_it = make_geometry_id_iterator(rhs.offsets_begin(), rhs.offsets_end()); - rmm::device_uvector rhs_keys(rhs.num_points(), stream); - rmm::device_uvector> rhs_point_sorted(rhs.num_points(), stream); + rmm::device_uvector rhs_keys(rhs.num_points(), stream); + rmm::device_uvector> rhs_point_sorted(rhs.num_points(), stream); - thrust::copy(rmm::exec_policy(stream), key_it, key_it + rhs.num_points(), rhs_keys.begin()); - thrust::copy( - rmm::exec_policy(stream), rhs.point_begin(), rhs.point_end(), rhs_point_sorted.begin()); + thrust::copy(rmm::exec_policy(stream), key_it, key_it + rhs.num_points(), rhs_keys.begin()); + thrust::copy( + rmm::exec_policy(stream), rhs.point_begin(), rhs.point_end(), rhs_point_sorted.begin()); - auto rhs_with_keys = - thrust::make_zip_iterator(thrust::make_tuple(rhs_keys.begin(), rhs_point_sorted.begin())); + auto rhs_with_keys = + thrust::make_zip_iterator(thrust::make_tuple(rhs_keys.begin(), rhs_point_sorted.begin())); - thrust::sort(rmm::exec_policy(stream), rhs_with_keys, rhs_with_keys + rhs.num_points()); + thrust::sort(rmm::exec_policy(stream), rhs_with_keys, rhs_with_keys + rhs.num_points()); - auto rhs_sorted = multipoint_range{ - rhs.offsets_begin(), rhs.offsets_end(), rhs_point_sorted.begin(), rhs_point_sorted.end()}; + auto rhs_sorted = multipoint_range{ + rhs.offsets_begin(), rhs.offsets_end(), rhs_point_sorted.begin(), rhs_point_sorted.end()}; - detail::zero_data_async(output, output + lhs.size(), stream); - auto [tpb, n_blocks] = grid_1d(lhs.num_points()); - detail::pairwise_multipoint_equals_count_kernel<<>>( - lhs, rhs_sorted, output); + detail::zero_data_async(output, output + lhs.size(), stream); + auto [tpb, n_blocks] = grid_1d(lhs.num_points()); + detail::pairwise_multipoint_equals_count_kernel<<>>( + lhs, rhs_sorted, output); - CUSPATIAL_CHECK_CUDA(stream.value()); + CUSPATIAL_CHECK_CUDA(stream.value()); - return output + lhs.size(); - } + return output + lhs.size(); +} } // namespace cuspatial diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh index 0e10ed6f3..acc6de389 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh @@ -16,82 +16,58 @@ #pragma once -<<<<<<< HEAD:cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh -#include - == == == - = #include - >>>>>>> branch - 23.06 : cpp / include / cuspatial / - pairwise_multipoint_equals_count.cuh #include #include - namespace cuspatial -{ - /** - <<<<<<< HEAD:cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh - * @brief Compute the number of multipoint pairs that are equal. - ======= - * @brief Count the number of equal points in multipoint pairs. - >>>>>>> branch-23.06:cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh - * - * Given two ranges of multipoints, this function counts points in the left-hand - * multipoint that exist in the corresponding right-hand multipoint. - * - * @example - * - * lhs: { {0, 0} } - * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } - * count: { 1 } +namespace cuspatial { +/** + * @brief Count the number of equal points in multipoint pairs. + * + * Given two ranges of multipoints, this function counts points in the left-hand + * multipoint that exist in the corresponding right-hand multipoint. + * + * @example + * + * lhs: { {0, 0} } + * rhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * count: { 1 } - * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } - * rhs: { {0, 0} } - * count: { 1 } + * lhs: { {0, 0}, {1, 1}, {2, 2}, {3, 3} } + * rhs: { {0, 0} } + * count: { 1 } - * lhs: { { {3, 3}, {3, 3}, {0, 0} }, { {0, 0}, {1, 1}, {2, 2} }, { {0, 0} } } - * rhs: { { {0, 0}, {2, 2}, {1, 1} }, { {2, 2}, {0, 0}, {1, 1} }, { {1, 1} } } - * count: { 1, 3, 0 } - * - * @note All input iterators must conform to the specification defined by - * `multipoint_range.cuh` and the output iterator must be able to accept for - * storage values of type - * `uint32_t`. - * - * @param[in] lhs_first multipoint_range of first array of multipoints - * @param[in] rhs_first multipoint_range of second array of multipoints - * @param[out] count_first: beginning of range of uint32_t counts - * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. - <<<<<<< HEAD:cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh - * - * @tparam MultiPointRangeA Iterator over multipoints. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - * @tparam MultiPointRangeB Iterator over multipoints. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible. - ======= - * @tparam MultiPointRangeA The multipolygon range to compare point equality from - * @tparam MultiPointRangeB The multipolygon range to compare point equality to - >>>>>>> branch-23.06:cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh - * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of - * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. - * - * @return Output iterator to the element past the last count result written. - * - * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator - * "LegacyRandomAccessIterator" - */ - template - OutputIt pairwise_multipoint_equals_count( - MultiPointRangeA lhs_first, - MultiPointRangeB rhs_first, - OutputIt count_first, - rmm::cuda_stream_view stream = rmm::cuda_stream_default); + * lhs: { { {3, 3}, {3, 3}, {0, 0} }, { {0, 0}, {1, 1}, {2, 2} }, { {0, 0} } } + * rhs: { { {0, 0}, {2, 2}, {1, 1} }, { {2, 2}, {0, 0}, {1, 1} }, { {1, 1} } } + * count: { 1, 3, 0 } + * + * @note All input iterators must conform to the specification defined by + * `multipoint_range.cuh` and the output iterator must be able to accept for + * storage values of type + * `uint32_t`. + * + * @param[in] lhs_first multipoint_range of first array of multipoints + * @param[in] rhs_first multipoint_range of second array of multipoints + * @param[out] count_first: beginning of range of uint32_t counts + * @param[in] stream: The CUDA stream on which to perform computations and allocate memory. + * @tparam MultiPointRangeA The multipolygon range to compare point equality from + * @tparam MultiPointRangeB The multipolygon range to compare point equality to + * @tparam OutputIt Iterator over uint32_t. Must meet the requirements of + * [LegacyRandomAccessIterator][LinkLRAI] and be device-accessible and mutable. + * + * @return Output iterator to the element past the last count result written. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +OutputIt pairwise_multipoint_equals_count(MultiPointRangeA lhs_first, + MultiPointRangeB rhs_first, + OutputIt count_first, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); } // namespace cuspatial -<<<<<<< HEAD:cpp/include/cuspatial/experimental/pairwise_multipoint_equals_count.cuh -#include -======= #include - >>>>>>> branch - 23.06 : cpp / include / cuspatial / pairwise_multipoint_equals_count.cuh From e60af293fbd6fab9111a7fb8c7c8febef80f2eb6 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 11:10:52 -0500 Subject: [PATCH 079/126] Return whitespace. --- .../cuspatial/detail/pairwise_multipoint_equals_count.cuh | 1 + cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh | 1 + 2 files changed, 2 insertions(+) diff --git a/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh index 38758ee85..f6d4f46c1 100644 --- a/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/detail/pairwise_multipoint_equals_count.cuh @@ -37,6 +37,7 @@ #include namespace cuspatial { + namespace detail { template diff --git a/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh b/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh index acc6de389..f15617c60 100644 --- a/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh +++ b/cpp/include/cuspatial/pairwise_multipoint_equals_count.cuh @@ -23,6 +23,7 @@ #include namespace cuspatial { + /** * @brief Count the number of equal points in multipoint pairs. * From 85676d125964010a8c4b768f2c6883b9219804f7 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 11:11:56 -0500 Subject: [PATCH 080/126] Still working on missed files. --- .../pairwise_multipoint_equals_count.cu | 148 +++++++++--------- 1 file changed, 70 insertions(+), 78 deletions(-) diff --git a/cpp/src/spatial/pairwise_multipoint_equals_count.cu b/cpp/src/spatial/pairwise_multipoint_equals_count.cu index f1bc24b7e..71e2b6d2b 100644 --- a/cpp/src/spatial/pairwise_multipoint_equals_count.cu +++ b/cpp/src/spatial/pairwise_multipoint_equals_count.cu @@ -18,14 +18,8 @@ #include #include -<<<<<<< HEAD -#include -#include - == == == - = #include #include - >>>>>>> branch - 23.06 #include #include @@ -41,83 +35,81 @@ #include #include - namespace cuspatial -{ - namespace detail { - namespace { - - template - struct pairwise_multipoint_equals_count_impl { - using SizeType = cudf::device_span::size_type; - - template )> - std::unique_ptr operator()(geometry_column_view const& lhs, - geometry_column_view const& rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) - { - auto size = lhs.size(); // lhs is a buffer of xy coords - auto type = cudf::data_type(cudf::type_to_id()); - auto result = - cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); - - auto lhs_range = make_multipoint_range(lhs); - auto rhs_range = make_multipoint_range(rhs); - - cuspatial::pairwise_multipoint_equals_count( - lhs_range, rhs_range, result->mutable_view().begin(), stream); - - return result; - } - - template ), typename... Args> - std::unique_ptr operator()(Args&&...) - - { - CUSPATIAL_FAIL("pairwise_multipoint_equals_count only supports floating point types."); - } - }; - - } // namespace - - template - struct pairwise_multipoint_equals_count { - std::unique_ptr operator()(geometry_column_view lhs, - geometry_column_view rhs, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) - { - return cudf::type_dispatcher( - lhs.coordinate_type(), - pairwise_multipoint_equals_count_impl{}, - lhs, - rhs, - stream, - mr); - } - }; - - } // namespace detail - - std::unique_ptr pairwise_multipoint_equals_count( - geometry_column_view const& lhs, - geometry_column_view const& rhs, - rmm::mr::device_memory_resource* mr) +namespace cuspatial { +namespace detail { +namespace { + +template +struct pairwise_multipoint_equals_count_impl { + using SizeType = cudf::device_span::size_type; + + template )> + std::unique_ptr operator()(geometry_column_view const& lhs, + geometry_column_view const& rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) { - CUSPATIAL_EXPECTS(lhs.geometry_type() == geometry_type_id::POINT && - rhs.geometry_type() == geometry_type_id::POINT, + auto size = lhs.size(); // lhs is a buffer of xy coords + auto type = cudf::data_type(cudf::type_to_id()); + auto result = + cudf::make_fixed_width_column(type, size, cudf::mask_state::UNALLOCATED, stream, mr); - "pairwise_multipoint_equals_count only supports POINT geometries" - "for both lhs and rhs"); + auto lhs_range = make_multipoint_range(lhs); + auto rhs_range = make_multipoint_range(rhs); - CUSPATIAL_EXPECTS(lhs.coordinate_type() == rhs.coordinate_type(), - "Input geometries must have the same coordinate data types."); + cuspatial::pairwise_multipoint_equals_count( + lhs_range, rhs_range, result->mutable_view().begin(), stream); + + return result; + } - CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), - "Input geometries must have the same number of multipoints."); + template ), typename... Args> + std::unique_ptr operator()(Args&&...) - return multi_geometry_double_dispatch( - lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); + { + CUSPATIAL_FAIL("pairwise_multipoint_equals_count only supports floating point types."); } +}; + +} // namespace + +template +struct pairwise_multipoint_equals_count { + std::unique_ptr operator()(geometry_column_view lhs, + geometry_column_view rhs, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + return cudf::type_dispatcher( + lhs.coordinate_type(), + pairwise_multipoint_equals_count_impl{}, + lhs, + rhs, + stream, + mr); + } +}; + +} // namespace detail + +std::unique_ptr pairwise_multipoint_equals_count(geometry_column_view const& lhs, + geometry_column_view const& rhs, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS(lhs.geometry_type() == geometry_type_id::POINT && + rhs.geometry_type() == geometry_type_id::POINT, + + "pairwise_multipoint_equals_count only supports POINT geometries" + "for both lhs and rhs"); + + CUSPATIAL_EXPECTS(lhs.coordinate_type() == rhs.coordinate_type(), + "Input geometries must have the same coordinate data types."); + + CUSPATIAL_EXPECTS(lhs.size() == rhs.size(), + "Input geometries must have the same number of multipoints."); + + return multi_geometry_double_dispatch( + lhs.collection_type(), rhs.collection_type(), lhs, rhs, rmm::cuda_stream_default, mr); +} } // namespace cuspatial From f64b461a5c0e429ba6946a3fdb95b92dbd9bab6d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 11:13:01 -0500 Subject: [PATCH 081/126] Rename CMakeFiles --- cpp/tests/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index d19767823..3ec61c6ce 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -97,7 +97,7 @@ ConfigureTest(POINT_POLYGON_DISTANCE_TEST # equality ConfigureTest(PAIRWISE_MULTIPOINT_EQUALS_COUNT_TEST - spatial/pairwise_multipoint_equals_count_test.cpp) + spatial/equality/pairwise_multipoint_equals_count_test.cpp) # intersection ConfigureTest(LINESTRING_INTERSECTION_TEST @@ -209,7 +209,7 @@ ConfigureTest(POLYGON_DISTANCE_TEST_EXP # equality ConfigureTest(PAIRWISE_MULTIPOINT_EQUALS_COUNT_TEST_EXP - experimental/spatial/pairwise_multipoint_equals_count_test.cu) + spatial/equality/pairwise_multipoint_equals_count_test.cu) # intersection ConfigureTest(LINESTRING_INTERSECTION_TEST_EXP From ddda72c151f2b85d6aba0459a486bf81759f08b4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 11:13:33 -0500 Subject: [PATCH 082/126] One too many files. --- .../pairwise_multipoint_equals_count_test.cpp | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp diff --git a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp b/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp deleted file mode 100644 index 1d2923e4b..000000000 --- a/cpp/tests/spatial/pairwise_multipoint_equals_count_test.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2023, 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 - -using namespace cuspatial; -using namespace cuspatial::test; - -using namespace cudf::test; - -constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; - -template -struct PairwiseMultipointEqualsCountTestTyped : public BaseFixture { - rmm::cuda_stream_view stream() { return cudf::get_default_stream(); } -}; - -struct PairwiseMultipointEqualsCountTestUntyped : public BaseFixture { - rmm::cuda_stream_view stream() { return cudf::get_default_stream(); } -}; - -// float and double are logically the same but would require separate tests due to precision. -using TestTypes = Types; -TYPED_TEST_CASE(PairwiseMultipointEqualsCountTestTyped, TestTypes); - -TYPED_TEST(PairwiseMultipointEqualsCountTestTyped, Empty) -{ - using T = TypeParam; - auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); - auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); - - auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); - auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); - - auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv); - - auto expected = fixed_width_column_wrapper({}); - - expect_columns_equivalent(expected, output->view(), verbosity); -} - -TYPED_TEST(PairwiseMultipointEqualsCountTestTyped, InvalidLength) -{ - using T = TypeParam; - auto [ptype, lhs] = make_point_column({0, 1}, {0.0, 0.0}, this->stream()); - auto [pytpe, rhs] = make_point_column({0, 1, 2}, {1.0, 1.0, 0.0, 0.0}, this->stream()); - - auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); - auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); - - EXPECT_THROW(auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv), - cuspatial::logic_error); -} - -TEST_F(PairwiseMultipointEqualsCountTestUntyped, InvalidTypes) -{ - auto [ptype, lhs] = make_point_column(std::initializer_list{}, this->stream()); - auto [pytpe, rhs] = make_point_column(std::initializer_list{}, this->stream()); - - auto lhs_gcv = geometry_column_view(lhs->view(), ptype, geometry_type_id::POINT); - auto rhs_gcv = geometry_column_view(rhs->view(), ptype, geometry_type_id::POINT); - - EXPECT_THROW(auto output = cuspatial::pairwise_multipoint_equals_count(lhs_gcv, rhs_gcv), - cuspatial::logic_error); -} From 0d7ca82342be53fb9cac52a89a89e311b1d7f723 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 17:51:19 +0000 Subject: [PATCH 083/126] Pull similar files from the master branch. --- .../binpreds/complex_geometry_predicate.py | 33 ++++- .../core/binpreds/feature_contains.py | 95 ++++++++++++-- .../binpreds/feature_contains_properly.py | 47 +------ .../cuspatial/core/binpreds/feature_within.py | 2 +- python/cuspatial/cuspatial/core/geoseries.py | 60 +++++---- .../cuspatial/tests/binpreds/test_contains.py | 4 +- .../cuspatial/utils/binpred_utils.py | 119 ++++++++++++++++++ 7 files changed, 281 insertions(+), 79 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index 171251f63..150a2f44d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -16,6 +16,7 @@ from cuspatial.utils.binpred_utils import ( _count_results_in_multipoint_geometries, _false_series, + _true_series, ) from cuspatial.utils.column_utils import ( contains_only_linestrings, @@ -154,7 +155,9 @@ def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: return allpairs_result - def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): + def _postprocess_multi( + self, lhs, rhs, preprocessor_result, op_result, mode + ): """Reconstruct the original geometry from the result of the contains_properly call. @@ -190,6 +193,31 @@ def _postprocess_multi(self, lhs, rhs, preprocessor_result, op_result): result_df = hits.reset_index().merge( expected_count.reset_index(), on="rhs_index" ) + + # Handling for the basic predicates + if mode == "basic_none": + none_result = _true_series(len(rhs)) + if len(result_df) == 0: + return none_result + none_result.loc[result_df["point_index_x"] > 0] = False + return none_result + elif mode == "basic_any": + any_result = _false_series(len(rhs)) + if len(result_df) == 0: + return any_result + indexes = result_df["rhs_index"][result_df["point_index_x"] > 0] + any_result.iloc[indexes] = True + return any_result + elif mode == "basic_count": + count_result = cudf.Series(cp.zeros(len(rhs)), dtype="int32") + if len(result_df) == 0: + return count_result + hits = result_df["point_index_x"] + hits.index = count_result.iloc[result_df["rhs_index"]].index + count_result.iloc[result_df["rhs_index"]] = hits + return count_result + + # Handling for full contains (equivalent to basic predicate all) result_df["feature_in_polygon"] = ( result_df["point_index_x"] >= result_df["point_index_y"] ) @@ -204,6 +232,9 @@ def _postprocess_points(self, lhs, rhs, preprocessor_result, op_result): contains_properly call. Used when the rhs is naturally points. """ allpairs_result = self._reindex_allpairs(lhs, op_result) + if self.config.allpairs: + return allpairs_result + final_result = _false_series(len(rhs)) if len(lhs) == len(rhs): matches = ( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 13e52bf92..f83c5ce62 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -2,7 +2,10 @@ from typing import TypeVar +import cudf + from cuspatial.core.binpreds.binpred_interface import ( + BinPred, ImpossiblePredicate, NotImplementedPredicate, ) @@ -14,7 +17,16 @@ MultiPoint, Point, Polygon, - _multipoints_from_geometry, + _false_series, + _linestrings_to_center_point, + _open_polygon_rings, + _points_and_lines_to_multipoints, + _zero_series, +) +from cuspatial.utils.column_utils import ( + contains_only_linestrings, + contains_only_points, + contains_only_polygons, ) GeoSeries = TypeVar("GeoSeries") @@ -30,15 +42,75 @@ def _preprocess(self, lhs, rhs): preprocessor_result = super()._preprocess_multi(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) - def _compute_predicate(self, lhs, rhs, preprocessor_result): - contains = lhs._basic_contains_count(rhs).reset_index(drop=True) - rhs_points = _multipoints_from_geometry(rhs) - intersects = lhs._basic_intersects_count(rhs_points).reset_index( - drop=True + def _intersection_results_for_contains(self, lhs, rhs): + pli = lhs._basic_intersects_pli(rhs) + pli_features = pli[1] + if len(pli_features) == 0: + return _zero_series(len(lhs)) + + pli_offsets = cudf.Series(pli[0]) + + multipoints = _points_and_lines_to_multipoints( + pli_features, pli_offsets ) - # TODO: Need better point counting in intersection. + + intersect_equals_count = rhs._basic_equals_count(multipoints) + return intersect_equals_count + + def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): + lines_rhs = _open_polygon_rings(rhs) + contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) + intersects = self._intersection_results_for_contains(lhs, lines_rhs) + polygon_size_reduction = rhs.polygons.part_offset.take( + rhs.polygons.geometry_offset[1:] + ) - rhs.polygons.part_offset.take(rhs.polygons.geometry_offset[:-1]) + return contains + intersects >= rhs.sizes - polygon_size_reduction + + def _compute_polygon_linestring_contains( + self, lhs, rhs, preprocessor_result + ): + contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + intersects = self._intersection_results_for_contains(lhs, rhs) + if (contains == 0).all() and (intersects != 0).all(): + # The hardest case. We need to check if the linestring is + # contained in the boundary of the polygon, the interior, + # or the exterior. + # We only need to test linestrings that are length 2. + # Divide the linestring in half and test the point for containment + # in the polygon. + + if (rhs.sizes == 2).any(): + center_points = _linestrings_to_center_point( + rhs[rhs.sizes == 2] + ) + size_two_results = _false_series(len(lhs)) + size_two_results[rhs.sizes == 2] = ( + lhs._basic_contains_count(center_points) > 0 + ) + return size_two_results + else: + line_intersections = _false_series(len(lhs)) + line_intersections[intersects == rhs.sizes] = True + return line_intersections return contains + intersects >= rhs.sizes + def _compute_predicate(self, lhs, rhs, preprocessor_result): + if contains_only_points(rhs): + # Special case in GeoPandas, points are not contained + # in the boundary of a polygon. + contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + return contains > 0 + elif contains_only_linestrings(rhs): + return self._compute_polygon_linestring_contains( + lhs, rhs, preprocessor_result + ) + elif contains_only_polygons(rhs): + return self._compute_polygon_polygon_contains( + lhs, rhs, preprocessor_result + ) + else: + raise NotImplementedError("Invalid rhs for contains operation") + class ContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): @@ -50,6 +122,13 @@ def _preprocess(self, lhs, rhs): return lhs._basic_equals(rhs) +class LineStringPointContains(BinPred): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + equals = lhs._basic_equals(rhs) + return intersects & ~equals + + class LineStringMultiPointContainsPredicate(ContainsPredicateBase): def _compute_results(self, lhs, rhs, preprocessor_result): return lhs._linestring_multipoint_contains(rhs) @@ -72,7 +151,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): ContainsPredicateBase, + (LineString, Point): LineStringPointContains, (LineString, MultiPoint): LineStringMultiPointContainsPredicate, (LineString, LineString): LineStringLineStringContainsPredicate, (LineString, Polygon): ImpossiblePredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 69dad17d4..519bcb8cd 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -2,10 +2,6 @@ from typing import TypeVar -import cupy as cp - -import cudf - from cuspatial.core.binpreds.binpred_interface import ( BinPred, ContainsOpResult, @@ -23,7 +19,6 @@ MultiPoint, Point, Polygon, - _false_series, _is_complex, ) from cuspatial.utils.column_utils import ( @@ -114,42 +109,6 @@ def _compute_predicate( op_result = ContainsOpResult(pip_result, preprocessor_result) return self._postprocess(lhs, rhs, preprocessor_result, op_result) - def _return_unprocessed_result(self, lhs, op_result, preprocessor_result): - """Return the result of the basic predicate without any - postprocessing. - """ - reindex_pip_result = self._reindex_allpairs(lhs, op_result) - if len(reindex_pip_result) == 0: - if self.config.mode == "basic_count": - return cudf.Series(cp.zeros(len(lhs), dtype="int32")) - else: - return _false_series(len(lhs)) - # Postprocessing early termination. Basic requests, or allpairs - # requests do not do object reconstruction. - if self.config.allpairs: - return reindex_pip_result - elif self.config.mode == "basic_none": - final_result = cudf.Series(cp.repeat([True], len(lhs))) - final_result.loc[reindex_pip_result["point_index"]] = False - return final_result - elif self.config.mode == "basic_any": - final_result = _false_series(len(lhs)) - final_result.loc[reindex_pip_result["point_index"]] = True - return final_result - elif self.config.mode == "basic_all": - sizes = ( - preprocessor_result.point_indices[1:] - - preprocessor_result.point_indices[:-1] - ) - result_sizes = reindex_pip_result["polygon_index"].value_counts() - final_result = _false_series( - len(preprocessor_result.point_indices) - ) - final_result.loc[sizes == result_sizes] = True - return final_result - elif self.config.mode == "basic_count": - return reindex_pip_result["polygon_index"].value_counts() - def _postprocess(self, lhs, rhs, preprocessor_result, op_result): """Postprocess the output GeoSeries to ensure that they are of the correct type for the predicate. @@ -183,16 +142,12 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): point index and the polygon index for each point in the polygon. """ - if self.config.mode != "full" or self.config.allpairs: - return self._return_unprocessed_result( - lhs, op_result, preprocessor_result - ) # for each input pair i: result[i] =  true iff point[i] is # contained in at least one polygon of multipolygon[i]. if _is_complex(rhs): return super()._postprocess_multi( - lhs, rhs, preprocessor_result, op_result + lhs, rhs, preprocessor_result, op_result, mode=self.config.mode ) else: return super()._postprocess_points( diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 056c1f9d6..b907cad81 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -49,7 +49,7 @@ def _preprocess(self, lhs, rhs): class PointPolygonWithin(ContainsPredicateBase): def _preprocess(self, lhs, rhs): - return rhs._basic_contains_any(lhs) + return rhs.contains_properly(lhs) class LineStringLineStringWithin(IntersectsPredicateBase): diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 6ce3812d1..9e41e288b 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -41,6 +41,9 @@ from cuspatial.utils.binpred_utils import ( _linestrings_from_geometry, _multipoints_from_geometry, + _multipoints_is_degenerate, + _points_and_lines_to_multipoints, + _zero_series, ) from cuspatial.utils.column_utils import ( contains_only_linestrings, @@ -190,20 +193,20 @@ def sizes(self): full_sizes = self.polygons.ring_offset.take( self.polygons.part_offset.take(self.polygons.geometry_offset) ) - return full_sizes[1:] - full_sizes[:-1] - 1 + return cudf.Series(full_sizes[1:] - full_sizes[:-1]) elif contains_only_linestrings(self): # Not supporting multilinestring yet full_sizes = self.lines.part_offset.take( self.lines.geometry_offset ) - return full_sizes[1:] - full_sizes[:-1] + return cudf.Series(full_sizes[1:] - full_sizes[:-1]) elif contains_only_multipoints(self): return ( self.multipoints.geometry_offset[1:] - self.multipoints.geometry_offset[:-1] ) elif contains_only_points(self): - return cp.repeat(cp.array(1), len(self)) + return cudf.Series(cp.repeat(cp.array(1), len(self))) else: if len(self) == 0: return cudf.Series([0], dtype="int32") @@ -370,12 +373,7 @@ def __getitem__(self, item): return self._sr.iloc[item] map_df = cudf.DataFrame( - { - "map": self._sr.index, - "idx": cp.arange(len(self._sr.index)) - if not isinstance(item, Integral) - else 0, - } + {"map": self._sr.index, "idx": cp.arange(len(self._sr.index))} ) index_df = cudf.DataFrame({"map": item}).reset_index() new_index = index_df.merge( @@ -1405,8 +1403,8 @@ def _basic_equals_all(self, other): rhs = _multipoints_from_geometry(other) result = pairwise_multipoint_equals_count(lhs, rhs) sizes = ( - rhs.multipoints.geometry_offset[1:] - - rhs.multipoints.geometry_offset[:-1] + lhs.multipoints.geometry_offset[1:] + - lhs.multipoints.geometry_offset[:-1] ) return result == sizes @@ -1436,13 +1434,17 @@ def _basic_intersects_pli(self, other): def _basic_intersects_count(self, other): """Utility method that returns the number of points in the lhs geometry that intersect with the rhs geometry.""" - result = self._basic_intersects_pli(other) - # Flatten result into list of sizes - is_offsets = cudf.Series(result[0]) - is_sizes = is_offsets[1:].reset_index(drop=True) - is_offsets[ - :-1 - ].reset_index(drop=True) - return is_sizes + pli = self._basic_intersects_pli(other) + if len(pli[1]) == 0: + return _zero_series(len(other)) + intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) + sizes = cudf.Series(intersections.sizes) + # If the result is degenerate + is_degenerate = _multipoints_is_degenerate(intersections) + # If all the points in the intersection are in the rhs + if len(is_degenerate) > 0: + sizes[is_degenerate] = 1 + return sizes def _basic_intersects(self, other): """Utility method that returns True if any point in the lhs geometry @@ -1467,21 +1469,34 @@ def _basic_contains_count(self, other): """ lhs = self rhs = _multipoints_from_geometry(other) - return lhs.contains_properly(rhs, mode="basic_count") + contains = lhs.contains_properly(rhs, mode="basic_count") + return contains def _basic_contains_none(self, other): """Utility method that returns True if none of the points in the lhs geometry are contained_properly in the rhs geometry.""" lhs = self rhs = _multipoints_from_geometry(other) - return lhs.contains_properly(rhs, mode="basic_none") + contains = lhs.contains_properly(rhs, mode="basic_none") + intersects = lhs._basic_intersects(other) + return contains & ~intersects def _basic_contains_any(self, other): """Utility method that returns True if any point in the lhs geometry is contained_properly in the rhs geometry.""" lhs = self rhs = _multipoints_from_geometry(other) - return lhs.contains_properly(rhs, mode="basic_any") + contains = lhs.contains_properly(rhs, mode="basic_any") + intersects = lhs._basic_intersects(other) + return contains | intersects + + def _basic_contains_properly_any(self, other): + """Utility method that returns True if any point in the lhs geometry + is contained_properly in the rhs geometry.""" + lhs = self + rhs = _multipoints_from_geometry(other) + contains = lhs.contains_properly(rhs, mode="basic_any") + return contains def _basic_contains_all(self, other): """Utililty method that returns True if all points in the lhs geometry @@ -1489,4 +1504,5 @@ def _basic_contains_all(self, other): `.contains_properly call.""" lhs = self rhs = _multipoints_from_geometry(other) - return lhs.contains_properly(rhs, mode="basic_all") + contains = lhs.contains(rhs, mode="basic_all") + return contains diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py index a643b71f0..274c96165 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py @@ -29,7 +29,9 @@ def test_adjacent(): def test_interior(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) + lhs = cuspatial.GeoSeries( + [Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])] + ) rhs = cuspatial.GeoSeries( [Polygon([(0, 0), (0, 0.5), (0.5, 0.5), (0.5, 0)])] ) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 32de863a8..7229df632 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, NVIDIA CORPORATION. import cupy as cp +import numpy as np import cudf @@ -22,6 +23,21 @@ def _false_series(size): return cudf.Series(cp.zeros(size, dtype=cp.bool_)) +def _true_series(size): + """Return a Series of True values""" + return cudf.Series(cp.ones(size, dtype=cp.bool_)) + + +def _zero_series(size): + """Return a Series of zeros""" + return cudf.Series(cp.zeros(size, dtype=cp.int32)) + + +def _one_series(size): + """Return a Series of ones""" + return cudf.Series(cp.ones(size, dtype=cp.int32)) + + def _count_results_in_multipoint_geometries(point_indices, point_result): """Count the number of points in each multipoint geometry. @@ -161,6 +177,7 @@ def _multipoints_from_polygons(geoseries): polygon_offsets = geoseries.polygons.ring_offset.take( geoseries.polygons.part_offset.take(geoseries.polygons.geometry_offset) ) + # Drop the endpoint from all polygons return cuspatial.GeoSeries.from_multipoints_xy(xy, polygon_offsets) @@ -261,3 +278,105 @@ def _is_complex(geoseries): if len(geoseries.multipoints.xy) > 0: return True return False + + +def _open_polygon_rings(geoseries): + """Converts a geoseries of polygons into a geoseries of linestrings + by opening the rings of each polygon.""" + x = geoseries.polygons.x + y = geoseries.polygons.y + parts = geoseries.polygons.part_offset.take( + geoseries.polygons.geometry_offset + ) + rings_mask = geoseries.polygons.ring_offset - 1 + rings_mask[0] = 0 + mask = _true_series(len(x)) + mask[rings_mask[1:]] = False + x = x[mask] + y = y[mask] + xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() + rings = geoseries.polygons.ring_offset - cp.arange(len(rings_mask)) + return cuspatial.GeoSeries.from_linestrings_xy( + xy, + rings, + parts, + ) + + +def _points_and_lines_to_multipoints(geoseries, offsets): + """Converts a geoseries of points and lines into a geoseries of + multipoints.""" + points_mask = geoseries.type == "Point" + lines_mask = geoseries.type == "Linestring" + if (points_mask + lines_mask).sum() != len(geoseries): + raise ValueError("Geoseries must contain only points and lines") + points = geoseries[points_mask] + lines = geoseries[lines_mask] + points_offsets = _zero_series(len(geoseries)) + points_offsets[points_mask] = 1 + lines_series = geoseries[lines_mask] + lines_sizes = lines_series.sizes + xy = _zero_series(len(points.points.xy) + len(lines.lines.xy)) + sizes = _zero_series(len(geoseries)) + if (lines_sizes != 0).all(): + lines_sizes.index = points_offsets[lines_mask].index + points_offsets[lines_mask] = lines_series.sizes.values + sizes[lines_mask] = lines.sizes.values * 2 + sizes[points_mask] = 2 + # TODO Inevitable host device copy + points_xy_mask = cp.array(np.repeat(points_mask, sizes.values_host)) + xy.iloc[points_xy_mask] = points.points.xy.reset_index(drop=True) + xy.iloc[~points_xy_mask] = lines.lines.xy.reset_index(drop=True) + collected_offsets = cudf.concat( + [cudf.Series([0]), sizes.cumsum()] + ).reset_index(drop=True)[offsets] + result = cuspatial.GeoSeries.from_multipoints_xy( + xy, collected_offsets // 2 + ) + return result + + +def _linestrings_to_center_point(geoseries): + if (geoseries.sizes != 2).any(): + raise ValueError( + "Geoseries must contain only linestrings with two points" + ) + x = geoseries.lines.x + y = geoseries.lines.y + return cuspatial.GeoSeries.from_points_xy( + cudf.DataFrame( + { + "x": ( + x[::2].reset_index(drop=True) + + x[1::2].reset_index(drop=True) + ) + / 2, + "y": ( + y[::2].reset_index(drop=True) + + y[1::2].reset_index(drop=True) + ) + / 2, + } + ).interleave_columns() + ) + + +def _multipoints_is_degenerate(geoseries): + """Only tests if the first two points are degenerate.""" + offsets = geoseries.multipoints.geometry_offset[:-1] + sizes_mask = geoseries.sizes > 1 + x1 = geoseries.multipoints.x[offsets[sizes_mask]] + x2 = geoseries.multipoints.x[offsets[sizes_mask] + 1] + y1 = geoseries.multipoints.y[offsets[sizes_mask]] + y2 = geoseries.multipoints.y[offsets[sizes_mask] + 1] + result = _false_series(len(geoseries)) + is_degenerate = ( + x1.reset_index(drop=True) == x2.reset_index(drop=True) + ) & (y1.reset_index(drop=True) == y2.reset_index(drop=True)) + result[sizes_mask] = is_degenerate.reset_index(drop=True) + return result + + +def _linestrings_is_degenerate(geoseries): + multipoints = _multipoints_from_geometry(geoseries) + return _multipoints_is_degenerate(multipoints) From 12fa90fc9106b12bcbb22eee00b5e05ab54bf94c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 13:52:40 -0500 Subject: [PATCH 084/126] Rename points back to final_rhs --- .../cuspatial/cuspatial/core/binpreds/binpred_interface.py | 6 +++--- .../cuspatial/core/binpreds/feature_contains_properly.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py index 858efe96d..d9a7c3837 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py @@ -50,7 +50,7 @@ class PreprocessorResult: The left-hand GeoSeries. rhs : GeoSeries The right-hand GeoSeries. - points : GeoSeries + final_rhs : GeoSeries The rhs GeoSeries, if modified by the preprocessor. For example the contains preprocessor converts any complex feature type into a collection of points. @@ -68,12 +68,12 @@ def __init__( ): self.lhs = lhs self.rhs = rhs - self.points = final_rhs + self.final_rhs = final_rhs self.point_indices = point_indices def __repr__(self): return f"PreprocessorResult(lhs={self.lhs}, rhs={self.rhs}, \ - points={self.points}, point_indices={self.point_indices})" + points={self.final_rhs}, point_indices={self.point_indices})" def __str__(self): return self.__repr__() diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 519bcb8cd..c6615a754 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -100,11 +100,11 @@ def _compute_predicate( ) if self._should_use_quadtree(lhs): pip_result = contains_properly( - lhs, preprocessor_result.points, how="quadtree" + lhs, preprocessor_result.final_rhs, how="quadtree" ) else: pip_result = contains_properly( - lhs, preprocessor_result.points, how="byte-limited" + lhs, preprocessor_result.final_rhs, how="byte-limited" ) op_result = ContainsOpResult(pip_result, preprocessor_result) return self._postprocess(lhs, rhs, preprocessor_result, op_result) From b45a73138df4bac86b3476924a67b5a0d0ac1a51 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 19:01:05 +0000 Subject: [PATCH 085/126] Update docs a bit. --- .../core/binpreds/complex_geometry_predicate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index 150a2f44d..b84c091fb 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -40,8 +40,8 @@ def _preprocess_multi(self, lhs, rhs): Returns ------- - result : GeoSeries - A GeoSeries of boolean values indicating whether each feature in + result : cudf.Series + A cudf.Series of boolean values indicating whether each feature in the right-hand GeoSeries satisfies the requirements of the point- in-polygon basic predicate with its corresponding feature in the left-hand GeoSeries. @@ -135,7 +135,11 @@ def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: Returns ------- cudf.DataFrame - + A cudf.DataFrame with two columns: `polygon_index` and + `point_index`. The `polygon_index` column contains the index + of the polygon from the original lhs that contains the point, + and the `point_index` column contains the index of the point + from the preprocessor final_rhs input to point-in-polygon. """ # Convert the quadtree part indices df into a polygon indices df polygon_indices = ( From fe978a333f9a0a4a5c1b851fbcfd4931feb3ae74 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 19:35:51 +0000 Subject: [PATCH 086/126] Add mode argument docs. --- .../core/binpreds/complex_geometry_predicate.py | 17 +++++++++++++++++ .../core/binpreds/feature_contains_properly.py | 9 ++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py index b84c091fb..d92590fb7 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py @@ -175,6 +175,23 @@ def _postprocess_multi( The result of the preprocessor. op_result : ContainsProperlyOpResult The result of the contains_properly call. + mode : str + The mode of the predicate. Various mode options are available + to support binary predicates. The mode options are `full`, + `basic_none`, `basic_any`, and `basic_count`. If the default + option `full` is specified, `.contains` or .contains_properly` + will return a boolean series indicating whether each feature + in the right-hand GeoSeries is contained by the corresponding + feature in the left-hand GeoSeries. If `basic_none` is + specified, `.contains` or .contains_properly` returns the + inverse of `full`. If `basic_any` is specified, `.contains` or + .contains_properly` returns a boolean series indicating + whether any point in the right-hand GeoSeries is contained by + the corresponding feature in the left-hand GeoSeries. If the + `basic_count` option is specified, `.contains` or + .contains_properly` returns a series of integers indicating + the number of points in the right-hand GeoSeries that are + contained by the corresponding feature in the left-hand Returns ------- diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index c6615a754..7f9a48539 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -10,10 +10,7 @@ PreprocessorResult, ) from cuspatial.core.binpreds.contains import contains_properly -from cuspatial.core.binpreds.feature_contains import ( - ComplexGeometryPredicate, - ContainsPredicateBase, -) +from cuspatial.core.binpreds.feature_contains import ComplexGeometryPredicate from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -29,9 +26,7 @@ GeoSeries = TypeVar("GeoSeries") -class ContainsProperlyPredicate( - ContainsPredicateBase, ComplexGeometryPredicate -): +class ContainsProperlyPredicate(ComplexGeometryPredicate): def __init__(self, **kwargs): """Base class for binary predicates that are defined in terms of a `contains` basic predicate. This class implements the logic that From 3b68ec298373e15ef230010dffc344d5be163bd6 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 20:03:41 +0000 Subject: [PATCH 087/126] Rename complex_geometry_predicate and cleaning up ContainsPredicates. --- ...cate.py => contains_geometry_processor.py} | 2 +- .../core/binpreds/feature_contains.py | 30 +++++++------------ .../binpreds/feature_contains_properly.py | 6 ++-- 3 files changed, 15 insertions(+), 23 deletions(-) rename python/cuspatial/cuspatial/core/binpreds/{complex_geometry_predicate.py => contains_geometry_processor.py} (99%) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py similarity index 99% rename from python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py rename to python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index d92590fb7..481222939 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -25,7 +25,7 @@ ) -class ComplexGeometryPredicate(BinPred): +class ContainsGeometryProcessor(BinPred): def _preprocess_multi(self, lhs, rhs): """Flatten any rhs into only its points xy array. This is necessary because the basic predicate for contains, point-in-polygon, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index f83c5ce62..2f1ccb2e0 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -9,8 +9,8 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.complex_geometry_predicate import ( - ComplexGeometryPredicate, +from cuspatial.core.binpreds.contains_geometry_processor import ( + ContainsGeometryProcessor, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -32,7 +32,7 @@ GeoSeries = TypeVar("GeoSeries") -class ContainsPredicateBase(ComplexGeometryPredicate): +class ContainsPredicate(ContainsGeometryProcessor): def __init__(self, **kwargs): super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) @@ -112,12 +112,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): raise NotImplementedError("Invalid rhs for contains operation") -class ContainsPredicate(ContainsPredicateBase): - def _compute_results(self, lhs, rhs, preprocessor_result): - return lhs._contains(rhs) - - -class PointPointContains(ContainsPredicateBase): +class PointPointContains(BinPred): def _preprocess(self, lhs, rhs): return lhs._basic_equals(rhs) @@ -129,12 +124,7 @@ def _preprocess(self, lhs, rhs): return intersects & ~equals -class LineStringMultiPointContainsPredicate(ContainsPredicateBase): - def _compute_results(self, lhs, rhs, preprocessor_result): - return lhs._linestring_multipoint_contains(rhs) - - -class LineStringLineStringContainsPredicate(ContainsPredicateBase): +class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): count = lhs._basic_equals_count(rhs) return count == rhs.sizes @@ -152,11 +142,11 @@ def _preprocess(self, lhs, rhs): (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointContains, - (LineString, MultiPoint): LineStringMultiPointContainsPredicate, + (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): LineStringLineStringContainsPredicate, (LineString, Polygon): ImpossiblePredicate, - (Polygon, Point): ContainsPredicateBase, - (Polygon, MultiPoint): ContainsPredicateBase, - (Polygon, LineString): ContainsPredicateBase, - (Polygon, Polygon): ContainsPredicateBase, + (Polygon, Point): ContainsPredicate, + (Polygon, MultiPoint): ContainsPredicate, + (Polygon, LineString): ContainsPredicate, + (Polygon, Polygon): ContainsPredicate, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 7f9a48539..cc127b49e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -10,7 +10,9 @@ PreprocessorResult, ) from cuspatial.core.binpreds.contains import contains_properly -from cuspatial.core.binpreds.feature_contains import ComplexGeometryPredicate +from cuspatial.core.binpreds.contains_geometry_processor import ( + ContainsGeometryProcessor, +) from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -26,7 +28,7 @@ GeoSeries = TypeVar("GeoSeries") -class ContainsProperlyPredicate(ComplexGeometryPredicate): +class ContainsProperlyPredicate(ContainsGeometryProcessor): def __init__(self, **kwargs): """Base class for binary predicates that are defined in terms of a `contains` basic predicate. This class implements the logic that From aae2f28db5064ba8794632643180f9d8689be081 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 15:21:09 -0500 Subject: [PATCH 088/126] Updating contains and contains_properly to be neater. --- ...cate.py => contains_geometry_processor.py} | 19 +++++++++++- .../core/binpreds/feature_contains.py | 30 +++++++------------ .../binpreds/feature_contains_properly.py | 9 ++---- 3 files changed, 31 insertions(+), 27 deletions(-) rename python/cuspatial/cuspatial/core/binpreds/{complex_geometry_predicate.py => contains_geometry_processor.py} (89%) diff --git a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py similarity index 89% rename from python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py rename to python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index b84c091fb..481222939 100644 --- a/python/cuspatial/cuspatial/core/binpreds/complex_geometry_predicate.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -25,7 +25,7 @@ ) -class ComplexGeometryPredicate(BinPred): +class ContainsGeometryProcessor(BinPred): def _preprocess_multi(self, lhs, rhs): """Flatten any rhs into only its points xy array. This is necessary because the basic predicate for contains, point-in-polygon, @@ -175,6 +175,23 @@ def _postprocess_multi( The result of the preprocessor. op_result : ContainsProperlyOpResult The result of the contains_properly call. + mode : str + The mode of the predicate. Various mode options are available + to support binary predicates. The mode options are `full`, + `basic_none`, `basic_any`, and `basic_count`. If the default + option `full` is specified, `.contains` or .contains_properly` + will return a boolean series indicating whether each feature + in the right-hand GeoSeries is contained by the corresponding + feature in the left-hand GeoSeries. If `basic_none` is + specified, `.contains` or .contains_properly` returns the + inverse of `full`. If `basic_any` is specified, `.contains` or + .contains_properly` returns a boolean series indicating + whether any point in the right-hand GeoSeries is contained by + the corresponding feature in the left-hand GeoSeries. If the + `basic_count` option is specified, `.contains` or + .contains_properly` returns a series of integers indicating + the number of points in the right-hand GeoSeries that are + contained by the corresponding feature in the left-hand Returns ------- diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index f83c5ce62..2f1ccb2e0 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -9,8 +9,8 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.complex_geometry_predicate import ( - ComplexGeometryPredicate, +from cuspatial.core.binpreds.contains_geometry_processor import ( + ContainsGeometryProcessor, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -32,7 +32,7 @@ GeoSeries = TypeVar("GeoSeries") -class ContainsPredicateBase(ComplexGeometryPredicate): +class ContainsPredicate(ContainsGeometryProcessor): def __init__(self, **kwargs): super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) @@ -112,12 +112,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): raise NotImplementedError("Invalid rhs for contains operation") -class ContainsPredicate(ContainsPredicateBase): - def _compute_results(self, lhs, rhs, preprocessor_result): - return lhs._contains(rhs) - - -class PointPointContains(ContainsPredicateBase): +class PointPointContains(BinPred): def _preprocess(self, lhs, rhs): return lhs._basic_equals(rhs) @@ -129,12 +124,7 @@ def _preprocess(self, lhs, rhs): return intersects & ~equals -class LineStringMultiPointContainsPredicate(ContainsPredicateBase): - def _compute_results(self, lhs, rhs, preprocessor_result): - return lhs._linestring_multipoint_contains(rhs) - - -class LineStringLineStringContainsPredicate(ContainsPredicateBase): +class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): count = lhs._basic_equals_count(rhs) return count == rhs.sizes @@ -152,11 +142,11 @@ def _preprocess(self, lhs, rhs): (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointContains, - (LineString, MultiPoint): LineStringMultiPointContainsPredicate, + (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): LineStringLineStringContainsPredicate, (LineString, Polygon): ImpossiblePredicate, - (Polygon, Point): ContainsPredicateBase, - (Polygon, MultiPoint): ContainsPredicateBase, - (Polygon, LineString): ContainsPredicateBase, - (Polygon, Polygon): ContainsPredicateBase, + (Polygon, Point): ContainsPredicate, + (Polygon, MultiPoint): ContainsPredicate, + (Polygon, LineString): ContainsPredicate, + (Polygon, Polygon): ContainsPredicate, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index c6615a754..cc127b49e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -10,9 +10,8 @@ PreprocessorResult, ) from cuspatial.core.binpreds.contains import contains_properly -from cuspatial.core.binpreds.feature_contains import ( - ComplexGeometryPredicate, - ContainsPredicateBase, +from cuspatial.core.binpreds.contains_geometry_processor import ( + ContainsGeometryProcessor, ) from cuspatial.utils.binpred_utils import ( LineString, @@ -29,9 +28,7 @@ GeoSeries = TypeVar("GeoSeries") -class ContainsProperlyPredicate( - ContainsPredicateBase, ComplexGeometryPredicate -): +class ContainsProperlyPredicate(ContainsGeometryProcessor): def __init__(self, **kwargs): """Base class for binary predicates that are defined in terms of a `contains` basic predicate. This class implements the logic that From 96e442e1227e801594ec6a727b987ac6b444931d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 20:46:26 +0000 Subject: [PATCH 089/126] Cleaning up things throughout. --- .../cuspatial/core/binpreds/feature_covers.py | 3 +-- .../cuspatial/core/binpreds/feature_overlaps.py | 11 +++++++---- .../cuspatial/core/binpreds/feature_touches.py | 6 +++--- .../cuspatial/core/binpreds/feature_within.py | 15 +++------------ 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 3a81da522..c6286f183 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -5,7 +5,6 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.core.binpreds.feature_intersects import ( IntersectsPredicateBase, @@ -75,7 +74,7 @@ def _preprocess(self, lhs, rhs): return contains_count + equality >= rhs.sizes -class PolygonPolygonCovers(ContainsPredicateBase): +class PolygonPolygonCovers(BinPred): def _preprocess(self, lhs, rhs): contains = lhs.contains(rhs) return contains diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index e1dac734d..b126afdd0 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -2,8 +2,11 @@ import cudf -from cuspatial.core.binpreds.binpred_interface import ImpossiblePredicate -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + ImpossiblePredicate, +) +from cuspatial.core.binpreds.feature_contains import ContainsPredicate from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, @@ -33,7 +36,7 @@ class OverlapsPredicateBase(EqualsPredicateBase): pass -class PolygonPolygonOverlaps(ContainsPredicateBase): +class PolygonPolygonOverlaps(BinPred): def _preprocess(self, lhs, rhs): contains_lhs = lhs.contains(rhs) contains_rhs = rhs.contains(lhs) @@ -44,7 +47,7 @@ def _preprocess(self, lhs, rhs): ) -class PolygonPointOverlaps(ContainsPredicateBase): +class PolygonPointOverlaps(ContainsPredicate): def _postprocess(self, lhs, rhs, op_result): if not has_same_geometry(lhs, rhs) or len(op_result.point_result) == 0: return _false_series(len(lhs)) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 56ac75432..bb257216b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -4,7 +4,7 @@ BinPred, ImpossiblePredicate, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.core.binpreds.feature_contains import ContainsPredicate from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -15,7 +15,7 @@ ) -class TouchesPredicateBase(ContainsPredicateBase): +class TouchesPredicateBase(ContainsPredicate): """Base class for binary predicates that use the contains predicate to implement the touches predicate. For example, a Point-Polygon Touches predicate is defined in terms of a Point-Polygon Contains @@ -39,7 +39,7 @@ def _preprocess(self, lhs, rhs): return lhs._basic_equals(rhs) -class PointPolygonTouches(ContainsPredicateBase): +class PointPolygonTouches(ContainsPredicate): def _preprocess(self, lhs, rhs): # Reverse argument order. equals_all = rhs._basic_equals_all(lhs) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 4ac811666..549e7d415 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -5,7 +5,6 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -14,17 +13,9 @@ ) -class WithinPredicateBase(EqualsPredicateBase): - """Base class for binary predicates that are defined in terms of a - root-level binary predicate. For example, a Point-Point Within - predicate is defined in terms of a Point-Point Contains predicate. - Used by: - (Polygon, Point) - (Polygon, MultiPoint) - (Polygon, LineString) - """ - - pass +class WithinPredicateBase(BinPred): + def _preprocess(self, lhs, rhs): + return lhs._basic_equals_all(rhs) class WithinIntersectsPredicate(BinPred): From 37daece436adb9a1977689c7bef78387397ae339 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 20:50:00 +0000 Subject: [PATCH 090/126] Cleaning up intersects and within --- .../core/binpreds/feature_intersects.py | 15 ++++- .../cuspatial/core/binpreds/feature_within.py | 67 ++++++------------- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index 5addb1428..b12c81795 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -155,13 +155,22 @@ def _preprocess(self, lhs, rhs): return intersects | contains -class PolygonPolygonIntersects(IntersectsPredicateBase): +class PolygonLineStringIntersects(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs._basic_intersects(rhs) - contains = rhs._basic_contains_any(lhs) + contains = lhs._basic_contains_any(rhs) return intersects | contains +class PolygonPolygonIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + intersects = lhs._basic_intersects(rhs) + contains_rhs = rhs._basic_contains_any(lhs) + contains_lhs = lhs._basic_contains_any(rhs) + + return intersects | contains_rhs | contains_lhs + + """ Type dispatch dictionary for intersects binary predicates. """ DispatchDict = { (Point, Point): IntersectsByEquals, @@ -178,6 +187,6 @@ def _preprocess(self, lhs, rhs): (LineString, Polygon): LineStringPolygonIntersects, (Polygon, Point): PolygonPointIntersects, (Polygon, MultiPoint): NotImplementedPredicate, - (Polygon, LineString): NotImplementedPredicate, + (Polygon, LineString): PolygonLineStringIntersects, (Polygon, Polygon): PolygonPolygonIntersects, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index b907cad81..549e7d415 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -2,17 +2,9 @@ from cuspatial.core.binpreds.binpred_interface import ( BinPred, + ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.complex_geometry_predicate import ( - ComplexGeometryPredicate, -) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase -from cuspatial.core.binpreds.feature_contains_properly import ( - ContainsProperlyPredicate, -) -from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase -from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -21,66 +13,45 @@ ) -class WithinPredicateBase(EqualsPredicateBase): - """Base class for binary predicates that are defined in terms of a - root-level binary predicate. For example, a Point-Point Within - predicate is defined in terms of a Point-Point Contains predicate. - Used by: - (Polygon, Point) - (Polygon, MultiPoint) - (Polygon, LineString) - """ - - pass +class WithinPredicateBase(BinPred): + def _preprocess(self, lhs, rhs): + return lhs._basic_equals_all(rhs) -class WithinIntersectsPredicate(IntersectsPredicateBase): +class WithinIntersectsPredicate(BinPred): def _preprocess(self, lhs, rhs): intersects = rhs._basic_intersects(lhs) equals = rhs._basic_equals(lhs) return intersects & ~equals -class PointLineStringWithin(WithinIntersectsPredicate): +class PointLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): - # Note the order of arguments is reversed. - return super()._preprocess(rhs, lhs) + intersects = lhs.intersects(rhs) + equals = lhs._basic_equals(rhs) + return intersects & ~equals -class PointPolygonWithin(ContainsPredicateBase): +class PointPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): return rhs.contains_properly(lhs) -class LineStringLineStringWithin(IntersectsPredicateBase): - def _compute_predicate(self, lhs, rhs, preprocessor_result): +class LineStringLineStringWithin(BinPred): + def _preprocess(self, lhs, rhs): intersects = rhs._basic_intersects(lhs) equals = rhs._basic_equals_all(lhs) return intersects & equals -class ComplexPolygonWithin( - ContainsProperlyPredicate, ComplexGeometryPredicate -): - """Implements within for complex polygons. Depends on contains result - for the types. - - Used by: - (MultiPoint, Polygon) - (LineString, Polygon) - (Polygon, Polygon) - """ - +class LineStringPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - # Note the order of arguments is reversed. - return super()._preprocess(rhs, lhs) + return rhs.contains(lhs) -class LineStringPolygonWithin(BinPred): +class PolygonPolygonWithin(BinPred): def _preprocess(self, lhs, rhs): - contains_all = rhs._basic_contains_all(lhs) - intersects = rhs._basic_intersects(lhs) - return contains_all & intersects + return rhs.contains(lhs) DispatchDict = { @@ -91,13 +62,13 @@ def _preprocess(self, lhs, rhs): (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): WithinIntersectsPredicate, - (MultiPoint, Polygon): ComplexPolygonWithin, - (LineString, Point): WithinIntersectsPredicate, + (MultiPoint, Polygon): PolygonPolygonWithin, + (LineString, Point): ImpossiblePredicate, (LineString, MultiPoint): WithinIntersectsPredicate, (LineString, LineString): LineStringLineStringWithin, (LineString, Polygon): LineStringPolygonWithin, (Polygon, Point): WithinPredicateBase, (Polygon, MultiPoint): WithinPredicateBase, (Polygon, LineString): WithinPredicateBase, - (Polygon, Polygon): ComplexPolygonWithin, + (Polygon, Polygon): PolygonPolygonWithin, } From 7b5211758e7b04b58f7d59c1bc13da38f4ecff9d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 21:28:16 +0000 Subject: [PATCH 091/126] Delete unused _basic functions. --- python/cuspatial/cuspatial/core/geoseries.py | 23 -------------------- 1 file changed, 23 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 9e41e288b..5d3762f03 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1452,11 +1452,6 @@ def _basic_intersects(self, other): is_sizes = self._basic_intersects_count(other) return is_sizes > 0 - def _basic_intersects_at_point_only(self, other): - """Utility method that returns True if only a single point in the lhs - geometry intersects with the rhs geometry.""" - return self._basic_intersects_count(other) == 1 - def _basic_intersects_through(self, other): """Utility method that returns True if at least two points in the lhs geometry intersect with the rhs geometry.""" @@ -1472,15 +1467,6 @@ def _basic_contains_count(self, other): contains = lhs.contains_properly(rhs, mode="basic_count") return contains - def _basic_contains_none(self, other): - """Utility method that returns True if none of the points in the lhs - geometry are contained_properly in the rhs geometry.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains_properly(rhs, mode="basic_none") - intersects = lhs._basic_intersects(other) - return contains & ~intersects - def _basic_contains_any(self, other): """Utility method that returns True if any point in the lhs geometry is contained_properly in the rhs geometry.""" @@ -1497,12 +1483,3 @@ def _basic_contains_properly_any(self, other): rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_any") return contains - - def _basic_contains_all(self, other): - """Utililty method that returns True if all points in the lhs geometry - are contained_properly in the rhs geometry. Equivalent to the public - `.contains_properly call.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains(rhs, mode="basic_all") - return contains From d7fffac34216530d4f371c4c2bcea73653d4c77a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 21:34:21 +0000 Subject: [PATCH 092/126] Remove unsupported test. --- .../test_contains_basic_predicate.py | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py index 0ad6757fd..8595cd3c6 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py @@ -5,38 +5,6 @@ import cuspatial -def test_basic_contains_none_outside(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) - rhs = cuspatial.GeoSeries([Point(2, 2)]) - got = lhs._basic_contains_none(rhs).to_pandas() - expected = [True] - assert (got == expected).all() - - -def test_basic_contains_none_inside(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) - rhs = cuspatial.GeoSeries([Point(0.5, 0.5)]) - got = lhs._basic_contains_none(rhs).to_pandas() - expected = [False] - assert (got == expected).all() - - -def test_basic_contains_none_point(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) - rhs = cuspatial.GeoSeries([Point(0, 0)]) - got = lhs._basic_contains_none(rhs).to_pandas() - expected = [False] - assert (got == expected).all() - - -def test_basic_contains_none_edge(): - lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) - rhs = cuspatial.GeoSeries([Point(0, 0.5)]) - got = lhs._basic_contains_none(rhs).to_pandas() - expected = [False] - assert (got == expected).all() - - def test_basic_contains_any_outside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(2, 2)]) From 675f145bfd93be80c46a8d906957fcc303bea238 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 21:42:36 +0000 Subject: [PATCH 093/126] Reduce the number of basic predicates. --- python/cuspatial/cuspatial/core/geoseries.py | 23 -------------------- 1 file changed, 23 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 9e41e288b..5d3762f03 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1452,11 +1452,6 @@ def _basic_intersects(self, other): is_sizes = self._basic_intersects_count(other) return is_sizes > 0 - def _basic_intersects_at_point_only(self, other): - """Utility method that returns True if only a single point in the lhs - geometry intersects with the rhs geometry.""" - return self._basic_intersects_count(other) == 1 - def _basic_intersects_through(self, other): """Utility method that returns True if at least two points in the lhs geometry intersect with the rhs geometry.""" @@ -1472,15 +1467,6 @@ def _basic_contains_count(self, other): contains = lhs.contains_properly(rhs, mode="basic_count") return contains - def _basic_contains_none(self, other): - """Utility method that returns True if none of the points in the lhs - geometry are contained_properly in the rhs geometry.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains_properly(rhs, mode="basic_none") - intersects = lhs._basic_intersects(other) - return contains & ~intersects - def _basic_contains_any(self, other): """Utility method that returns True if any point in the lhs geometry is contained_properly in the rhs geometry.""" @@ -1497,12 +1483,3 @@ def _basic_contains_properly_any(self, other): rhs = _multipoints_from_geometry(other) contains = lhs.contains_properly(rhs, mode="basic_any") return contains - - def _basic_contains_all(self, other): - """Utililty method that returns True if all points in the lhs geometry - are contained_properly in the rhs geometry. Equivalent to the public - `.contains_properly call.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains(rhs, mode="basic_all") - return contains From 2015405d396775f74c4a38749b85ee34a870555a Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 21:44:32 +0000 Subject: [PATCH 094/126] Missed a couple of ContainsPredicateBases --- python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py | 4 ++-- python/cuspatial/cuspatial/core/binpreds/feature_touches.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index 20bad01a3..b0eab48a9 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -6,7 +6,7 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.core.binpreds.feature_contains import ContainsPredicate from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, @@ -36,7 +36,7 @@ class OverlapsPredicateBase(EqualsPredicateBase): pass -class PolygonPointOverlaps(ContainsPredicateBase): +class PolygonPointOverlaps(ContainsPredicate): def _postprocess(self, lhs, rhs, op_result): if not has_same_geometry(lhs, rhs) or len(op_result.point_result) == 0: return _false_series(len(lhs)) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 8b4844577..c6935b782 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -4,7 +4,7 @@ ImpossiblePredicate, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.core.binpreds.feature_contains import ContainsPredicate from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -13,7 +13,7 @@ ) -class TouchesPredicateBase(ContainsPredicateBase): +class TouchesPredicateBase(ContainsPredicate): """Base class for binary predicates that use the contains predicate to implement the touches predicate. For example, a Point-Polygon Touches predicate is defined in terms of a Point-Polygon Contains From 069832298bd29d8392aeccaab8bc8929d6274ef2 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 21:59:52 +0000 Subject: [PATCH 095/126] Updating comments for confusing sections. --- .../core/binpreds/contains_geometry_processor.py | 16 ++++++++-------- .../cuspatial/core/binpreds/feature_contains.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index 481222939..055913587 100644 --- a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -40,11 +40,10 @@ def _preprocess_multi(self, lhs, rhs): Returns ------- - result : cudf.Series - A cudf.Series of boolean values indicating whether each feature in - the right-hand GeoSeries satisfies the requirements of the point- - in-polygon basic predicate with its corresponding feature in the - left-hand GeoSeries. + result : PreprocessorResult + A PreprocessorResult object containing the original lhs and rhs, + the rhs with only its points, and the indices of the points in + the original rhs. """ # RHS conditioning: point_indices = None @@ -63,8 +62,7 @@ def _preprocess_multi(self, lhs, rhs): geom = rhs.points xy_points = geom.xy - # Arrange into shape for calling point-in-polygon, intersection, or - # equals + # Arrange into shape for calling point-in-polygon point_indices = geom.point_indices() from cuspatial.core.geoseries import GeoSeries @@ -123,7 +121,9 @@ def _convert_quadtree_result_from_part_to_polygon_indices( def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: """Prepare the allpairs result of a contains_properly call as - the first step of postprocessing. + the first step of postprocessing. An allpairs result is reindexed + by replacing the polygon index with the original index of the + polygon from the lhs. Parameters ---------- diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 2f1ccb2e0..ec5f54f34 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -50,10 +50,17 @@ def _intersection_results_for_contains(self, lhs, rhs): pli_offsets = cudf.Series(pli[0]) + # Convert the pli to multipoints for equality checking multipoints = _points_and_lines_to_multipoints( pli_features, pli_offsets ) + # A point in the rhs can be one of three possible states: + # 1. It is in the interior of the lhs + # 2. It is in the exterior of the lhs + # 3. It is on the boundary of the lhs + # This function tests if the point in the rhs is in the boundary + # of the lhs intersect_equals_count = rhs._basic_equals_count(multipoints) return intersect_equals_count @@ -61,6 +68,9 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): lines_rhs = _open_polygon_rings(rhs) contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, lines_rhs) + # A closed polygon has an extra line segment that is not used in + # counting the number of points. We need to subtract this from the + # number of points in the polygon. polygon_size_reduction = rhs.polygons.part_offset.take( rhs.polygons.geometry_offset[1:] ) - rhs.polygons.part_offset.take(rhs.polygons.geometry_offset[:-1]) @@ -97,7 +107,8 @@ def _compute_polygon_linestring_contains( def _compute_predicate(self, lhs, rhs, preprocessor_result): if contains_only_points(rhs): # Special case in GeoPandas, points are not contained - # in the boundary of a polygon. + # in the boundary of a polygon, so only return true if + # the points are contained_properly. contains = lhs._basic_contains_count(rhs).reset_index(drop=True) return contains > 0 elif contains_only_linestrings(rhs): From 4fd668e1ef510d9b2bcb9f1ec59e216be057d5c3 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 22:04:15 +0000 Subject: [PATCH 096/126] Begin cleaning up intersects. --- .../cuspatial/core/binpreds/contains_geometry_processor.py | 2 ++ .../cuspatial/core/binpreds/feature_contains_properly.py | 2 -- .../cuspatial/cuspatial/core/binpreds/feature_intersects.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index 055913587..670d81d4c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -239,6 +239,8 @@ def _postprocess_multi( return count_result # Handling for full contains (equivalent to basic predicate all) + # for each input pair i: result[i] =  true iff point[i] is + # contained in at least one polygon of multipolygon[i]. result_df["feature_in_polygon"] = ( result_df["point_index_x"] >= result_df["point_index_y"] ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index cc127b49e..546624125 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -140,8 +140,6 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): polygon. """ - # for each input pair i: result[i] =  true iff point[i] is - # contained in at least one polygon of multipolygon[i]. if _is_complex(rhs): return super()._postprocess_multi( lhs, rhs, preprocessor_result, op_result, mode=self.config.mode diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index b12c81795..d7bd9149b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -87,14 +87,14 @@ class IntersectsByEquals(EqualsPredicateBase): pass -class PolygonPointIntersects(IntersectsPredicateBase): +class PolygonPointIntersects(BinPred): def _preprocess(self, lhs, rhs): contains = lhs._basic_contains_any(rhs) intersects = lhs._basic_intersects(rhs) return contains | intersects -class PointPolygonIntersects(IntersectsPredicateBase): +class PointPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): contains = rhs._basic_contains_any(lhs) intersects = rhs._basic_intersects(lhs) From 3de22b31a5954a9eb5adcbee7142ab0f0f47d91f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 27 Apr 2023 22:12:43 +0000 Subject: [PATCH 097/126] Theoretically well refactored intersects. --- .../core/binpreds/feature_intersects.py | 39 ++----------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index d7bd9149b..df5b571c6 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -5,7 +5,6 @@ import cudf -import cuspatial from cuspatial.core.binops.intersection import pairwise_linestring_intersection from cuspatial.core.binpreds.binpred_interface import ( BinPred, @@ -20,6 +19,7 @@ Point, Polygon, _false_series, + _linestrings_from_geometry, ) @@ -105,24 +105,7 @@ class LineStringPointIntersects(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): """Convert rhs to linestrings by making a linestring that has the same start and end point.""" - x = cp.repeat(rhs.points.x, 2) - y = cp.repeat(rhs.points.y, 2) - xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() - parts = cp.arange((len(lhs) + 1)) * 2 - geometries = cp.arange(len(lhs) + 1) - ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) - return self._compute_predicate( - lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) - ) - - -class LineStringMultiPointIntersects(IntersectsPredicateBase): - def _preprocess(self, lhs, rhs): - """Convert rhs to linestrings.""" - xy = rhs.multipoints.xy - parts = rhs.multipoints.geometry_offset - geometries = cp.arange(len(lhs) + 1) - ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) + ls_rhs = _linestrings_from_geometry(rhs) return self._compute_predicate( lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) ) @@ -134,21 +117,7 @@ def _preprocess(self, lhs, rhs): return super()._preprocess(rhs, lhs) -class LineStringPointIntersects(IntersectsPredicateBase): - def _preprocess(self, lhs, rhs): - """Convert rhs to linestrings.""" - x = cp.repeat(rhs.points.x, 2) - y = cp.repeat(rhs.points.y, 2) - xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() - parts = cp.arange((len(lhs) + 1)) * 2 - geometries = cp.arange(len(lhs) + 1) - ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) - return self._compute_predicate( - lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) - ) - - -class LineStringPolygonIntersects(IntersectsPredicateBase): +class LineStringPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs._basic_intersects(rhs) contains = rhs._basic_contains_any(lhs) @@ -182,7 +151,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, (LineString, Point): LineStringPointIntersects, - (LineString, MultiPoint): LineStringMultiPointIntersects, + (LineString, MultiPoint): LineStringPointIntersects, (LineString, LineString): IntersectsPredicateBase, (LineString, Polygon): LineStringPolygonIntersects, (Polygon, Point): PolygonPointIntersects, From 643309609c8aaec7baa1f6d71e982f3bbf60be2d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 13:46:38 +0000 Subject: [PATCH 098/126] Add another linestring example that proves touches. --- .../cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 8d4769995..b8b93651d 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -453,6 +453,7 @@ def predicate(request): "linestring-linestring-disjoint", "linestring-linestring-same", "linestring-linestring-touches", + "linestring-linestring-touch-interior", "linestring-linestring-crosses", ] From 32a18de6d23006a6df2f03439f9ef01d451bf787 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 20:27:07 +0000 Subject: [PATCH 099/126] Add complex polygons test to .contains test. It passes! --- .../cuspatial/tests/binpreds/test_contains.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py index 274c96165..fe8197b37 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_contains.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_contains.py @@ -8,6 +8,28 @@ import cuspatial +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(rhs).values_host + expected = gpdlhs.contains(gpdrhs).values + assert (got == expected).all() + got = rhs.contains(lhs).values_host + expected = gpdrhs.contains(gpdlhs).values + assert (got == expected).all() + + def test_same(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) From 9e65cb467c47763d66e448e4f1b47714d2600b99 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 15:37:41 -0500 Subject: [PATCH 100/126] Update python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py Co-authored-by: Michael Wang --- .../core/binpreds/feature_contains_properly.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 546624125..69fa38ec4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -95,14 +95,10 @@ def _compute_predicate( raise TypeError( "`.contains` can only be called with polygon series." ) - if self._should_use_quadtree(lhs): - pip_result = contains_properly( - lhs, preprocessor_result.final_rhs, how="quadtree" - ) - else: - pip_result = contains_properly( - lhs, preprocessor_result.final_rhs, how="byte-limited" - ) + how = "quadtree" if self._should_use_quadtree(lhs) else "byte-limited" + pip_result = contains_properly( + lhs, preprocessor_result.final_rhs, how=how + ) op_result = ContainsOpResult(pip_result, preprocessor_result) return self._postprocess(lhs, rhs, preprocessor_result, op_result) From 8a1d2342c6c4a46afc2ef7b300fd313bb35decd5 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 20:42:26 +0000 Subject: [PATCH 101/126] Handle review comments. --- .../core/binpreds/contains_geometry_processor.py | 14 +++++++------- .../cuspatial/core/binpreds/feature_contains.py | 2 +- .../core/binpreds/feature_contains_properly.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index 670d81d4c..18038e748 100644 --- a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -1,7 +1,5 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from typing import Union - import cupy as cp import cudf @@ -26,7 +24,7 @@ class ContainsGeometryProcessor(BinPred): - def _preprocess_multi(self, lhs, rhs): + def _preprocess_multipoint_rhs(self, lhs, rhs): """Flatten any rhs into only its points xy array. This is necessary because the basic predicate for contains, point-in-polygon, only accepts points. @@ -119,7 +117,7 @@ def _convert_quadtree_result_from_part_to_polygon_indices( ["polygon_index", "point_index"] ] - def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: + def _reindex_allpairs(self, lhs, op_result) -> DataFrame: """Prepare the allpairs result of a contains_properly call as the first step of postprocessing. An allpairs result is reindexed by replacing the polygon index with the original index of the @@ -159,7 +157,7 @@ def _reindex_allpairs(self, lhs, op_result) -> Union[Series, DataFrame]: return allpairs_result - def _postprocess_multi( + def _postprocess_multipoint_rhs( self, lhs, rhs, preprocessor_result, op_result, mode ): """Reconstruct the original geometry from the result of the @@ -251,8 +249,10 @@ def _postprocess_multi( return final_result def _postprocess_points(self, lhs, rhs, preprocessor_result, op_result): - """Reconstruct the original geometry from the result of the - contains_properly call. Used when the rhs is naturally points. + """Used when the rhs is naturally points. Instead of reconstructing + the original geometry, this method applies the `point_index` results + to the original rhs points and returns a boolean series reflecting + which `point_index`es were found. """ allpairs_result = self._reindex_allpairs(lhs, op_result) if self.config.allpairs: diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index ec5f54f34..06ed185e4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -39,7 +39,7 @@ def __init__(self, **kwargs): self.config.mode = kwargs.get("mode", "full") def _preprocess(self, lhs, rhs): - preprocessor_result = super()._preprocess_multi(lhs, rhs) + preprocessor_result = super()._preprocess_multipoint_rhs(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) def _intersection_results_for_contains(self, lhs, rhs): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 69fa38ec4..8b981ba45 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -55,7 +55,7 @@ def __init__(self, **kwargs): self.config.mode = kwargs.get("mode", "full") def _preprocess(self, lhs, rhs): - preprocessor_result = super()._preprocess_multi(lhs, rhs) + preprocessor_result = super()._preprocess_multipoint_rhs(lhs, rhs) return self._compute_predicate(lhs, rhs, preprocessor_result) def _should_use_quadtree(self, lhs): @@ -137,7 +137,7 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): """ if _is_complex(rhs): - return super()._postprocess_multi( + return super()._postprocess_multipoint_rhs( lhs, rhs, preprocessor_result, op_result, mode=self.config.mode ) else: From c4339494d4f7172c86b3a5f90758453d75fa4a9b Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 20:53:43 +0000 Subject: [PATCH 102/126] Drop one useless inheritance. --- python/cuspatial/cuspatial/core/binpreds/feature_intersects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index df5b571c6..e9c8b57d8 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -131,7 +131,7 @@ def _preprocess(self, lhs, rhs): return intersects | contains -class PolygonPolygonIntersects(IntersectsPredicateBase): +class PolygonPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs._basic_intersects(rhs) contains_rhs = rhs._basic_contains_any(lhs) From 24e4d9af94caf019dc44856484ad27fb5396525f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 15:54:50 -0500 Subject: [PATCH 103/126] 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 5d3762f03..fec0e1f46 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -168,7 +168,7 @@ def point_indices(self): @property def sizes(self): - """Returns the size in points of each geometry in the GeoSeries." + """Returns the number of points of each geometry in the GeoSeries." Returns ------- From a86fb2bf53717c72011a4788c2823726b929f407 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 21:01:58 +0000 Subject: [PATCH 104/126] Change error type of mixed geometry geoseries.sizes call. --- 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 5d3762f03..87e865a52 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -210,7 +210,7 @@ def sizes(self): else: if len(self) == 0: return cudf.Series([0], dtype="int32") - raise TypeError( + raise NotImplementedError( "GeoSeries must contain only Points, MultiPoints, Lines, or " "Polygons to return sizes." ) From c813ba7aae9d0bf7ad72d1e14c262f56e86ccd01 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 21:12:17 +0000 Subject: [PATCH 105/126] Remove local imports from _basic_predicates. --- .../cuspatial/cuspatial/core/binops/equals_count.py | 3 +-- python/cuspatial/cuspatial/core/geoseries.py | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binops/equals_count.py b/python/cuspatial/cuspatial/core/binops/equals_count.py index 80f63027e..83cd97b9e 100644 --- a/python/cuspatial/cuspatial/core/binops/equals_count.py +++ b/python/cuspatial/cuspatial/core/binops/equals_count.py @@ -5,11 +5,10 @@ from cuspatial._lib.pairwise_multipoint_equals_count import ( pairwise_multipoint_equals_count as c_pairwise_multipoint_equals_count, ) -from cuspatial.core.geoseries import GeoSeries from cuspatial.utils.column_utils import contains_only_multipoints -def pairwise_multipoint_equals_count(lhs: GeoSeries, rhs: GeoSeries): +def pairwise_multipoint_equals_count(lhs, rhs): """Compute the number of points in each multipoint in the lhs that exist in the corresponding multipoint in the rhs. diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 193add145..36f239e7f 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -26,6 +26,7 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import ColumnType, GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta +from cuspatial.core.binops.equals_count import pairwise_multipoint_equals_count from cuspatial.core.binpreds.binpred_dispatch import ( CONTAINS_DISPATCH, CONTAINS_PROPERLY_DISPATCH, @@ -1383,10 +1384,6 @@ def touches(self, other, align=True): def _basic_equals(self, other): """Utility method that returns True if any point in the lhs geometry is equal to a point in the rhs geometry.""" - from cuspatial.core.binops.equals_count import ( - pairwise_multipoint_equals_count, - ) - lhs = _multipoints_from_geometry(self) rhs = _multipoints_from_geometry(other) result = pairwise_multipoint_equals_count(lhs, rhs) @@ -1395,10 +1392,6 @@ def _basic_equals(self, other): def _basic_equals_all(self, other): """Utility method that returns True if all points in the lhs geometry are equal to points in the rhs geometry.""" - from cuspatial.core.binops.equals_count import ( - pairwise_multipoint_equals_count, - ) - lhs = _multipoints_from_geometry(self) rhs = _multipoints_from_geometry(other) result = pairwise_multipoint_equals_count(lhs, rhs) @@ -1411,10 +1404,6 @@ def _basic_equals_all(self, other): def _basic_equals_count(self, other): """Utility method that returns the number of points in the lhs geometry that are equal to a point in the rhs geometry.""" - from cuspatial.core.binops.equals_count import ( - pairwise_multipoint_equals_count, - ) - lhs = _multipoints_from_geometry(self) rhs = _multipoints_from_geometry(other) result = pairwise_multipoint_equals_count(lhs, rhs) From c3d03cbf7fbf39796fbcc7009e9ba04a385aefd0 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 21:27:49 +0000 Subject: [PATCH 106/126] Get rid of intesrsects_through --- python/cuspatial/cuspatial/core/geoseries.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 36f239e7f..b7d8ff33c 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1441,12 +1441,6 @@ def _basic_intersects(self, other): is_sizes = self._basic_intersects_count(other) return is_sizes > 0 - def _basic_intersects_through(self, other): - """Utility method that returns True if at least two points in the lhs - geometry intersect with the rhs geometry.""" - is_sizes = self._basic_intersects_count(other) - return is_sizes > 1 - def _basic_contains_count(self, other): """Utility method that returns the number of points in the lhs geometry that are contained_properly in the rhs geometry. From a2bf9d33e358a406d4661922e6061033156e3184 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Fri, 28 Apr 2023 18:15:39 -0500 Subject: [PATCH 107/126] Get rid of through call --- python/cuspatial/cuspatial/core/binpreds/feature_crosses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index a30ed051b..e1e915bcd 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -40,7 +40,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): - intersects = rhs._basic_intersects_through(lhs) + intersects = rhs._basic_intersects_count(lhs) > 1 touches = rhs.touches(lhs) contains = rhs.contains(lhs) return ~touches & intersects & ~contains From 92c918c84fdda6c3163bfaa8eb39e65b0371638f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 15:21:10 +0000 Subject: [PATCH 108/126] Move _basic_predicates into a dedicated file. Handle various review comments. --- .../core/binpreds/basic_predicates.py | 107 ++++++++++++++++++ .../binpreds/contains_geometry_processor.py | 8 +- .../core/binpreds/feature_contains.py | 27 +++-- .../binpreds/feature_contains_properly.py | 16 +-- .../core/binpreds/feature_intersects.py | 26 +++-- .../cuspatial/core/binpreds/feature_within.py | 17 ++- python/cuspatial/cuspatial/core/geoseries.py | 94 --------------- 7 files changed, 163 insertions(+), 132 deletions(-) create mode 100644 python/cuspatial/cuspatial/core/binpreds/basic_predicates.py diff --git a/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py b/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py new file mode 100644 index 000000000..399eed58c --- /dev/null +++ b/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py @@ -0,0 +1,107 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +import cudf + +from cuspatial.core.binops.equals_count import pairwise_multipoint_equals_count +from cuspatial.utils.binpred_utils import ( + _linestrings_from_geometry, + _multipoints_from_geometry, + _multipoints_is_degenerate, + _points_and_lines_to_multipoints, + _zero_series, +) + + +def _basic_equals(lhs, rhs): + """Utility method that returns True if any point in the lhs geometry + is equal to a point in the rhs geometry.""" + lhs = _multipoints_from_geometry(lhs) + rhs = _multipoints_from_geometry(rhs) + result = pairwise_multipoint_equals_count(lhs, rhs) + return result > 0 + + +def _basic_equals_all(lhs, rhs): + """Utility method that returns True if all points in the lhs geometry + are equal to points in the rhs geometry.""" + lhs = _multipoints_from_geometry(lhs) + rhs = _multipoints_from_geometry(rhs) + result = pairwise_multipoint_equals_count(lhs, rhs) + sizes = ( + lhs.multipoints.geometry_offset[1:] + - lhs.multipoints.geometry_offset[:-1] + ) + return result == sizes + + +def _basic_equals_count(lhs, rhs): + """Utility method that returns the number of points in the lhs geometry + that are equal to a point in the rhs geometry.""" + lhs = _multipoints_from_geometry(lhs) + rhs = _multipoints_from_geometry(rhs) + result = pairwise_multipoint_equals_count(lhs, rhs) + return result + + +def _basic_intersects_pli(lhs, rhs): + """Utility method that returns the original results of + `pairwise_linestring_intersection` (pli).""" + from cuspatial.core.binops.intersection import ( + pairwise_linestring_intersection, + ) + + lhs = _linestrings_from_geometry(lhs) + rhs = _linestrings_from_geometry(rhs) + return pairwise_linestring_intersection(lhs, rhs) + + +def _basic_intersects_count(lhs, rhs): + """Utility method that returns the number of points in the lhs geometry + that intersect with the rhs geometry.""" + pli = _basic_intersects_pli(lhs, rhs) + if len(pli[1]) == 0: + return _zero_series(len(rhs)) + intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) + sizes = cudf.Series(intersections.sizes) + # If the result is degenerate + is_degenerate = _multipoints_is_degenerate(intersections) + # If all the points in the intersection are in the rhs + if len(is_degenerate) > 0: + sizes[is_degenerate] = 1 + return sizes + + +def _basic_intersects(lhs, rhs): + """Utility method that returns True if any point in the lhs geometry + intersects with the rhs geometry.""" + is_sizes = _basic_intersects_count(lhs, rhs) + return is_sizes > 0 + + +def _basic_contains_count(lhs, rhs): + """Utility method that returns the number of points in the lhs geometry + that are contained_properly in the rhs geometry. + """ + lhs = lhs + rhs = _multipoints_from_geometry(rhs) + contains = lhs.contains_properly(rhs, mode="basic_count") + return contains + + +def _basic_contains_any(lhs, rhs): + """Utility method that returns True if any point in the lhs geometry + is contained_properly in the rhs geometry.""" + lhs = lhs + rhs = _multipoints_from_geometry(rhs) + contains = lhs.contains_properly(rhs, mode="basic_any") + intersects = _basic_intersects(lhs, rhs) + return contains | intersects + + +def _basic_contains_properly_any(lhs, rhs): + """Utility method that returns True if any point in the lhs geometry + is contained_properly in the rhs geometry.""" + lhs = lhs + rhs = _multipoints_from_geometry(rhs) + contains = lhs.contains_properly(rhs, mode="basic_any") + return contains diff --git a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py index 18038e748..12b2fc37d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains_geometry_processor.py @@ -182,14 +182,14 @@ def _postprocess_multipoint_rhs( in the right-hand GeoSeries is contained by the corresponding feature in the left-hand GeoSeries. If `basic_none` is specified, `.contains` or .contains_properly` returns the - inverse of `full`. If `basic_any` is specified, `.contains` or - .contains_properly` returns a boolean series indicating + negation of `basic_any`.`. If `basic_any` is specified, `.contains` + or `.contains_properly` returns a boolean series indicating whether any point in the right-hand GeoSeries is contained by the corresponding feature in the left-hand GeoSeries. If the `basic_count` option is specified, `.contains` or - .contains_properly` returns a series of integers indicating + .contains_properly` returns a Series of integers indicating the number of points in the right-hand GeoSeries that are - contained by the corresponding feature in the left-hand + contained by the corresponding feature in the left-hand GeoSeries. Returns ------- diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 06ed185e4..d576930bf 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -4,6 +4,13 @@ import cudf +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_count, + _basic_equals, + _basic_equals_count, + _basic_intersects, + _basic_intersects_pli, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -43,7 +50,7 @@ def _preprocess(self, lhs, rhs): return self._compute_predicate(lhs, rhs, preprocessor_result) def _intersection_results_for_contains(self, lhs, rhs): - pli = lhs._basic_intersects_pli(rhs) + pli = _basic_intersects_pli(lhs, rhs) pli_features = pli[1] if len(pli_features) == 0: return _zero_series(len(lhs)) @@ -61,12 +68,12 @@ def _intersection_results_for_contains(self, lhs, rhs): # 3. It is on the boundary of the lhs # This function tests if the point in the rhs is in the boundary # of the lhs - intersect_equals_count = rhs._basic_equals_count(multipoints) + intersect_equals_count = _basic_equals_count(rhs, multipoints) return intersect_equals_count def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): lines_rhs = _open_polygon_rings(rhs) - contains = lhs._basic_contains_count(lines_rhs).reset_index(drop=True) + contains = _basic_contains_count(lhs, lines_rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, lines_rhs) # A closed polygon has an extra line segment that is not used in # counting the number of points. We need to subtract this from the @@ -79,7 +86,7 @@ def _compute_polygon_polygon_contains(self, lhs, rhs, preprocessor_result): def _compute_polygon_linestring_contains( self, lhs, rhs, preprocessor_result ): - contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + contains = _basic_contains_count(lhs, rhs).reset_index(drop=True) intersects = self._intersection_results_for_contains(lhs, rhs) if (contains == 0).all() and (intersects != 0).all(): # The hardest case. We need to check if the linestring is @@ -95,7 +102,7 @@ def _compute_polygon_linestring_contains( ) size_two_results = _false_series(len(lhs)) size_two_results[rhs.sizes == 2] = ( - lhs._basic_contains_count(center_points) > 0 + _basic_contains_count(lhs, center_points) > 0 ) return size_two_results else: @@ -109,7 +116,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): # Special case in GeoPandas, points are not contained # in the boundary of a polygon, so only return true if # the points are contained_properly. - contains = lhs._basic_contains_count(rhs).reset_index(drop=True) + contains = _basic_contains_count(lhs, rhs).reset_index(drop=True) return contains > 0 elif contains_only_linestrings(rhs): return self._compute_polygon_linestring_contains( @@ -125,19 +132,19 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class PointPointContains(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_equals(rhs) + return _basic_equals(lhs, rhs) class LineStringPointContains(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - equals = lhs._basic_equals(rhs) + intersects = _basic_intersects(lhs, rhs) + equals = _basic_equals(lhs, rhs) return intersects & ~equals class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): - count = lhs._basic_equals_count(rhs) + count = _basic_equals_count(lhs, rhs) return count == rhs.sizes diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 8b981ba45..b9d00d9e4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -2,6 +2,10 @@ from typing import TypeVar +from cuspatial.core.binpreds.basic_predicates import ( + _basic_equals_all, + _basic_intersects, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ContainsOpResult, @@ -70,12 +74,12 @@ def _should_use_quadtree(self, lhs): ----- 1. Quadtree is always used if user requests `allpairs=True`. 2. If the number of polygons in the lhs is less than 32, we use the - byte-limited algorithm because it is faster and has less memory + brute-force algorithm because it is faster and has less memory overhead. 3. If the lhs contains more than 32 polygons, we use the quadtree because it does not have a polygon-count limit. 4. If the lhs contains multipolygons, we use quadtree because the - performance between quadtree and byte-limited is similar, but + performance between quadtree and brute-force is similar, but code complexity would be higher if we did multipolygon reconstruction on both code paths. """ @@ -95,7 +99,7 @@ def _compute_predicate( raise TypeError( "`.contains` can only be called with polygon series." ) - how = "quadtree" if self._should_use_quadtree(lhs) else "byte-limited" + how = "quadtree" if self._should_use_quadtree(lhs) else "brute-force" pip_result = contains_properly( lhs, preprocessor_result.final_rhs, how=how ) @@ -109,8 +113,6 @@ def _postprocess(self, lhs, rhs, preprocessor_result, op_result): Postprocess for contains_properly has to handle multiple input and output configurations. - The input can be a single polygon, a single multipolygon, or a - GeoSeries containing a mix of polygons and multipolygons. The input to postprocess is `point_indices`, which can be either a cudf.DataFrame with one row per point and one column per polygon or @@ -155,12 +157,12 @@ class ContainsProperlyByIntersection(BinPred): """ def _preprocess(self, lhs, rhs): - return lhs._basic_intersects(rhs) + return _basic_intersects(lhs, rhs) class LineStringLineStringContainsProperly(BinPred): def _preprocess(self, lhs, rhs): - count = lhs._basic_equals_all(rhs) + count = _basic_equals_all(lhs, rhs) return count diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index e9c8b57d8..d8ecfdb38 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -6,6 +6,10 @@ import cudf from cuspatial.core.binops.intersection import pairwise_linestring_intersection +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_any, + _basic_intersects, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, IntersectsOpResult, @@ -89,15 +93,15 @@ class IntersectsByEquals(EqualsPredicateBase): class PolygonPointIntersects(BinPred): def _preprocess(self, lhs, rhs): - contains = lhs._basic_contains_any(rhs) - intersects = lhs._basic_intersects(rhs) + contains = _basic_contains_any(lhs, rhs) + intersects = _basic_intersects(lhs, rhs) return contains | intersects class PointPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): - contains = rhs._basic_contains_any(lhs) - intersects = rhs._basic_intersects(lhs) + contains = _basic_contains_any(rhs, lhs) + intersects = _basic_intersects(rhs, lhs) return contains | intersects @@ -119,23 +123,23 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains = rhs._basic_contains_any(lhs) + intersects = _basic_intersects(lhs, rhs) + contains = _basic_contains_any(rhs, lhs) return intersects | contains class PolygonLineStringIntersects(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains = lhs._basic_contains_any(rhs) + intersects = _basic_intersects(lhs, rhs) + contains = _basic_contains_any(lhs, rhs) return intersects | contains class PolygonPolygonIntersects(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains_rhs = rhs._basic_contains_any(lhs) - contains_lhs = lhs._basic_contains_any(rhs) + intersects = _basic_intersects(lhs, rhs) + contains_rhs = _basic_contains_any(rhs, lhs) + contains_lhs = _basic_contains_any(lhs, rhs) return intersects | contains_rhs | contains_lhs diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 549e7d415..043f4629e 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -1,5 +1,10 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from cuspatial.core.binpreds.basic_predicates import ( + _basic_equals, + _basic_equals_all, + _basic_intersects, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -15,20 +20,20 @@ class WithinPredicateBase(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_equals_all(rhs) + return _basic_equals_all(lhs, rhs) class WithinIntersectsPredicate(BinPred): def _preprocess(self, lhs, rhs): - intersects = rhs._basic_intersects(lhs) - equals = rhs._basic_equals(lhs) + intersects = _basic_intersects(rhs, lhs) + equals = _basic_equals(rhs, lhs) return intersects & ~equals class PointLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs.intersects(rhs) - equals = lhs._basic_equals(rhs) + equals = _basic_equals(lhs, rhs) return intersects & ~equals @@ -39,8 +44,8 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): - intersects = rhs._basic_intersects(lhs) - equals = rhs._basic_equals_all(lhs) + intersects = _basic_intersects(rhs, lhs) + equals = _basic_equals_all(rhs, lhs) return intersects & equals diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index b7d8ff33c..2b42dab68 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -26,7 +26,6 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import ColumnType, GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.binops.equals_count import pairwise_multipoint_equals_count from cuspatial.core.binpreds.binpred_dispatch import ( CONTAINS_DISPATCH, CONTAINS_PROPERLY_DISPATCH, @@ -39,13 +38,6 @@ TOUCHES_DISPATCH, WITHIN_DISPATCH, ) -from cuspatial.utils.binpred_utils import ( - _linestrings_from_geometry, - _multipoints_from_geometry, - _multipoints_is_degenerate, - _points_and_lines_to_multipoints, - _zero_series, -) from cuspatial.utils.column_utils import ( contains_only_linestrings, contains_only_multipoints, @@ -1380,89 +1372,3 @@ def touches(self, other, align=True): align=align ) return predicate(self, other) - - def _basic_equals(self, other): - """Utility method that returns True if any point in the lhs geometry - is equal to a point in the rhs geometry.""" - lhs = _multipoints_from_geometry(self) - rhs = _multipoints_from_geometry(other) - result = pairwise_multipoint_equals_count(lhs, rhs) - return result > 0 - - def _basic_equals_all(self, other): - """Utility method that returns True if all points in the lhs geometry - are equal to points in the rhs geometry.""" - lhs = _multipoints_from_geometry(self) - rhs = _multipoints_from_geometry(other) - result = pairwise_multipoint_equals_count(lhs, rhs) - sizes = ( - lhs.multipoints.geometry_offset[1:] - - lhs.multipoints.geometry_offset[:-1] - ) - return result == sizes - - def _basic_equals_count(self, other): - """Utility method that returns the number of points in the lhs geometry - that are equal to a point in the rhs geometry.""" - lhs = _multipoints_from_geometry(self) - rhs = _multipoints_from_geometry(other) - result = pairwise_multipoint_equals_count(lhs, rhs) - return result - - def _basic_intersects_pli(self, other): - """Utility method that returns the original results of - `pairwise_linestring_intersection`.""" - from cuspatial.core.binops.intersection import ( - pairwise_linestring_intersection, - ) - - lhs = _linestrings_from_geometry(self) - rhs = _linestrings_from_geometry(other) - return pairwise_linestring_intersection(lhs, rhs) - - def _basic_intersects_count(self, other): - """Utility method that returns the number of points in the lhs geometry - that intersect with the rhs geometry.""" - pli = self._basic_intersects_pli(other) - if len(pli[1]) == 0: - return _zero_series(len(other)) - intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) - sizes = cudf.Series(intersections.sizes) - # If the result is degenerate - is_degenerate = _multipoints_is_degenerate(intersections) - # If all the points in the intersection are in the rhs - if len(is_degenerate) > 0: - sizes[is_degenerate] = 1 - return sizes - - def _basic_intersects(self, other): - """Utility method that returns True if any point in the lhs geometry - intersects with the rhs geometry.""" - is_sizes = self._basic_intersects_count(other) - return is_sizes > 0 - - def _basic_contains_count(self, other): - """Utility method that returns the number of points in the lhs geometry - that are contained_properly in the rhs geometry. - """ - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains_properly(rhs, mode="basic_count") - return contains - - def _basic_contains_any(self, other): - """Utility method that returns True if any point in the lhs geometry - is contained_properly in the rhs geometry.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains_properly(rhs, mode="basic_any") - intersects = lhs._basic_intersects(other) - return contains | intersects - - def _basic_contains_properly_any(self, other): - """Utility method that returns True if any point in the lhs geometry - is contained_properly in the rhs geometry.""" - lhs = self - rhs = _multipoints_from_geometry(other) - contains = lhs.contains_properly(rhs, mode="basic_any") - return contains From 86dd5b34c73d584d664a7ee34cda014f2b11cb88 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 10:21:48 -0500 Subject: [PATCH 109/126] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- python/cuspatial/cuspatial/core/geoseries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index b7d8ff33c..17998ac54 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -179,8 +179,7 @@ def sizes(self): Notes ----- The size of a geometry is the number of points it contains. - The size of a polygon is the number of points in its exterior ring - plus the number of points in its interior rings. + The size of a polygon is the total number of points in all of its rings. The size of a multipolygon is the sum of all its polygons. The size of a linestring is the number of points in its single line. The size of a multilinestring is the sum of all its linestrings. From 4f6f8a80b3cf1b7da9e6d5353303970e6a1b0ced Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 10:22:19 -0500 Subject: [PATCH 110/126] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- 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 17998ac54..0e2b0ad3d 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -180,7 +180,7 @@ def sizes(self): ----- The size of a geometry is the number of points it contains. The size of a polygon is the total number of points in all of its rings. - The size of a multipolygon is the sum of all its polygons. + The size of a multipolygon is the sum of the sizes of all of its polygons. The size of a linestring is the number of points in its single line. The size of a multilinestring is the sum of all its linestrings. The size of a multipoint is the number of points in its single point. From c14b4ca2f13e25ab8e1fedad451b2f96973ee553 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 10:22:35 -0500 Subject: [PATCH 111/126] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- 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 0e2b0ad3d..5cda4924a 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -182,7 +182,7 @@ def sizes(self): The size of a polygon is the total number of points in all of its rings. The size of a multipolygon is the sum of the sizes of all of its polygons. The size of a linestring is the number of points in its single line. - The size of a multilinestring is the sum of all its linestrings. + The size of a multilinestring is the sum of the sizes of all of its linestrings. The size of a multipoint is the number of points in its single point. The size of a point is 1. """ From 72ad8c9a94d7e42ad0bdc2c2c98c8bcdbefb78e1 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 10:48:44 -0500 Subject: [PATCH 112/126] Update contains predicate docs. --- python/cuspatial/cuspatial/core/geoseries.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index d7f0a2de2..49a975752 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -171,10 +171,13 @@ def sizes(self): Notes ----- The size of a geometry is the number of points it contains. - The size of a polygon is the total number of points in all of its rings. - The size of a multipolygon is the sum of the sizes of all of its polygons. + The size of a polygon is the total number of points in all of its + rings. + The size of a multipolygon is the sum of the sizes of all of its + polygons. The size of a linestring is the number of points in its single line. - The size of a multilinestring is the sum of the sizes of all of its linestrings. + The size of a multilinestring is the sum of the sizes of all of its + linestrings. The size of a multipoint is the number of points in its single point. The size of a point is 1. """ @@ -998,9 +1001,9 @@ def contains(self, other, align=False, allpairs=False, mode="full"): """Returns a `Series` of `dtype('bool')` with value `True` for each aligned geometry that contains _other_. - Compute from a GeoSeries of points and a GeoSeries of polygons which - points are contained within the corresponding polygon. Polygon A - contains Point B if B is within the interior or on the boundary of A. + An object `a` is said to contain `b` if `b`'s `boundary` and + `interiors` are within those of `a` and no point of `b` lies in the + exterior of `a`. If `allpairs=False`, the result will be a `Series` of `dtype('bool')`. If `allpairs=True`, the result will be a `DataFrame` containing two From 6c6f097c02a1df5f49b44061c5c65bc476df579c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 16:07:30 +0000 Subject: [PATCH 113/126] Refactor use of quadtree and language. --- .../cuspatial/cuspatial/core/binpreds/contains.py | 14 +++++--------- .../core/binpreds/feature_contains_properly.py | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/contains.py b/python/cuspatial/cuspatial/core/binpreds/contains.py index 51ced0031..398f134ff 100644 --- a/python/cuspatial/cuspatial/core/binpreds/contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/contains.py @@ -69,7 +69,7 @@ def _quadtree_contains_properly(points, polygons): return polygons_and_points -def _byte_limited_contains_properly(points, polygons): +def _brute_force_contains_properly(points, polygons): """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 @@ -115,14 +115,14 @@ def _byte_limited_contains_properly(points, polygons): return final_result -def contains_properly(polygons, points, how="quadtree"): - if "quadtree" == how: +def contains_properly(polygons, points, quadtree=True): + if quadtree: return _quadtree_contains_properly(points, polygons) - elif "byte-limited" == how: + else: # Use stack to convert the result to the same shape as quadtree's # result, name the columns appropriately, and return the # two-column DataFrame. - bitmask_result = _byte_limited_contains_properly(points, polygons) + bitmask_result = _brute_force_contains_properly(points, polygons) quadtree_shaped_result = bitmask_result.stack().reset_index() quadtree_shaped_result.columns = [ "point_index", @@ -136,7 +136,3 @@ def contains_properly(polygons, points, how="quadtree"): drop=True ) return result - else: - raise NotImplementedError( - "contains_properly only supports 'quadtree' and 'byte_limited'" - ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index b9d00d9e4..638ebd29c 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -99,9 +99,9 @@ def _compute_predicate( raise TypeError( "`.contains` can only be called with polygon series." ) - how = "quadtree" if self._should_use_quadtree(lhs) else "brute-force" + use_quadtree = self._should_use_quadtree(lhs) pip_result = contains_properly( - lhs, preprocessor_result.final_rhs, how=how + lhs, preprocessor_result.final_rhs, quadtree=use_quadtree ) op_result = ContainsOpResult(pip_result, preprocessor_result) return self._postprocess(lhs, rhs, preprocessor_result, op_result) From 43c50ab58e6a2d4981147a268b741fa2dbdd695d Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 2 May 2023 11:34:49 -0500 Subject: [PATCH 114/126] Refactor basic predicates. --- .../cuspatial/core/binpreds/feature_covers.py | 19 +++++++--- .../core/binpreds/feature_crosses.py | 11 ++++-- .../core/binpreds/feature_disjoint.py | 16 +++++--- .../core/binpreds/feature_overlaps.py | 7 +++- .../core/binpreds/feature_touches.py | 38 ++++++++++++------- .../test_contains_basic_predicate.py | 20 ++++++---- .../basicpreds/test_equals_basic_predicate.py | 13 ++++--- .../test_intersects_basic_predicate.py | 15 ++++---- 8 files changed, 87 insertions(+), 52 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index c6286f183..95552f0c3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -1,5 +1,12 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_any, + _basic_contains_count, + _basic_equals_all, + _basic_equals_count, + _basic_intersects_pli, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -45,32 +52,32 @@ class CoversPredicateBase(EqualsPredicateBase): class LineStringLineStringCovers(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): - return rhs._basic_equals_all(lhs) + return _basic_equals_all(rhs, lhs) class PolygonPointCovers(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_contains_any(rhs) + return _basic_contains_any(lhs, rhs) class PolygonLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): - contains_count = lhs._basic_contains_count(rhs) - pli = lhs._basic_intersects_pli(rhs) + contains_count = _basic_contains_count(lhs, rhs) + pli = _basic_intersects_pli(lhs, rhs) intersections = pli[1] equality = _zero_series(len(rhs)) if len(intersections) == len(rhs): # If the result is degenerate is_degenerate = _linestrings_is_degenerate(intersections) # If all the points in the intersection are in the rhs - equality = intersections._basic_equals_count(rhs) + equality = _basic_equals_count(intersections, rhs) if len(is_degenerate) > 0: equality[is_degenerate] = 1 elif len(intersections) > 0: matching_length_multipoints = _points_and_lines_to_multipoints( intersections, pli[0] ) - equality = matching_length_multipoints._basic_equals_count(rhs) + equality = _basic_equals_count(matching_length_multipoints, rhs) return contains_count + equality >= rhs.sizes diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index e1e915bcd..5e004dffd 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -1,5 +1,10 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from cuspatial.core.binpreds.basic_predicates import ( + _basic_equals, + _basic_intersects, + _basic_intersects_count, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -33,14 +38,14 @@ class CrossesPredicateBase(EqualsPredicateBase): class CrossesByIntersectionPredicate(IntersectsPredicateBase): def _compute_predicate(self, lhs, rhs, preprocessor_result): - intersects = rhs._basic_intersects(lhs) - equals = rhs._basic_equals(lhs) + intersects = _basic_intersects(rhs, lhs) + equals = _basic_equals(rhs, lhs) return intersects & ~equals class LineStringPolygonCrosses(BinPred): def _preprocess(self, lhs, rhs): - intersects = rhs._basic_intersects_count(lhs) > 1 + intersects = _basic_intersects_count(rhs, lhs) > 1 touches = rhs.touches(lhs) contains = rhs.contains(lhs) return ~touches & intersects & ~contains diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index 7b2ee5e76..05a116cfa 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -1,5 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_any, + _basic_intersects, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, NotImplementedPredicate, @@ -26,7 +30,7 @@ def _preprocess(self, lhs, rhs): (Point, Polygon) (Polygon, Point) """ - return ~lhs._basic_contains_any(rhs) + return ~_basic_contains_any(lhs, rhs) class PointLineStringDisjoint(PointLineStringIntersects): @@ -39,8 +43,8 @@ def _postprocess(self, lhs, rhs, op_result): class PointPolygonDisjoint(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains = lhs._basic_contains_any(rhs) + intersects = _basic_intersects(lhs, rhs) + contains = _basic_contains_any(lhs, rhs) return ~intersects & ~contains @@ -60,14 +64,14 @@ def _postprocess(self, lhs, rhs, op_result): class LineStringPolygonDisjoint(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) - contains = rhs._basic_contains_any(lhs) + intersects = _basic_intersects(lhs, rhs) + contains = _basic_contains_any(rhs, lhs) return ~intersects & ~contains class PolygonPolygonDisjoint(BinPred): def _preprocess(self, lhs, rhs): - return ~lhs._basic_contains_any(rhs) & ~rhs._basic_contains_any(lhs) + return ~_basic_contains_any(lhs, rhs) & ~_basic_contains_any(rhs, lhs) DispatchDict = { diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index b126afdd0..d515d92fe 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -2,6 +2,9 @@ import cudf +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_properly_any, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -40,8 +43,8 @@ class PolygonPolygonOverlaps(BinPred): def _preprocess(self, lhs, rhs): contains_lhs = lhs.contains(rhs) contains_rhs = rhs.contains(lhs) - contains_properly_lhs = lhs._basic_contains_properly_any(rhs) - contains_properly_rhs = rhs._basic_contains_properly_any(lhs) + contains_properly_lhs = _basic_contains_properly_any(lhs, rhs) + contains_properly_rhs = _basic_contains_properly_any(rhs, lhs) return ~(contains_lhs | contains_rhs) & ( contains_properly_lhs | contains_properly_rhs ) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index bb257216b..2a5ce9f8a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -1,5 +1,15 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_count, + _basic_contains_properly_any, + _basic_equals, + _basic_equals_all, + _basic_equals_count, + _basic_intersects, + _basic_intersects_count, + _basic_intersects_pli, +) from cuspatial.core.binpreds.binpred_interface import ( BinPred, ImpossiblePredicate, @@ -30,20 +40,20 @@ class TouchesPredicateBase(ContainsPredicate): """ def _preprocess(self, lhs, rhs): - equals = lhs._basic_equals(rhs) + equals = _basic_equals(lhs, rhs) return equals class PointLineStringTouches(BinPred): def _preprocess(self, lhs, rhs): - return lhs._basic_equals(rhs) + return _basic_equals(lhs, rhs) class PointPolygonTouches(ContainsPredicate): def _preprocess(self, lhs, rhs): # Reverse argument order. - equals_all = rhs._basic_equals_all(lhs) - touches = rhs._basic_intersects(lhs) + equals_all = _basic_equals_all(rhs, lhs) + touches = _basic_intersects(rhs, lhs) return ~equals_all & touches @@ -52,9 +62,9 @@ def _preprocess(self, lhs, rhs): """A and B have at least one point in common, and the common points lie in at least one boundary""" # Point is equal - equals = lhs._basic_equals(rhs) + equals = _basic_equals(lhs, rhs) # Linestrings are not equal - equals_all = lhs._basic_equals_all(rhs) + equals_all = _basic_equals_all(lhs, rhs) # Linestrings do not cross crosses = ~lhs.crosses(rhs) return equals & crosses & ~equals_all @@ -62,24 +72,24 @@ def _preprocess(self, lhs, rhs): class LineStringPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - pli = lhs._basic_intersects_pli(rhs) + pli = _basic_intersects_pli(lhs, rhs) if len(pli[1]) == 0: return _false_series(len(lhs)) intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) # A touch can only occur if the point in the intersection # is equal to a point in the linestring, it must # terminate in the boundary of the polygon. - equals = intersections._basic_equals_count(lhs) > 0 - intersects = lhs._basic_intersects_count(rhs) + equals = _basic_equals_count(intersections, lhs) > 0 + intersects = _basic_intersects_count(lhs, rhs) contains = rhs.contains(lhs) - contains_any = rhs._basic_contains_properly_any(lhs) + contains_any = _basic_contains_properly_any(rhs, lhs) intersects = (intersects == 1) | (intersects == 2) return equals & intersects & ~contains & ~contains_any class PolygonPointTouches(BinPred): def _preprocess(self, lhs, rhs): - intersects = lhs._basic_intersects(rhs) + intersects = _basic_intersects(lhs, rhs) return intersects @@ -90,9 +100,9 @@ def _preprocess(self, lhs, rhs): class PolygonPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): - contains_lhs_none = lhs._basic_contains_count(rhs) == 0 - contains_rhs_none = rhs._basic_contains_count(lhs) == 0 - intersects = lhs._basic_intersects_count(rhs) == 1 + contains_lhs_none = _basic_contains_count(lhs, rhs) == 0 + contains_rhs_none = _basic_contains_count(rhs, lhs) == 0 + intersects = _basic_intersects_count(lhs, rhs) == 1 return contains_lhs_none & contains_rhs_none & intersects diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py index 8595cd3c6..c299770cc 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_contains_basic_predicate.py @@ -3,12 +3,16 @@ from shapely.geometry import LineString, Point, Polygon import cuspatial +from cuspatial.core.binpreds.basic_predicates import ( + _basic_contains_any, + _basic_contains_count, +) def test_basic_contains_any_outside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(2, 2)]) - got = lhs._basic_contains_any(rhs).to_pandas() + got = _basic_contains_any(lhs, rhs).to_pandas() expected = [False] assert (got == expected).all() @@ -16,7 +20,7 @@ def test_basic_contains_any_outside(): def test_basic_contains_any_inside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) - got = lhs._basic_contains_any(rhs).to_pandas() + got = _basic_contains_any(lhs, rhs).to_pandas() expected = [True] assert (got == expected).all() @@ -24,7 +28,7 @@ def test_basic_contains_any_inside(): def test_basic_contains_any_point(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0)]) - got = lhs._basic_contains_any(rhs).to_pandas() + got = _basic_contains_any(lhs, rhs).to_pandas() expected = [True] assert (got == expected).all() @@ -32,7 +36,7 @@ def test_basic_contains_any_point(): def test_basic_contains_any_edge(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0.5)]) - got = lhs._basic_contains_any(rhs).to_pandas() + got = _basic_contains_any(lhs, rhs).to_pandas() expected = [True] assert (got == expected).all() @@ -40,7 +44,7 @@ def test_basic_contains_any_edge(): def test_basic_contains_count_outside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(2, 2)]) - got = lhs._basic_contains_count(rhs).to_pandas() + got = _basic_contains_count(lhs, rhs).to_pandas() expected = [0] assert (got == expected).all() @@ -48,7 +52,7 @@ def test_basic_contains_count_outside(): def test_basic_contains_count_inside(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([LineString([(0.5, 0.5), (1.5, 1.5)])]) - got = lhs._basic_contains_count(rhs).to_pandas() + got = _basic_contains_count(lhs, rhs).to_pandas() expected = [1] assert (got == expected).all() @@ -56,7 +60,7 @@ def test_basic_contains_count_inside(): def test_basic_contains_count_point(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0)]) - got = lhs._basic_contains_count(rhs).to_pandas() + got = _basic_contains_count(lhs, rhs).to_pandas() expected = [0] assert (got == expected).all() @@ -64,6 +68,6 @@ def test_basic_contains_count_point(): def test_basic_contains_count_edge(): lhs = cuspatial.GeoSeries([Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]) rhs = cuspatial.GeoSeries([Point(0, 0.5)]) - got = lhs._basic_contains_count(rhs).to_pandas() + got = _basic_contains_count(lhs, rhs).to_pandas() expected = [0] assert (got == expected).all() diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py index 0174312c6..f25cebbdc 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py @@ -3,45 +3,46 @@ from shapely.geometry import Point import cuspatial +from cuspatial.core.binpreds.basic_predicates import _basic_equals def test_single_true(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(0, 0)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True])) def test_single_false(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False])) def test_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False])) def test_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True])) def test_true_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2), Point(2, 2)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False, True])) def test_false_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0), Point(2, 2)]) - result = p1._basic_equals(p2) + result = _basic_equals(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True, False])) diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py index a1b432ad1..00193c5d1 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_intersects_basic_predicate.py @@ -3,47 +3,48 @@ from shapely.geometry import LineString, Point, Polygon import cuspatial +from cuspatial.core.binpreds.basic_predicates import _basic_intersects def test_single_true(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(0, 0)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True])) def test_single_false(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False])) def test_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False])) def test_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True])) def test_true_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2), Point(2, 2)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False, True])) def test_false_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0), Point(2, 2)]) - result = p1._basic_intersects(p2) + result = _basic_intersects(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True, False])) @@ -62,5 +63,5 @@ def test_linestring_polygon_within(): Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), ] ) - result = lhs._basic_intersects(rhs) + result = _basic_intersects(lhs, rhs) assert_series_equal(result.to_pandas(), pd.Series([True, True, True])) From d628a44b0b2931fcf7ad47532d09671877b1330f Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 4 May 2023 10:59:31 -0500 Subject: [PATCH 115/126] Update python/cuspatial/cuspatial/core/geoseries.py Co-authored-by: Mark Harris <783069+harrism@users.noreply.github.com> --- python/cuspatial/cuspatial/core/geoseries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 49a975752..067cc2798 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1355,8 +1355,7 @@ def touches(self, other, align=True): """Returns True for all aligned geometries that touch other, else False. - Geometries touch if they have at least one point in common, but their - interiors do not intersect. + Geometries touch if they have any coincident edges or share any vertices, and their interiors do not intersect. Parameters ---------- From 2eb4b3237dfda9f37aaad9f4a8948b796ebcedb4 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 4 May 2023 22:18:24 +0000 Subject: [PATCH 116/126] Handle @isVoid's final comments. --- .../core/binpreds/feature_contains_properly.py | 6 ++---- python/cuspatial/cuspatial/core/geoseries.py | 11 ++++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py index 638ebd29c..0c81ead59 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains_properly.py @@ -34,10 +34,8 @@ class ContainsProperlyPredicate(ContainsGeometryProcessor): def __init__(self, **kwargs): - """Base class for binary predicates that are defined in terms of a - `contains` basic predicate. This class implements the logic that - underlies `polygon.contains` primarily, and is implemented for many - cases. + """Base class for binary predicates that are defined in terms of + `contains_properly`. Subclasses are selected using the `DispatchDict` located at the end of this file. diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 49a975752..705152f41 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1001,13 +1001,13 @@ def contains(self, other, align=False, allpairs=False, mode="full"): """Returns a `Series` of `dtype('bool')` with value `True` for each aligned geometry that contains _other_. - An object `a` is said to contain `b` if `b`'s `boundary` and - `interiors` are within those of `a` and no point of `b` lies in the - exterior of `a`. + An object a is said to contain b if b's boundary and + interiors are within those of a and no point of b lies in the + exterior of a. If `allpairs=False`, the result will be a `Series` of `dtype('bool')`. If `allpairs=True`, the result will be a `DataFrame` containing two - columns, `point_indices` and `polygon_indices`, each of which is a + columns, `point_indices` a`nd `polygon_indices`, each of which is a `Series` of `dtype('int32')`. The `point_indices` `Series` contains the indices of the points in the right GeoSeries, and the `polygon_indices` `Series` contains the indices of the polygons in the @@ -1032,7 +1032,8 @@ def contains(self, other, align=False, allpairs=False, mode="full"): the indices of the points in the right GeoSeries, and the `polygon_indices` `Series` contains the indices of the polygons in the left GeoSeries. Excludes boundary points. - mode : str, default "full" + mode : str, default "full" or "basic_none", "basic_any", + "basic_all", or "basic_count". If "full", the result will be a `Series` of `dtype('bool')` with value `True` for each aligned geometry that contains _other_. If "intersects", the result will be a `Series` of `dtype('bool')` From 9855137884f4c7dda9e265aed392f7a517bfe6a5 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 4 May 2023 22:38:53 +0000 Subject: [PATCH 117/126] Style --- 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 7ad38d7a2..c13b673ed 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -1356,7 +1356,8 @@ def touches(self, other, align=True): """Returns True for all aligned geometries that touch other, else False. - Geometries touch if they have any coincident edges or share any vertices, and their interiors do not intersect. + Geometries touch if they have any coincident edges or share any + vertices, and their interiors do not intersect. Parameters ---------- From f9b2df655528bbd3b89213f7014ca592a4a0c314 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 8 May 2023 18:42:30 +0000 Subject: [PATCH 118/126] Disable logging without environment variable. --- .../cuspatial/core/binpreds/feature_covers.py | 3 +- .../binpreds/test_binpred_test_dispatch.py | 143 +++++++++++------- 2 files changed, 86 insertions(+), 60 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 95552f0c3..535993e8a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -14,7 +14,6 @@ ) from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.core.binpreds.feature_intersects import ( - IntersectsPredicateBase, LineStringPointIntersects, ) from cuspatial.utils.binpred_utils import ( @@ -50,7 +49,7 @@ class CoversPredicateBase(EqualsPredicateBase): pass -class LineStringLineStringCovers(IntersectsPredicateBase): +class LineStringLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): return _basic_equals_all(rhs, lhs) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 232b94bc0..b9175c643 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -1,16 +1,17 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +import os from functools import wraps import pandas as pd import pytest from binpred_test_dispatch import predicate, simple_test # noqa: F401 -"""Decorator function that xfails a test if an exception is throw -by the test function. Will be removed when all tests are passing.""" - def xfail_on_exception(func): + """Decorator function that xfails a test if an exception is throw + by the test function. Will be removed when all tests are passing.""" + @wraps(func) def wrapper(*args, **kwargs): try: @@ -22,7 +23,9 @@ def wrapper(*args, **kwargs): # In the below file, all failing tests are recorded with visualizations. -out_file = open("test_binpred_test_dispatch.log", "w") +LOG_DISPATCHED_PREDICATES = os.environ.get("LOG_DISPATCHED_PREDICATES", False) +if LOG_DISPATCHED_PREDICATES: + out_file = open("test_binpred_test_dispatch.log", "w") # @xfail_on_exception # TODO: Remove when all tests are passing @@ -38,10 +41,15 @@ def test_simple_features( """Parameterized test fixture that runs a binary predicate test for each combination of geometry types and binary predicates. + Enable the `LOG_DISPATCHED_PREDICATES` environment variable to + log the dispatched predicate results. + Uses four fixtures from `conftest.py` to store the number of times each binary predicate has passed and failed, and the number of times each combination of geometry types has passed and failed. These - results are saved to CSV files after each test. + results are saved to CSV files after each test. The result of the + tests can be summarized with + `tests/binpreds/summarize_binpred_test_dispatch_results.py`. Uses the @xfail_on_exception decorator to mark a test as xfailed if an exception is thrown. This is a temporary measure to allow @@ -71,7 +79,7 @@ def test_simple_features( The pytest request object. Used to print the test name in diagnostic output. """ - try: + if not LOG_DISPATCHED_PREDICATES: (lhs, rhs) = simple_test[2], simple_test[3] gpdlhs = lhs.to_geopandas() gpdrhs = rhs.to_geopandas() @@ -89,66 +97,85 @@ def test_simple_features( gpd_pred_fn = getattr(gpdlhs, predicate) expected = gpd_pred_fn(gpdrhs) assert (got.values_host == expected.values).all() - - # The test is complete, the rest is just logging. + else: try: - # The test passed, store the results. - predicate_passes[predicate] = ( + (lhs, rhs) = simple_test[2], simple_test[3] + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + + # Reverse + pred_fn = getattr(rhs, predicate) + got = pred_fn(lhs) + gpd_pred_fn = getattr(gpdrhs, predicate) + expected = gpd_pred_fn(gpdlhs) + assert (got.values_host == expected.values).all() + + # Forward + pred_fn = getattr(lhs, predicate) + got = pred_fn(rhs) + gpd_pred_fn = getattr(gpdlhs, predicate) + expected = gpd_pred_fn(gpdrhs) + assert (got.values_host == expected.values).all() + + # The test is complete, the rest is just logging. + try: + # The test passed, store the results. + predicate_passes[predicate] = ( + 1 + if predicate not in predicate_passes + else predicate_passes[predicate] + 1 + ) + feature_passes[(lhs.column_type, rhs.column_type)] = ( + 1 + if (lhs.column_type, rhs.column_type) not in feature_passes + else feature_passes[(lhs.column_type, rhs.column_type)] + 1 + ) + passes_df = pd.DataFrame( + { + "predicate": list(predicate_passes.keys()), + "predicate_passes": list(predicate_passes.values()), + } + ) + passes_df.to_csv("predicate_passes.csv", index=False) + passes_df = pd.DataFrame( + { + "feature": list(feature_passes.keys()), + "feature_passes": list(feature_passes.values()), + } + ) + passes_df.to_csv("feature_passes.csv", index=False) + except Exception as e: + raise ValueError(e) + except Exception as e: + # The test failed, store the results. + out_file.write( + f"""{predicate}, + ------------ + {simple_test[0]}\n{simple_test[1]}\nfailed + test: {request.node.name}\n\n""" + ) + predicate_fails[predicate] = ( 1 - if predicate not in predicate_passes - else predicate_passes[predicate] + 1 + if predicate not in predicate_fails + else predicate_fails[predicate] + 1 ) - feature_passes[(lhs.column_type, rhs.column_type)] = ( + feature_fails[(lhs.column_type, rhs.column_type)] = ( 1 - if (lhs.column_type, rhs.column_type) not in feature_passes - else feature_passes[(lhs.column_type, rhs.column_type)] + 1 + if (lhs.column_type, rhs.column_type) not in feature_fails + else feature_fails[(lhs.column_type, rhs.column_type)] + 1 ) - passes_df = pd.DataFrame( + predicate_fails_df = pd.DataFrame( { - "predicate": list(predicate_passes.keys()), - "predicate_passes": list(predicate_passes.values()), + "predicate": list(predicate_fails.keys()), + "predicate_fails": list(predicate_fails.values()), } ) - passes_df.to_csv("predicate_passes.csv", index=False) - passes_df = pd.DataFrame( + predicate_fails_df.to_csv("predicate_fails.csv", index=False) + feature_fails_df = pd.DataFrame( { - "feature": list(feature_passes.keys()), - "feature_passes": list(feature_passes.values()), + "feature": list(feature_fails.keys()), + "feature_fails": list(feature_fails.values()), } ) - passes_df.to_csv("feature_passes.csv", index=False) - except Exception as e: - raise ValueError(e) - except Exception as e: - # The test failed, store the results. - out_file.write( - f"""{predicate}, ------------- -{simple_test[0]}\n{simple_test[1]}\nfailed -test: {request.node.name}\n\n""" - ) - predicate_fails[predicate] = ( - 1 - if predicate not in predicate_fails - else predicate_fails[predicate] + 1 - ) - feature_fails[(lhs.column_type, rhs.column_type)] = ( - 1 - if (lhs.column_type, rhs.column_type) not in feature_fails - else feature_fails[(lhs.column_type, rhs.column_type)] + 1 - ) - predicate_fails_df = pd.DataFrame( - { - "predicate": list(predicate_fails.keys()), - "predicate_fails": list(predicate_fails.values()), - } - ) - predicate_fails_df.to_csv("predicate_fails.csv", index=False) - feature_fails_df = pd.DataFrame( - { - "feature": list(feature_fails.keys()), - "feature_fails": list(feature_fails.values()), - } - ) - feature_fails_df.to_csv("feature_fails.csv", index=False) - raise e + feature_fails_df.to_csv("feature_fails.csv", index=False) + raise e From 961d1992f837d92afd858add18db2a676365b951 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Mon, 8 May 2023 19:24:45 +0000 Subject: [PATCH 119/126] Replace missing geoseries tests and clean up touches and binpreds test. --- .../core/binpreds/feature_touches.py | 33 +++++++++---------- .../binpreds/test_binpred_test_dispatch.py | 18 ---------- .../cuspatial/tests/test_geoseries.py | 2 ++ 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 2a5ce9f8a..96f039b87 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -26,17 +26,19 @@ class TouchesPredicateBase(ContainsPredicate): - """Base class for binary predicates that use the contains predicate - to implement the touches predicate. For example, a Point-Polygon - Touches predicate is defined in terms of a Point-Polygon Contains - predicate. + """ + If any point is shared between the following geometry types, they touch: Used by: - (Point, Polygon) - (Polygon, Point) + (Point, MultiPoint) + (Point, LineString) + (MultiPoint, Point) + (MultiPoint, MultiPoint) + (MultiPoint, LineString) + (MultiPoint, Polygon) + (LineString, Point) + (LineString, MultiPoint) (Polygon, MultiPoint) - (Polygon, LineString) - (Polygon, Polygon) """ def _preprocess(self, lhs, rhs): @@ -44,11 +46,6 @@ def _preprocess(self, lhs, rhs): return equals -class PointLineStringTouches(BinPred): - def _preprocess(self, lhs, rhs): - return _basic_equals(lhs, rhs) - - class PointPolygonTouches(ContainsPredicate): def _preprocess(self, lhs, rhs): # Reverse argument order. @@ -66,8 +63,8 @@ def _preprocess(self, lhs, rhs): # Linestrings are not equal equals_all = _basic_equals_all(lhs, rhs) # Linestrings do not cross - crosses = ~lhs.crosses(rhs) - return equals & crosses & ~equals_all + crosses = lhs.crosses(rhs) + return equals & ~crosses & ~equals_all class LineStringPolygonTouches(BinPred): @@ -77,13 +74,13 @@ def _preprocess(self, lhs, rhs): return _false_series(len(lhs)) intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) # A touch can only occur if the point in the intersection - # is equal to a point in the linestring, it must + # is equal to a point in the linestring: it must # terminate in the boundary of the polygon. equals = _basic_equals_count(intersections, lhs) > 0 intersects = _basic_intersects_count(lhs, rhs) + intersects = (intersects == 1) | (intersects == 2) contains = rhs.contains(lhs) contains_any = _basic_contains_properly_any(rhs, lhs) - intersects = (intersects == 1) | (intersects == 2) return equals & intersects & ~contains & ~contains_any @@ -109,7 +106,7 @@ def _preprocess(self, lhs, rhs): DispatchDict = { (Point, Point): ImpossiblePredicate, (Point, MultiPoint): TouchesPredicateBase, - (Point, LineString): PointLineStringTouches, + (Point, LineString): TouchesPredicateBase, (Point, Polygon): PointPolygonTouches, (MultiPoint, Point): TouchesPredicateBase, (MultiPoint, MultiPoint): TouchesPredicateBase, diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index b9175c643..9663e4136 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -1,34 +1,16 @@ # Copyright (c) 2023, NVIDIA CORPORATION. import os -from functools import wraps import pandas as pd -import pytest from binpred_test_dispatch import predicate, simple_test # noqa: F401 - -def xfail_on_exception(func): - """Decorator function that xfails a test if an exception is throw - by the test function. Will be removed when all tests are passing.""" - - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - pytest.xfail(f"Xfailling due to an exception: {e}") - - return wrapper - - # In the below file, all failing tests are recorded with visualizations. LOG_DISPATCHED_PREDICATES = os.environ.get("LOG_DISPATCHED_PREDICATES", False) if LOG_DISPATCHED_PREDICATES: out_file = open("test_binpred_test_dispatch.log", "w") -# @xfail_on_exception # TODO: Remove when all tests are passing def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 diff --git a/python/cuspatial/cuspatial/tests/test_geoseries.py b/python/cuspatial/cuspatial/tests/test_geoseries.py index 3a8500c09..1a66d4457 100644 --- a/python/cuspatial/cuspatial/tests/test_geoseries.py +++ b/python/cuspatial/cuspatial/tests/test_geoseries.py @@ -273,6 +273,8 @@ def test_getitem_slice_points_loc(): gps = gpd.GeoSeries([p0, p1, p2]) cus = cuspatial.from_geopandas(gps) assert_eq_point(cus[0:1][0], gps[0:1][0]) + assert_eq_point(cus[0:2][0], gps[0:2][0]) + assert_eq_point(cus[1:2][1], gps[1:2][1]) assert_eq_point(cus[0:3][0], gps[0:3][0]) assert_eq_point(cus[1:3][1], gps[1:3][1]) assert_eq_point(cus[2:3][2], gps[2:3][2]) From ebc33b81a3427f70985c645161d6c255262221bf Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Tue, 9 May 2023 16:31:21 +0000 Subject: [PATCH 120/126] Enable previosly unsupported tests. --- .../core/binpreds/feature_disjoint.py | 13 +++---- .../cuspatial/core/binpreds/feature_equals.py | 13 +++---- .../core/binpreds/feature_intersects.py | 9 ++--- .../binpreds/test_equals_only_binpreds.py | 13 +++---- .../binpreds/test_intersects_only_binpreds.py | 36 ++----------------- 5 files changed, 17 insertions(+), 67 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py index 05a116cfa..a0347b76a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -8,10 +8,7 @@ BinPred, NotImplementedPredicate, ) -from cuspatial.core.binpreds.feature_intersects import ( - IntersectsPredicateBase, - PointLineStringIntersects, -) +from cuspatial.core.binpreds.feature_intersects import IntersectsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -33,12 +30,12 @@ def _preprocess(self, lhs, rhs): return ~_basic_contains_any(lhs, rhs) -class PointLineStringDisjoint(PointLineStringIntersects): - def _postprocess(self, lhs, rhs, op_result): +class PointLineStringDisjoint(BinPred): + def _preprocess(self, lhs, rhs): """Disjoint is the opposite of intersects, so just implement intersects and then negate the result.""" - result = super()._postprocess(lhs, rhs, op_result) - return ~result + intersects = _basic_intersects(lhs, rhs) + return ~intersects class PointPolygonDisjoint(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index d4fdae769..bf6997e0a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py @@ -335,16 +335,11 @@ def _preprocess(self, lhs, rhs): return _false_series(len(lhs)) -class PolygonPolygonEquals(EqualsPredicateBase): - def _compute_predicate(self, lhs, rhs, preprocessor_result): +class PolygonPolygonEquals(BinPred): + def _preprocess(self, lhs, rhs): """Two polygons are equal if they contain each other.""" - from cuspatial.core.binpreds.binpred_dispatch import CONTAINS_DISPATCH - - predicate = CONTAINS_DISPATCH[(lhs.column_type, rhs.column_type)]( - align=self.config.align - ) - lhs_contains_rhs = predicate(lhs, rhs) - rhs_contains_lhs = predicate(rhs, lhs) + lhs_contains_rhs = lhs.contains(rhs) + rhs_contains_lhs = rhs.contains(lhs) return lhs_contains_rhs & rhs_contains_lhs diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index d8ecfdb38..9f8690277 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -23,7 +23,6 @@ Point, Polygon, _false_series, - _linestrings_from_geometry, ) @@ -107,12 +106,8 @@ def _preprocess(self, lhs, rhs): class LineStringPointIntersects(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): - """Convert rhs to linestrings by making a linestring that has - the same start and end point.""" - ls_rhs = _linestrings_from_geometry(rhs) - return self._compute_predicate( - lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) - ) + intersects = _basic_intersects(lhs, rhs) + return intersects class PointLineStringIntersects(LineStringPointIntersects): diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_equals_only_binpreds.py b/python/cuspatial/cuspatial/tests/binpreds/test_equals_only_binpreds.py index 331f3002d..47a07bee9 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_equals_only_binpreds.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_equals_only_binpreds.py @@ -532,7 +532,10 @@ def test_pair_linestrings_different_last_two(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") +@pytest.mark.xfail( + reason="""The current implementation of .contains +conceals this special case. Unsure of the solution.""" +) def test_pair_polygons_different_ordering(): gpdpoly1 = gpd.GeoSeries( [ @@ -551,7 +554,6 @@ def test_pair_polygons_different_ordering(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_pair_polygons_different_winding(): gpdpoly1 = gpd.GeoSeries( [ @@ -570,7 +572,6 @@ def test_pair_polygons_different_winding(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_3_polygons_geom_equals_3_polygons_misordered_corrected_vertex(): gpdpoly1 = gpd.GeoSeries( [ @@ -593,7 +594,6 @@ def test_3_polygons_geom_equals_3_polygons_misordered_corrected_vertex(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_polygon_geom_equals_polygon(): gpdpolygon1 = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) gpdpolygon2 = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) @@ -604,7 +604,6 @@ def test_polygon_geom_equals_polygon(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_polygon_geom_equals_polygon_swap_inner(): gpdpolygon1 = gpd.GeoSeries(Polygon([[0, 0], [1, 0], [1, 1], [0, 0]])) gpdpolygon2 = gpd.GeoSeries(Polygon([[0, 0], [1, 1], [1, 0], [0, 0]])) @@ -615,7 +614,6 @@ def test_polygon_geom_equals_polygon_swap_inner(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") @pytest.mark.parametrize( "lhs", [ @@ -652,7 +650,6 @@ def test_3_polygons_geom_equals_3_polygons_one_equal(lhs): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_100_polygons_geom_equals_100_polygons(polygon_generator): gpdpolygons1 = gpd.GeoSeries([*polygon_generator(100, 0)]) gpdpolygons2 = gpd.GeoSeries([*polygon_generator(100, 0)]) @@ -663,7 +660,6 @@ def test_100_polygons_geom_equals_100_polygons(polygon_generator): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_3_polygons_geom_equals_3_polygons_different_sizes(): gpdpoly1 = gpd.GeoSeries( [ @@ -688,7 +684,6 @@ def test_3_polygons_geom_equals_3_polygons_different_sizes(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip(reason="NotImplemented: Depends on .contains") def test_3_polygons_geom_equals_3_polygons_misordered(): gpdpoly1 = gpd.GeoSeries( [ diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py b/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py index 69a99b6c6..46e11f8a4 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py @@ -261,9 +261,7 @@ def test_linestring_intersects_multipoint_cross_intersection(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="NotImplemented. Depends on allpairs_multipoint_equals_count" -) +@pytest.mark.xfail(reason="Multipoints not supported yet.") def test_linestring_intersects_multipoint_implicit_cross_intersection(): g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) g2 = cuspatial.GeoSeries([MultiPoint([(0.0, 1.0), (1.0, 0.0)])]) @@ -274,9 +272,7 @@ def test_linestring_intersects_multipoint_implicit_cross_intersection(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="NotImplemented. Depends on allpairs_multipoint_equals_count" -) +@pytest.mark.xfail(reason="Multipoints not supported yet.") def test_100_linestrings_intersects_100_multipoints( linestring_generator, multipoint_generator ): @@ -569,10 +565,6 @@ def test_multilinestring_intersects_linestring(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_linestring_intersects_polygon(): g1 = cuspatial.GeoSeries( [ @@ -593,10 +585,6 @@ def test_linestring_intersects_polygon(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_polygon_intersects_linestring(): g1 = cuspatial.GeoSeries( [ @@ -617,10 +605,6 @@ def test_polygon_intersects_linestring(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_multipolygon_intersects_linestring(): g1 = cuspatial.GeoSeries( [ @@ -651,10 +635,6 @@ def test_multipolygon_intersects_linestring(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_linestring_intersects_multipolygon(): g1 = cuspatial.GeoSeries( [ @@ -685,10 +665,6 @@ def test_linestring_intersects_multipolygon(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_polygon_intersects_multipolygon(): g1 = cuspatial.GeoSeries( [ @@ -719,10 +695,6 @@ def test_polygon_intersects_multipolygon(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_multipolygon_intersects_polygon(): g1 = cuspatial.GeoSeries( [ @@ -753,10 +725,6 @@ def test_multipolygon_intersects_polygon(): pd.testing.assert_series_equal(expected, got.to_pandas()) -@pytest.mark.skip( - reason="""NotImplemented. Depends on a a combination -of intersects and contains.""" -) def test_multipolygon_intersects_multipolygon(): g1 = cuspatial.GeoSeries( [ From 30eded75e2c6c74d225d3f3c4c9b765174c73cc7 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Wed, 17 May 2023 22:10:23 +0000 Subject: [PATCH 121/126] Add deeper docs to _points_and_lines_to_multipoints --- .../cuspatial/utils/binpred_utils.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 7229df632..2750a5178 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -305,7 +305,49 @@ def _open_polygon_rings(geoseries): def _points_and_lines_to_multipoints(geoseries, offsets): """Converts a geoseries of points and lines into a geoseries of - multipoints.""" + multipoints. + + Given a geoseries of points and lines, this function will return a + geoseries of multipoints. The multipoints will contain the points + and lines in the same order as the original geoseries. The offsets + parameter groups the points and lines into multipoints. The offsets + parameter must be a list of integers that contains the offsets of + the multipoints in the original geoseries. A group of four points + and lines can be arranged into four sets of multipoints depending + on the offset used: + + >>> import cuspatial + >>> from cuspatial.utils.binpred_utils import ( + ... _points_and_lines_to_multipoints + ... ) + >>> from shapely.geometry import Point, LineString + >>> mixed = cuspatial.GeoSeries([ + ... Point(0, 0), + ... LineString([(1, 1), (2, 2)]), + ... Point(3, 3), + ... LineString([(4, 4), (5, 5)]), + ... ]) + >>> offsets = [0, 4] + >>> # Place all of the points and linestrings into a single + >>> # multipoint + >>> _points_and_lines_to_multipoints(mixed, offsets) + 0 MULTIPOINT (0.00000 0.00000, 1.00000, 1.0000, ... + dtype: geometry + >>> offsets = [0, 1, 2, 3, 4] + >>> # Place each point and linestring into its own multipoint + >>> _points_and_lines_to_multipoints(mixed, offsets) + 0 MULTIPOINT (0.00000 0.00000) + 1 MULTIPOINT (1.00000, 1.00000, 2.00000, 2.00000) + 2 MULTIPOINT (3.00000 3.00000) + 3 MULTIPOINT (4.00000, 4.00000, 5.00000, 5.00000) + dtype: geometry + >>> offsets = [0, 2, 4] + >>> # Split the points and linestrings into two multipoints + >>> _points_and_lines_to_multipoints(mixed, offsets) + 0 MULTIPOINT (0.00000 0.00000, 1.00000, 1.0000, ... + 1 MULTIPOINT (3.00000 3.00000, 4.00000, 4.0000, ... + dtype: geometry + """ points_mask = geoseries.type == "Point" lines_mask = geoseries.type == "Linestring" if (points_mask + lines_mask).sum() != len(geoseries): From ded3d74c3f4831ebc54ac29d2452b7830ad91411 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 18 May 2023 14:21:54 +0000 Subject: [PATCH 122/126] Resolve core issue with LineStringLineStringTouches and Crosses. --- .../core/binpreds/basic_predicates.py | 2 +- .../core/binpreds/feature_contains.py | 6 +++--- .../core/binpreds/feature_crosses.py | 20 +++++++++++++------ .../core/binpreds/feature_touches.py | 9 ++++----- .../cuspatial/core/binpreds/feature_within.py | 6 +++--- .../basicpreds/test_equals_basic_predicate.py | 14 ++++++------- .../tests/binpreds/binpred_test_dispatch.py | 13 ++++++++++++ 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py b/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py index 399eed58c..85438fefa 100644 --- a/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py +++ b/python/cuspatial/cuspatial/core/binpreds/basic_predicates.py @@ -12,7 +12,7 @@ ) -def _basic_equals(lhs, rhs): +def _basic_equals_any(lhs, rhs): """Utility method that returns True if any point in the lhs geometry is equal to a point in the rhs geometry.""" lhs = _multipoints_from_geometry(lhs) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index d576930bf..51b0ab651 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -6,7 +6,7 @@ from cuspatial.core.binpreds.basic_predicates import ( _basic_contains_count, - _basic_equals, + _basic_equals_any, _basic_equals_count, _basic_intersects, _basic_intersects_pli, @@ -132,13 +132,13 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): class PointPointContains(BinPred): def _preprocess(self, lhs, rhs): - return _basic_equals(lhs, rhs) + return _basic_equals_any(lhs, rhs) class LineStringPointContains(BinPred): def _preprocess(self, lhs, rhs): intersects = _basic_intersects(lhs, rhs) - equals = _basic_equals(lhs, rhs) + equals = _basic_equals_any(lhs, rhs) return intersects & ~equals diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 5e004dffd..0316f3cbd 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -1,9 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. from cuspatial.core.binpreds.basic_predicates import ( - _basic_equals, - _basic_intersects, + _basic_equals_count, _basic_intersects_count, + _basic_intersects_pli, ) from cuspatial.core.binpreds.binpred_interface import ( BinPred, @@ -17,6 +17,7 @@ Point, Polygon, _false_series, + _points_and_lines_to_multipoints, ) @@ -36,10 +37,17 @@ class CrossesPredicateBase(EqualsPredicateBase): pass -class CrossesByIntersectionPredicate(IntersectsPredicateBase): +class LineStringLineStringCrosses(IntersectsPredicateBase): def _compute_predicate(self, lhs, rhs, preprocessor_result): - intersects = _basic_intersects(rhs, lhs) - equals = _basic_equals(rhs, lhs) + # A linestring crosses another linestring iff + # they intersect, and none of the points of the + # intersection are in the boundary of the other + pli = _basic_intersects_pli(rhs, lhs) + intersections = _points_and_lines_to_multipoints(pli[1], pli[0]) + equals = (_basic_equals_count(intersections, lhs) > 0) | ( + _basic_equals_count(intersections, rhs) > 0 + ) + intersects = _basic_intersects_count(rhs, lhs) > 0 return intersects & ~equals @@ -73,7 +81,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, Polygon): ImpossiblePredicate, (LineString, Point): ImpossiblePredicate, (LineString, MultiPoint): ImpossiblePredicate, - (LineString, LineString): CrossesByIntersectionPredicate, + (LineString, LineString): LineStringLineStringCrosses, (LineString, Polygon): LineStringPolygonCrosses, (Polygon, Point): CrossesPredicateBase, (Polygon, MultiPoint): CrossesPredicateBase, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 96f039b87..e054e24ae 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -3,8 +3,8 @@ from cuspatial.core.binpreds.basic_predicates import ( _basic_contains_count, _basic_contains_properly_any, - _basic_equals, _basic_equals_all, + _basic_equals_any, _basic_equals_count, _basic_intersects, _basic_intersects_count, @@ -42,7 +42,7 @@ class TouchesPredicateBase(ContainsPredicate): """ def _preprocess(self, lhs, rhs): - equals = _basic_equals(lhs, rhs) + equals = _basic_equals_any(lhs, rhs) return equals @@ -58,13 +58,12 @@ class LineStringLineStringTouches(BinPred): def _preprocess(self, lhs, rhs): """A and B have at least one point in common, and the common points lie in at least one boundary""" - # Point is equal - equals = _basic_equals(lhs, rhs) # Linestrings are not equal equals_all = _basic_equals_all(lhs, rhs) # Linestrings do not cross crosses = lhs.crosses(rhs) - return equals & ~crosses & ~equals_all + intersects = lhs.intersects(rhs) + return intersects & ~crosses & ~equals_all class LineStringPolygonTouches(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 043f4629e..57c9c5878 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -1,8 +1,8 @@ # Copyright (c) 2023, NVIDIA CORPORATION. from cuspatial.core.binpreds.basic_predicates import ( - _basic_equals, _basic_equals_all, + _basic_equals_any, _basic_intersects, ) from cuspatial.core.binpreds.binpred_interface import ( @@ -26,14 +26,14 @@ def _preprocess(self, lhs, rhs): class WithinIntersectsPredicate(BinPred): def _preprocess(self, lhs, rhs): intersects = _basic_intersects(rhs, lhs) - equals = _basic_equals(rhs, lhs) + equals = _basic_equals_any(rhs, lhs) return intersects & ~equals class PointLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): intersects = lhs.intersects(rhs) - equals = _basic_equals(lhs, rhs) + equals = _basic_equals_any(lhs, rhs) return intersects & ~equals diff --git a/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py index f25cebbdc..a164c5d0f 100644 --- a/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py +++ b/python/cuspatial/cuspatial/tests/basicpreds/test_equals_basic_predicate.py @@ -3,46 +3,46 @@ from shapely.geometry import Point import cuspatial -from cuspatial.core.binpreds.basic_predicates import _basic_equals +from cuspatial.core.binpreds.basic_predicates import _basic_equals_any def test_single_true(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(0, 0)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True])) def test_single_false(): p1 = cuspatial.GeoSeries([Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False])) def test_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False])) def test_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True])) def test_true_false_true(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)]) p2 = cuspatial.GeoSeries([Point(0, 0), Point(2, 2), Point(2, 2)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([True, False, True])) def test_false_true_false(): p1 = cuspatial.GeoSeries([Point(0, 0), Point(0, 0), Point(0, 0)]) p2 = cuspatial.GeoSeries([Point(1, 1), Point(0, 0), Point(2, 2)]) - result = _basic_equals(p1, p2) + result = _basic_equals_any(p1, p2) assert_series_equal(result.to_pandas(), pd.Series([False, True, False])) diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index b8b93651d..a9ded241e 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -138,6 +138,17 @@ def predicate(request): LineString([(0.0, 0.0), (1.0, 0.0)]), LineString([(0.5, 0.0), (0.5, 1.0)]), ), + "linestring-linestring-touch-edge-twice": ( + """ + x + x + / \ + x---x + x + """, + LineString([(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]), + LineString([(0.25, 0.25), (1.0, 0.0), (0.5, 0.5)]), + ), "linestring-linestring-crosses": ( """ x @@ -454,6 +465,8 @@ def predicate(request): "linestring-linestring-same", "linestring-linestring-touches", "linestring-linestring-touch-interior", + "linestring-linestring-touch-edge", + "linestring-linestring-touch-edge-twice", "linestring-linestring-crosses", ] From 55bac02d623540b6f8077aac75ee4ed345e4eb98 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 18 May 2023 14:47:51 +0000 Subject: [PATCH 123/126] Improve PolygonPolygonTouches handling of edge overlapping cases. --- .../cuspatial/core/binpreds/feature_touches.py | 5 +++-- .../tests/binpreds/binpred_test_dispatch.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index e054e24ae..62a48a0d8 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -98,8 +98,9 @@ class PolygonPolygonTouches(BinPred): def _preprocess(self, lhs, rhs): contains_lhs_none = _basic_contains_count(lhs, rhs) == 0 contains_rhs_none = _basic_contains_count(rhs, lhs) == 0 - intersects = _basic_intersects_count(lhs, rhs) == 1 - return contains_lhs_none & contains_rhs_none & intersects + equals = lhs.geom_equals(rhs) + intersects = _basic_intersects_count(lhs, rhs) > 0 + return ~equals & contains_lhs_none & contains_rhs_none & intersects DispatchDict = { diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index a9ded241e..43874936b 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -369,6 +369,19 @@ def predicate(request): Polygon([(0.0, 1.0), (0.0, 2.0), (1.0, 2.0)]), point_polygon, ), + "polygon-polygon-overlap-inside-edge": ( + """ + x + /| + x---x | + \\ / | + x | + / | + x-----x + """, + Polygon([(0, 0), (1, 0), (1, 1), (0, 0)]), + Polygon([(0.25, 0.25), (0.5, 0.5), (0, 0.5), (0.25, 0.25)]), + ), "polygon-polygon-point-inside": ( """ x---x @@ -488,6 +501,7 @@ def predicate(request): "polygon-polygon-touch-point", "polygon-polygon-touch-edge", "polygon-polygon-overlap-edge", + "polygon-polygon-overlap-inside-edge", "polygon-polygon-point-inside", "polygon-polygon-point-outside", "polygon-polygon-in-out-point", From ba4e5c6034c09bc97f2fabbe752d50b19ea67f4e Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 18 May 2023 15:57:39 -0500 Subject: [PATCH 124/126] Add a new test case that blew up linestringlinestring touches and covers. --- .../core/binpreds/feature_contains.py | 9 +++- .../cuspatial/core/binpreds/feature_covers.py | 9 +++- .../core/binpreds/feature_touches.py | 42 ++++++++++++++++--- .../cuspatial/core/binpreds/feature_within.py | 5 +-- .../tests/binpreds/binpred_test_dispatch.py | 14 ++++++- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 51b0ab651..82445d5e9 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -144,8 +144,13 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): - count = _basic_equals_count(lhs, rhs) - return count == rhs.sizes + # A linestring A covers another linestring B iff + # no point in B is outside of A. + pli = _basic_intersects_pli(lhs, rhs) + points = _points_and_lines_to_multipoints(pli[1], pli[0]) + # Every point in B must be in the intersection + equals = _basic_equals_count(rhs, points) == rhs.sizes + return equals """DispatchDict listing the classes to use for each combination of diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 535993e8a..4594c1ba2 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -3,7 +3,6 @@ from cuspatial.core.binpreds.basic_predicates import ( _basic_contains_any, _basic_contains_count, - _basic_equals_all, _basic_equals_count, _basic_intersects_pli, ) @@ -51,7 +50,13 @@ class CoversPredicateBase(EqualsPredicateBase): class LineStringLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): - return _basic_equals_all(rhs, lhs) + # A linestring A covers another linestring B iff + # no point in B is outside of A. + pli = _basic_intersects_pli(lhs, rhs) + points = _points_and_lines_to_multipoints(pli[1], pli[0]) + # Every point in B must be in the intersection + equals = _basic_equals_count(rhs, points) == rhs.sizes + return equals class PolygonPointCovers(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 62a48a0d8..9550bf4ca 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -1,5 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +import cupy as cp + +import cudf + from cuspatial.core.binpreds.basic_predicates import ( _basic_contains_count, _basic_contains_properly_any, @@ -58,12 +62,38 @@ class LineStringLineStringTouches(BinPred): def _preprocess(self, lhs, rhs): """A and B have at least one point in common, and the common points lie in at least one boundary""" - # Linestrings are not equal - equals_all = _basic_equals_all(lhs, rhs) - # Linestrings do not cross - crosses = lhs.crosses(rhs) - intersects = lhs.intersects(rhs) - return intersects & ~crosses & ~equals_all + + # First compute pli which will contain points for line crossings and + # linestrings for overlapping segments. + pli = _basic_intersects_pli(lhs, rhs) + offsets = cudf.Series(pli[0]) + pli_geometry_count = offsets[1:].reset_index(drop=True) - offsets[ + :-1 + ].reset_index(drop=True) + indices = ( + cudf.Series(cp.arange(len(pli_geometry_count))) + .repeat(pli_geometry_count) + .reset_index(drop=True) + ) + + # In order to be a touch, all of the intersecting geometries + # for a particular row must be points. + pli_types = pli[1]._column._meta.input_types + point_intersection = _false_series(len(lhs)) + only_points_in_intersection = ( + pli_types.groupby(indices).sum().sort_index() == 0 + ) + point_intersection.iloc[ + only_points_in_intersection.index + ] = only_points_in_intersection + + # Finally, we need to check if the points in the intersection + # are equal to endpoints of either linestring. + points = _points_and_lines_to_multipoints(pli[1], pli[0]) + equals_lhs = _basic_equals_count(points, lhs) > 0 + equals_rhs = _basic_equals_count(points, rhs) > 0 + touches = point_intersection & (equals_lhs | equals_rhs) + return touches class LineStringPolygonTouches(BinPred): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index 57c9c5878..3b6ea133d 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -44,9 +44,8 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringWithin(BinPred): def _preprocess(self, lhs, rhs): - intersects = _basic_intersects(rhs, lhs) - equals = _basic_equals_all(rhs, lhs) - return intersects & equals + contains = rhs.contains(lhs) + return contains class LineStringPolygonWithin(BinPred): diff --git a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py index 43874936b..55ceeaea3 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/binpred_test_dispatch.py @@ -105,6 +105,17 @@ def predicate(request): LineString([(0.0, 0.0), (1.0, 0.0)]), LineString([(0.0, 0.0), (1.0, 0.0)]), ), + "linestring-linestring-covers": ( + """ + x + x + / + x + x + """, + LineString([(0.0, 0.0), (1.0, 1.0)]), + LineString([(0.25, 0.25), (0.5, 0.5)]), + ), "linestring-linestring-touches": ( """ x @@ -142,7 +153,7 @@ def predicate(request): """ x x - / \ + / \\ x---x x """, @@ -476,6 +487,7 @@ def predicate(request): linestring_linestring_dispatch_list = [ "linestring-linestring-disjoint", "linestring-linestring-same", + "linestring-linestring-covers", "linestring-linestring-touches", "linestring-linestring-touch-interior", "linestring-linestring-touch-edge", From 805d5f3ef8a310b9c0be1f708b28a4309b271792 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 18 May 2023 17:00:07 -0500 Subject: [PATCH 125/126] Improve PolygonLineStringCovers. --- .../cuspatial/core/binpreds/feature_covers.py | 20 +++++++++---------- .../cuspatial/utils/binpred_utils.py | 5 ----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 4594c1ba2..20193897f 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -20,7 +20,6 @@ MultiPoint, Point, Polygon, - _linestrings_is_degenerate, _points_and_lines_to_multipoints, _zero_series, ) @@ -66,23 +65,24 @@ def _preprocess(self, lhs, rhs): class PolygonLineStringCovers(BinPred): def _preprocess(self, lhs, rhs): + # A polygon covers a linestring if all of the points in the linestring + # are in the interior or exterior of the polygon. This differs from + # a polygon that contains a linestring in that some point of the + # linestring must be in the interior of the polygon. + # Count the number of points from rhs in the interior of lhs contains_count = _basic_contains_count(lhs, rhs) + # Now count the number of points from rhs in the boundary of lhs pli = _basic_intersects_pli(lhs, rhs) intersections = pli[1] + # There may be no intersection, so start with _zero_series equality = _zero_series(len(rhs)) - if len(intersections) == len(rhs): - # If the result is degenerate - is_degenerate = _linestrings_is_degenerate(intersections) - # If all the points in the intersection are in the rhs - equality = _basic_equals_count(intersections, rhs) - if len(is_degenerate) > 0: - equality[is_degenerate] = 1 - elif len(intersections) > 0: + if len(intersections) > 0: matching_length_multipoints = _points_and_lines_to_multipoints( intersections, pli[0] ) equality = _basic_equals_count(matching_length_multipoints, rhs) - return contains_count + equality >= rhs.sizes + covers = contains_count + equality >= rhs.sizes + return covers class PolygonPolygonCovers(BinPred): diff --git a/python/cuspatial/cuspatial/utils/binpred_utils.py b/python/cuspatial/cuspatial/utils/binpred_utils.py index 2750a5178..22b495513 100644 --- a/python/cuspatial/cuspatial/utils/binpred_utils.py +++ b/python/cuspatial/cuspatial/utils/binpred_utils.py @@ -417,8 +417,3 @@ def _multipoints_is_degenerate(geoseries): ) & (y1.reset_index(drop=True) == y2.reset_index(drop=True)) result[sizes_mask] = is_degenerate.reset_index(drop=True) return result - - -def _linestrings_is_degenerate(geoseries): - multipoints = _multipoints_from_geometry(geoseries) - return _multipoints_is_degenerate(multipoints) From 69691d858100fabd21020d8c80f9bf4070712f46 Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 25 May 2023 14:41:15 -0500 Subject: [PATCH 126/126] Final tweaks. --- .../core/binpreds/feature_contains.py | 2 - .../cuspatial/core/binpreds/feature_covers.py | 12 +--- .../core/binpreds/feature_intersects.py | 3 +- .../core/binpreds/feature_touches.py | 3 +- .../binpreds/test_binpred_test_dispatch.py | 56 ++++++++----------- 5 files changed, 25 insertions(+), 51 deletions(-) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 82445d5e9..562ce03b7 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -144,8 +144,6 @@ def _preprocess(self, lhs, rhs): class LineStringLineStringContainsPredicate(BinPred): def _preprocess(self, lhs, rhs): - # A linestring A covers another linestring B iff - # no point in B is outside of A. pli = _basic_intersects_pli(lhs, rhs) points = _points_and_lines_to_multipoints(pli[1], pli[0]) # Every point in B must be in the intersection diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 20193897f..94e25c254 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -28,20 +28,10 @@ class CoversPredicateBase(EqualsPredicateBase): """Implements the covers predicate across different combinations of geometry types. For example, a Point-Polygon covers predicate is - defined in terms of a Point-Point equals predicate. The initial release - implements covers predicates that depend only on the equals predicate, or - depend on no predicate, such as impossible cases like - `LineString.covers(Polygon)`. - - For this initial release, cover is supported for the following types: + defined in terms of a Point-Polygon equals predicate. Point.covers(Point) - Point.covers(Polygon) LineString.covers(Polygon) - Polygon.covers(Point) - Polygon.covers(MultiPoint) - Polygon.covers(LineString) - Polygon.covers(Polygon) """ pass diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index 9f8690277..c35947826 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -106,8 +106,7 @@ def _preprocess(self, lhs, rhs): class LineStringPointIntersects(IntersectsPredicateBase): def _preprocess(self, lhs, rhs): - intersects = _basic_intersects(lhs, rhs) - return intersects + return _basic_intersects(lhs, rhs) class PointLineStringIntersects(LineStringPointIntersects): diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py index 9550bf4ca..c1ddc1312 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -46,8 +46,7 @@ class TouchesPredicateBase(ContainsPredicate): """ def _preprocess(self, lhs, rhs): - equals = _basic_equals_any(lhs, rhs) - return equals + return _basic_equals_any(lhs, rhs) class PointPolygonTouches(ContainsPredicate): diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py index 9663e4136..11e5ad8f1 100644 --- a/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py +++ b/python/cuspatial/cuspatial/tests/binpreds/test_binpred_test_dispatch.py @@ -11,6 +11,25 @@ out_file = open("test_binpred_test_dispatch.log", "w") +def execute_test(pred, lhs, rhs): + gpdlhs = lhs.to_geopandas() + gpdrhs = rhs.to_geopandas() + + # Reverse + pred_fn = getattr(rhs, pred) + got = pred_fn(lhs) + gpd_pred_fn = getattr(gpdrhs, pred) + expected = gpd_pred_fn(gpdlhs) + assert (got.values_host == expected.values).all() + + # Forward + pred_fn = getattr(lhs, pred) + got = pred_fn(rhs) + gpd_pred_fn = getattr(gpdlhs, pred) + expected = gpd_pred_fn(gpdrhs) + assert (got.values_host == expected.values).all() + + def test_simple_features( predicate, # noqa: F811 simple_test, # noqa: F811 @@ -63,41 +82,10 @@ def test_simple_features( """ if not LOG_DISPATCHED_PREDICATES: (lhs, rhs) = simple_test[2], simple_test[3] - gpdlhs = lhs.to_geopandas() - gpdrhs = rhs.to_geopandas() - - # Reverse - pred_fn = getattr(rhs, predicate) - got = pred_fn(lhs) - gpd_pred_fn = getattr(gpdrhs, predicate) - expected = gpd_pred_fn(gpdlhs) - assert (got.values_host == expected.values).all() - - # Forward - pred_fn = getattr(lhs, predicate) - got = pred_fn(rhs) - gpd_pred_fn = getattr(gpdlhs, predicate) - expected = gpd_pred_fn(gpdrhs) - assert (got.values_host == expected.values).all() + execute_test(predicate, lhs, rhs) else: try: - (lhs, rhs) = simple_test[2], simple_test[3] - gpdlhs = lhs.to_geopandas() - gpdrhs = rhs.to_geopandas() - - # Reverse - pred_fn = getattr(rhs, predicate) - got = pred_fn(lhs) - gpd_pred_fn = getattr(gpdrhs, predicate) - expected = gpd_pred_fn(gpdlhs) - assert (got.values_host == expected.values).all() - - # Forward - pred_fn = getattr(lhs, predicate) - got = pred_fn(rhs) - gpd_pred_fn = getattr(gpdlhs, predicate) - expected = gpd_pred_fn(gpdrhs) - assert (got.values_host == expected.values).all() + execute_test(predicate, lhs, rhs) # The test is complete, the rest is just logging. try: @@ -127,7 +115,7 @@ def test_simple_features( ) passes_df.to_csv("feature_passes.csv", index=False) except Exception as e: - raise ValueError(e) + raise e except Exception as e: # The test failed, store the results. out_file.write(