From 965b80acaa7ce73363086f7b9350b12702957042 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 28 Feb 2023 10:18:10 -0800 Subject: [PATCH 01/36] initial --- .../detail/geometry/polygon_ref.cuh | 66 ++++++ .../geometry_collection/multipolygon_ref.cuh | 110 +++++++++ .../detail/point_polygon_distance.cuh | 87 +++++++ .../detail/ranges/multilinestring_range.cuh | 2 +- .../detail/ranges/multipolygon_range.cuh | 223 ++++++++++++++++++ .../experimental/geometry/polygon_ref.cuh | 64 +++++ .../geometry_collection/multipolygon_ref.cuh | 79 +++++++ .../experimental/point_polygon_distance.cuh | 41 ++++ .../ranges/multipolygon_range.cuh | 136 +++++++++++ 9 files changed, 807 insertions(+), 1 deletion(-) create mode 100644 cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh create mode 100644 cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh create mode 100644 cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh create mode 100644 cpp/include/cuspatial/experimental/point_polygon_distance.cuh create mode 100644 cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh diff --git a/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh new file mode 100644 index 000000000..8b156ea6a --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.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 "linestring_ref.cuh" + +#include +#include +#include +#include + +#include +#include + +namespace cuspatial { + +template +CUSPATIAL_HOST_DEVICE polygon_ref::polygon_ref(RingIterator ring_begin, + RingIterator ring_end, + VecIterator point_begin, + VecIterator point_end) + : _ring_begin(ring_begin), _ring_end(ring_end), _point_begin(begin), _point_end(end) +{ + using T = iterator_vec_base_type; + static_assert(is_same, iterator_value_type>(), "must be vec2d type"); +} + +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::num_rings() const +{ + return thrust::distance(_point_begin, _point_end) - 1; +} + +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::ring_begin() const +{ + return detail::make_counting_transform_iterator(0, to_linestring_ref{_point_begin}); +} + +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::ring_end() const +{ + return ring_begin() + num_rings(); +} + +template +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::ring(IndexType i) const +{ + return *(ring_begin() + i); +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh new file mode 100644 index 000000000..fe4e60cde --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace cuspatial { + +template +struct to_polygon_functor { + using difference_type = typename thrust::iterator_difference::type; + PartIterator part_begin; + RingIterator ring_begin; + VecIterator point_begin; + + CUSPATIAL_HOST_DEVICE + to_polygon_functor(PartIterator part_begin, RingIterator ring_begin, VecIterator point_begin) + : part_begin(part_begin), ring_begin(ring_begin), point_begin(point_begin) + { + } + + CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) + { + return polygon_ref{point_begin + part_begin[i], point_begin + part_begin[i + 1]}; + } +}; + +template +class multipolygon_ref; + +template +CUSPATIAL_HOST_DEVICE multipolygon_ref::multipolygon_ref( + PartIterator part_begin, + PartIterator part_end, + RingIterator ring_begin, + RingIterator ring_end, + VecIterator point_begin, + VecIterator point_end) + : _part_begin(part_begin), + _part_end(part_end), + _ring_begin(ring_begin), + _ring_end(ring_end), + _point_begin(point_begin), + _point_end(point_end) +{ +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::num_polygons() + const +{ + return thrust::distance(_part_begin, _part_end) - 1; +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::part_begin() + const +{ + return detail::make_counting_transform_iterator( + 0, to_polygon_functor{_part_begin, _ring_begin, _point_begin}); +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::part_end() + const +{ + return part_begin() + num_polygons(); +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::ring_begin() + const +{ + return detail::make_counting_transform_iterator(0, + to_linestring_functor{_part_begin, _point_begin}); +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::ring_end() + const +{ + return part_begin() + num_polygons(); +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::point_begin() + const +{ + return _point_begin; +} + +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::point_end() + const +{ + return _point_end; +} + +template +template +CUSPATIAL_HOST_DEVICE auto multipolygon_ref::operator[]( + IndexType i) const +{ + return *(part_begin() + i); +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh new file mode 100644 index 000000000..b51c252aa --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -0,0 +1,87 @@ +/* + * 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 +#include +#include + +namespace cuspatial { +namespace detail { + +/** + * @brief Kernel to compute the distance between pairs of point and polygon. + */ +template +void __global__ pairwise_point_polygon_distance_kernel(MultiPointRange multipoints, + MultiPolygonRange multipolygons, + OutputIterator distances) +{ + using T = typename MultiPointRange::element_t; + + for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < multipolygons.num_points(); + idx += gridDim.x * blockDim.x) { + auto geometry_idx = multipolygons.geometry_idx_from_point_idx(idx); + } +} + +} // namespace detail +template +OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, + MultiPolygonRange multipoiygons, + OutputIt distances_first, + rmm::cuda_stream_view stream) +{ + using T = typename MultiPointRange::element_t; + + static_assert(is_same_floating_point(), + "Inputs must have same floating point value type."); + + static_assert( + is_same, typename MultiPointRange::point_t, typename MultiPolygonRange::point_t>(), + "Inputs must be cuspatial::vec_2d"); + + CUSPATIAL_EXPECTS(multipoints.size() == multipolygons.size(), + "Must have the same number of input rows."); + + auto [threads_per_block, n_blocks] = grid_id(multipolygons.num_points()); + + pairwise_point_polygon_distance_kernel<<>>( + multipoints, multipolygons, distances_first); + + CUSPATIAL_CHECK_CUDA(stream.value()); +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multilinestring_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multilinestring_range.cuh index dd373be3f..851269313 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multilinestring_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multilinestring_range.cuh @@ -16,7 +16,6 @@ #pragma once -#include #include #include #include @@ -29,6 +28,7 @@ #include #include +#include namespace cuspatial { diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh new file mode 100644 index 000000000..7fe9a9387 --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -0,0 +1,223 @@ +/* + * 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 +#include + +namespace cuspatial { + +using namespace detail; + +template +struct to_multipolygon_functor { + using difference_type = typename thrust::iterator_difference::type; + GeometryIterator _geometry_begin; + PartIterator _part_begin; + RingIterator _ring_begin; + VecIterator _point_begin; + VecIterator _point_end; + + CUSPATIAL_HOST_DEVICE + to_multipolygon_functor(GeometryIterator geometry_begin, + PartIterator part_begin, + RingIterator ring_begin, + VecIterator point_begin, + VecIterator point_end) + : _geometry_begin(geometry_begin), + _part_begin(part_begin), + _ring_begin(ring_begin), + _point_begin(point_begin), + _point_end(point_end) + { + } + + CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) + { + return multipolygon_ref{_part_begin + _geometry_begin[i], + thrust::next(_part_begin + _geometry_begin[i + 1]), + _point_begin, + _point_end}; + } +}; + +template +class multipolygon_range; + +template +multipolygon_range::multipolygon_range( + GeometryIterator geometry_begin, + GeometryIterator geometry_end, + PartIterator part_begin, + PartIterator part_end, + VecIterator point_begin, + VecIterator point_end) + : _geometry_begin(geometry_begin), + _geometry_end(geometry_end), + _part_begin(part_begin), + _part_end(part_end), + _point_begin(point_begin), + _point_end(point_end) +{ +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::num_multipolygons() +{ + return thrust::distance(_geometry_begin, _geometry_end) - 1; +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::num_polygons() +{ + return thrust::distance(_part_begin, _part_end) - 1; +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::num_rings() +{ + return thrust::distance(_ring_begin, _ring_end) - 1; +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::num_points() +{ + return thrust::distance(_point_begin, _point_end); +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::multipolygon_begin() +{ + return detail::make_counting_transform_iterator( + 0, + to_multipolygon_functor{_geometry_begin, _part_begin, _ring_begin, _point_begin, _point_end}); +} + +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::multipolygon_end() +{ + return multipolygon_begin() + num_multipolygons(); +} + +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::ring_idx_from_point_idx( + IndexType point_idx) +{ + return thrust::distance(_ring_begin, + thrust::prev(thrust::upper_bound(_ring_begin, _ring_end, point_idx))); +} + +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::part_idx_from_ring_idx( + IndexType ring_idx) +{ + return thrust::distance(_part_begin, + thrust::prev(thrust::upper_bound(_part_begin, _part_begin, ring_idx))); +} + +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::geometry_idx_from_part_idx( + IndexType part_idx) +{ + return thrust::distance( + _geometry_begin, thrust::prev(thrust::upper_bound(_geometry_begin, _geometry_end, part_idx))); +} + +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::geometry_idx_from_point_idx( + IndexType point_idx) +{ + return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx_from_part_idx(point_idx))); +} + +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::operator[]( + IndexType multipolygon_idx) +{ + return multipolygon_begin()[multipolygon_idx]; +} + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh b/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh new file mode 100644 index 000000000..42c3ce1a9 --- /dev/null +++ b/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh @@ -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 + +namespace cuspatial { + +/** + * @brief Represent a reference to a polygon stored in a structure of arrays. + * + * @tparam VecIterator type of iterator to the underlying point array. + */ +template +class polygon_ref { + public: + CUSPATIAL_HOST_DEVICE polygon_ref(RingIterator ring_begin, + RingIterator ring_end, + VecIterator point_begin, + VecIterator point_end); + + /// Return the number of rings in the polygon + CUSPATIAL_HOST_DEVICE auto num_rings() const; + + /// Return iterator to the first ring of the polygon + CUSPATIAL_HOST_DEVICE auto ring_begin() const; + /// Return iterator to one past the last ring + CUSPATIAL_HOST_DEVICE auto ring_end() const; + + /// Return iterator to the first ring of the polygon + CUSPATIAL_HOST_DEVICE auto begin() const { return ring_begin(); } + /// Return iterator to one past the last ring + CUSPATIAL_HOST_DEVICE auto end() const { return ring_end(); } + + /// Return an enumerated range to the rings. + CUSPATIAL_HOST_DEVICE auto enumerate() { return detail::enumerate_range{begin(), end()}; } + + /// Return the `ring_idx`th ring in the polygon. + template + CUSPATIAL_HOST_DEVICE auto ring(IndexType ring_idx) const; + + protected: + RingIterator _ring_begin; + RingIterator _ring_end; + VecIterator _point_begin; + VecIterator _point_end; +}; + +} // namespace cuspatial +#include diff --git a/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh b/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh new file mode 100644 index 000000000..a6aab7cc4 --- /dev/null +++ b/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh @@ -0,0 +1,79 @@ +/* + * 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 + +namespace cuspatial { + +/** + * @brief Represent a reference to a multipolygon stored in a structure of arrays. + * + * @tparam PartIterator type of iterator to the part offset array. + * @tparam VecIterator type of iterator to the underlying point array. + */ +template +class multipolygon_ref { + public: + CUSPATIAL_HOST_DEVICE multipolygon_ref(PartIterator part_begin, + PartIterator part_end, + VecIterator point_begin, + VecIterator point_end); + /// Return the number of polygons in the multipolygon. + CUSPATIAL_HOST_DEVICE auto num_polygons() const; + /// Return the number of polygons in the multipolygon. + CUSPATIAL_HOST_DEVICE auto size() const { return num_polygons(); } + + /// Return iterator to the first polygon. + CUSPATIAL_HOST_DEVICE auto part_begin() const; + /// Return iterator to one past the last polygon. + CUSPATIAL_HOST_DEVICE auto part_end() const; + + /// Return iterator to the first polygon. + CUSPATIAL_HOST_DEVICE auto ring_begin() const; + /// Return iterator to one past the last polygon. + CUSPATIAL_HOST_DEVICE auto ring_begin() const; + + /// Return iterator to the first point of the multipolygon. + CUSPATIAL_HOST_DEVICE auto point_begin() const; + /// Return iterator to one past the last point of the multipolygon. + CUSPATIAL_HOST_DEVICE auto point_end() const; + + /// Return iterator to the first polygon of the multipolygon. + CUSPATIAL_HOST_DEVICE auto begin() const { return part_begin(); } + /// Return iterator to one past the last polygon of the multipolygon. + CUSPATIAL_HOST_DEVICE auto end() const { return part_end(); } + + /// Return an enumerated range to the polygons. + CUSPATIAL_HOST_DEVICE auto enumerate() const { return detail::enumerate_range{begin(), end()}; } + + /// Return `polygon_idx`th polygon in the multipolygon. + template + CUSPATIAL_HOST_DEVICE auto operator[](IndexType polygon_idx) const; + + protected: + PartIterator _part_begin; + PartIterator _part_end; + RingIterator _ring_begin; + RingIterator _ring_end; + VecIterator _point_begin; + VecIterator _point_end; +}; + +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/experimental/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/point_polygon_distance.cuh new file mode 100644 index 000000000..c5394e902 --- /dev/null +++ b/cpp/include/cuspatial/experimental/point_polygon_distance.cuh @@ -0,0 +1,41 @@ +/* + * 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 + +namespace cuspatial { + +/** + * @ingroup distance + * @copybrief cuspatial::pairwise_point_polygon_distance + * + * @tparam MultiPointRange An instance of template type `MultiPointRange` + * @tparam MultiPolygonRangeB An instance of template type `MultiPolygonRange` + * + * @param multipoints Range of multipoints in each distance pair. + * @param multipolygons Range of multipolygons in each distance pair. + * @return Iterator past the last distance computed + */ +template +OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, + MultiPolygonRangeB multipoiygons, + OutputIt distances_first, + rmm::cuda_stream_view stream = rmm::cuda_stream_default); +} // namespace cuspatial + +#include diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh new file mode 100644 index 000000000..331302348 --- /dev/null +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -0,0 +1,136 @@ +/* + * 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 + +namespace cuspatial { + +/** + * @brief Non-owning range-based interface to multipolygon data + * @ingroup ranges + * + * Provides a range-based interface to contiguous storage of multipolygon data, to make it easier + * to access and iterate over multipolygons, polygons, rings and points. + * + * Conforms to GeoArrow's specification of multipolygon: + * https://github.com/geopandas/geo-arrow-spec/blob/main/format.md + * + * @tparam GeometryIterator iterator type for the geometry offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI]. + * @tparam PartIterator iterator type for the part offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI]. + * @tparam RingIterator iterator type for the ring offset array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI]. + * @tparam VecIterator iterator type for the point array. Must meet + * the requirements of [LegacyRandomAccessIterator][LinkLRAI]. + * + * @note Though this object is host/device compatible, + * The underlying iterator must be device accessible if used in device kernel. + * + * [LinkLRAI]: https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator + * "LegacyRandomAccessIterator" + */ +template +class multipolygon_range { + public: + using geometry_it_t = GeometryIterator; + using part_it_t = PartIterator; + using ring_it_t = RingIterator; + using point_it_t = VecIterator; + using point_t = iterator_value_type; + using element_t = iterator_vec_base_type; + + multipolygon_range(GeometryIterator geometry_begin, + GeometryIterator geometry_end, + PartIterator part_begin, + PartIterator part_end, + RingIterator ring_begin, + RingIterator ring_end, + VecIterator points_begin, + VecIterator points_end); + + /// Return the number of multipolygons in the array. + CUSPATIAL_HOST_DEVICE auto size() { return num_multipolygons(); } + + /// Return the number of multipolygons in the array. + CUSPATIAL_HOST_DEVICE auto num_multipolygons(); + + /// Return the total number of polygons in the array. + CUSPATIAL_HOST_DEVICE auto num_polygons(); + + /// Return the total number of rings in the array. + CUSPATIAL_HOST_DEVICE auto num_rings(); + + /// Return the total number of points in the array. + CUSPATIAL_HOST_DEVICE auto num_points(); + + /// Return the iterator to the first multipolygon in the range. + CUSPATIAL_HOST_DEVICE auto multipolygon_begin(); + + /// Return the iterator to the one past the last multipolygon in the range. + CUSPATIAL_HOST_DEVICE auto multipolygon_end(); + + /// Return the iterator to the first multipolygon in the range. + CUSPATIAL_HOST_DEVICE auto begin() { return multipolygon_begin(); } + + /// Return the iterator to the one past the last multipolygon in the range. + CUSPATIAL_HOST_DEVICE auto end() { return multipolygon_end(); } + + /// Given the index of a point, return the ring index where the point locates. + template + CUSPATIAL_HOST_DEVICE auto ring_idx_from_point_idx(IndexType point_idx); + + /// Given the index of a ring, return the part (polygon) index + /// where the polygon locates. + template + CUSPATIAL_HOST_DEVICE auto part_idx_from_ring_idx(IndexType ring_idx); + + /// Given the index of a part (polygon), return the geometry (multipolygon) index + /// where the polygon locates. + template + CUSPATIAL_HOST_DEVICE auto geometry_idx_from_part_idx(IndexType part_idx); + + /// Given the index of a point, return the geometry (multipolygon) index where the + /// point locates. + template + CUSPATIAL_HOST_DEVICE auto geometry_idx_from_point_idx(IndexType point_idx); + + /// Returns the `multipolygon_idx`th multipolygon in the range. + template + CUSPATIAL_HOST_DEVICE auto operator[](IndexType multipolygon_idx); + + protected: + GeometryIterator _geometry_begin; + GeometryIterator _geometry_end; + PartIterator _part_begin; + PartIterator _part_end; + VecIterator _point_begin; + VecIterator _point_end; +}; + +} // namespace cuspatial + +#include From 02f61feca933bca3de3f545d7331b1f2d8fb678b Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:01:25 -0800 Subject: [PATCH 02/36] add pragma once for floating_point.cuh --- cpp/include/cuspatial/detail/utility/floating_point.cuh | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/include/cuspatial/detail/utility/floating_point.cuh b/cpp/include/cuspatial/detail/utility/floating_point.cuh index 744c7cb88..7a44aeb73 100644 --- a/cpp/include/cuspatial/detail/utility/floating_point.cuh +++ b/cpp/include/cuspatial/detail/utility/floating_point.cuh @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once #include From 7bd797fc23864c9cc7a83e0e4ac743a0b64ebdb4 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:02:20 -0800 Subject: [PATCH 03/36] add polygon_ref structure --- .../detail/geometry/polygon_ref.cuh | 26 ++++++++++++++----- .../experimental/geometry/polygon_ref.cuh | 12 ++++++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh index 8b156ea6a..02a917cd4 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh @@ -15,11 +15,10 @@ */ #pragma once -#include "linestring_ref.cuh" - #include #include #include +#include #include #include @@ -32,28 +31,41 @@ CUSPATIAL_HOST_DEVICE polygon_ref::polygon_ref(RingIt RingIterator ring_end, VecIterator point_begin, VecIterator point_end) - : _ring_begin(ring_begin), _ring_end(ring_end), _point_begin(begin), _point_end(end) + : _ring_begin(ring_begin), _ring_end(ring_end), _point_begin(point_begin), _point_end(point_end) { - using T = iterator_vec_base_type; + using T = iterator_vec_base_type; static_assert(is_same, iterator_value_type>(), "must be vec2d type"); } template CUSPATIAL_HOST_DEVICE auto polygon_ref::num_rings() const { - return thrust::distance(_point_begin, _point_end) - 1; + return thrust::distance(_ring_begin, _ring_end) - 1; } template CUSPATIAL_HOST_DEVICE auto polygon_ref::ring_begin() const { - return detail::make_counting_transform_iterator(0, to_linestring_ref{_point_begin}); + return detail::make_counting_transform_iterator(0, + to_linestring_functor{_ring_begin, _point_begin}); } template CUSPATIAL_HOST_DEVICE auto polygon_ref::ring_end() const { - return ring_begin() + num_rings(); + return ring_begin() + size(); +} + +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::point_begin() const +{ + return _point_begin; +} + +template +CUSPATIAL_HOST_DEVICE auto polygon_ref::point_end() const +{ + return _point_end; } template diff --git a/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh b/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh index 42c3ce1a9..5905eab2c 100644 --- a/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry/polygon_ref.cuh @@ -16,7 +16,6 @@ #pragma once #include -#include namespace cuspatial { @@ -36,19 +35,24 @@ class polygon_ref { /// Return the number of rings in the polygon CUSPATIAL_HOST_DEVICE auto num_rings() const; + /// Return the number of rings in the polygon + CUSPATIAL_HOST_DEVICE auto size() const { return num_rings(); } + /// Return iterator to the first ring of the polygon CUSPATIAL_HOST_DEVICE auto ring_begin() const; /// Return iterator to one past the last ring CUSPATIAL_HOST_DEVICE auto ring_end() const; + /// Return iterator to the first point of the polygon + CUSPATIAL_HOST_DEVICE auto point_begin() const; + /// Return iterator to one past the last point + CUSPATIAL_HOST_DEVICE auto point_end() const; + /// Return iterator to the first ring of the polygon CUSPATIAL_HOST_DEVICE auto begin() const { return ring_begin(); } /// Return iterator to one past the last ring CUSPATIAL_HOST_DEVICE auto end() const { return ring_end(); } - /// Return an enumerated range to the rings. - CUSPATIAL_HOST_DEVICE auto enumerate() { return detail::enumerate_range{begin(), end()}; } - /// Return the `ring_idx`th ring in the polygon. template CUSPATIAL_HOST_DEVICE auto ring(IndexType ring_idx) const; From 0968c15b72d3632700b284bacd4dbc9f2481bfa5 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:04:23 -0800 Subject: [PATCH 04/36] add multipolygon_ref class --- .../geometry_collection/multipolygon_ref.cuh | 15 +++++++++++---- .../geometry_collection/multipolygon_ref.cuh | 10 ++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh index fe4e60cde..940c6ef83 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multipolygon_ref.cuh @@ -15,16 +15,23 @@ struct to_polygon_functor { PartIterator part_begin; RingIterator ring_begin; VecIterator point_begin; + VecIterator point_end; CUSPATIAL_HOST_DEVICE - to_polygon_functor(PartIterator part_begin, RingIterator ring_begin, VecIterator point_begin) - : part_begin(part_begin), ring_begin(ring_begin), point_begin(point_begin) + to_polygon_functor(PartIterator part_begin, + RingIterator ring_begin, + VecIterator point_begin, + VecIterator point_end) + : part_begin(part_begin), ring_begin(ring_begin), point_begin(point_begin), point_end(point_end) { } CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) { - return polygon_ref{point_begin + part_begin[i], point_begin + part_begin[i + 1]}; + return polygon_ref{ring_begin + part_begin[i], + thrust::next(ring_begin + part_begin[i + 1]), + point_begin, + point_end}; } }; @@ -60,7 +67,7 @@ CUSPATIAL_HOST_DEVICE auto multipolygon_ref diff --git a/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh b/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh index a6aab7cc4..64da05797 100644 --- a/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry_collection/multipolygon_ref.cuh @@ -26,11 +26,13 @@ namespace cuspatial { * @tparam PartIterator type of iterator to the part offset array. * @tparam VecIterator type of iterator to the underlying point array. */ -template +template class multipolygon_ref { public: CUSPATIAL_HOST_DEVICE multipolygon_ref(PartIterator part_begin, PartIterator part_end, + RingIterator ring_begin, + RingIterator ring_end, VecIterator point_begin, VecIterator point_end); /// Return the number of polygons in the multipolygon. @@ -43,10 +45,10 @@ class multipolygon_ref { /// Return iterator to one past the last polygon. CUSPATIAL_HOST_DEVICE auto part_end() const; - /// Return iterator to the first polygon. - CUSPATIAL_HOST_DEVICE auto ring_begin() const; - /// Return iterator to one past the last polygon. + /// Return iterator to the first ring. CUSPATIAL_HOST_DEVICE auto ring_begin() const; + /// Return iterator to one past the last ring. + CUSPATIAL_HOST_DEVICE auto ring_end() const; /// Return iterator to the first point of the multipolygon. CUSPATIAL_HOST_DEVICE auto point_begin() const; From a659eab1df056666af3fec2ac382c1221f6c5d91 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:05:06 -0800 Subject: [PATCH 05/36] update multipolygon_range class --- .../detail/ranges/multipolygon_range.cuh | 104 ++++++++++++++---- .../ranges/multipolygon_range.cuh | 51 ++++++--- 2 files changed, 114 insertions(+), 41 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index 7fe9a9387..b44420532 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -43,6 +44,7 @@ struct to_multipolygon_functor { GeometryIterator _geometry_begin; PartIterator _part_begin; RingIterator _ring_begin; + RingIterator _ring_end; VecIterator _point_begin; VecIterator _point_end; @@ -50,11 +52,13 @@ struct to_multipolygon_functor { to_multipolygon_functor(GeometryIterator geometry_begin, PartIterator part_begin, RingIterator ring_begin, + RingIterator ring_end, VecIterator point_begin, VecIterator point_end) : _geometry_begin(geometry_begin), _part_begin(part_begin), _ring_begin(ring_begin), + _ring_end(ring_end), _point_begin(point_begin), _point_end(point_end) { @@ -62,8 +66,22 @@ struct to_multipolygon_functor { CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) { + auto new_part_begin = _part_begin + _geometry_begin[i]; + auto new_part_end = thrust::next(_part_begin, _geometry_begin[i + 1] + 1); + + printf( + "In to_multipolygon_functor: %d %d %d\n\t new_part_begin_val: %d, " + "new_part_end_dist_from_begin: %d\n", + static_cast(i), + static_cast(_geometry_begin[i]), + static_cast(_geometry_begin[i + 1]), + static_cast(*new_part_begin), + static_cast(thrust::distance(new_part_end, new_part_begin))); + return multipolygon_ref{_part_begin + _geometry_begin[i], - thrust::next(_part_begin + _geometry_begin[i + 1]), + thrust::next(_part_begin, _geometry_begin[i + 1] + 1), + _ring_begin, + _ring_end, _point_begin, _point_end}; } @@ -79,17 +97,21 @@ template -multipolygon_range::multipolygon_range( +multipolygon_range::multipolygon_range( GeometryIterator geometry_begin, GeometryIterator geometry_end, PartIterator part_begin, PartIterator part_end, + RingIterator ring_begin, + RingIterator ring_end, VecIterator point_begin, VecIterator point_end) : _geometry_begin(geometry_begin), _geometry_end(geometry_end), _part_begin(part_begin), _part_end(part_end), + _ring_begin(ring_begin), + _ring_end(ring_end), _point_begin(point_begin), _point_end(point_end) { @@ -100,7 +122,7 @@ template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::num_multipolygons() +multipolygon_range::num_multipolygons() { return thrust::distance(_geometry_begin, _geometry_end) - 1; } @@ -110,7 +132,7 @@ template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::num_polygons() +multipolygon_range::num_polygons() { return thrust::distance(_part_begin, _part_end) - 1; } @@ -120,7 +142,7 @@ template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::num_rings() +multipolygon_range::num_rings() { return thrust::distance(_ring_begin, _ring_end) - 1; } @@ -130,7 +152,7 @@ template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::num_points() +multipolygon_range::num_points() { return thrust::distance(_point_begin, _point_end); } @@ -140,11 +162,12 @@ template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::multipolygon_begin() +multipolygon_range::multipolygon_begin() { return detail::make_counting_transform_iterator( 0, - to_multipolygon_functor{_geometry_begin, _part_begin, _ring_begin, _point_begin, _point_end}); + to_multipolygon_functor{ + _geometry_begin, _part_begin, _ring_begin, _ring_end, _point_begin, _point_end}); } template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::multipolygon_end() +multipolygon_range::multipolygon_end() { return multipolygon_begin() + num_multipolygons(); } @@ -163,11 +186,11 @@ template template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::ring_idx_from_point_idx( - IndexType point_idx) +multipolygon_range:: + ring_idx_from_point_idx(IndexType point_idx) { - return thrust::distance(_ring_begin, - thrust::prev(thrust::upper_bound(_ring_begin, _ring_end, point_idx))); + return thrust::distance( + _ring_begin, thrust::prev(thrust::upper_bound(thrust::seq, _ring_begin, _ring_end, point_idx))); } template template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::part_idx_from_ring_idx( - IndexType ring_idx) +multipolygon_range:: + part_idx_from_ring_idx(IndexType ring_idx) { - return thrust::distance(_part_begin, - thrust::prev(thrust::upper_bound(_part_begin, _part_begin, ring_idx))); + return thrust::distance( + _part_begin, + thrust::prev(thrust::upper_bound(thrust::seq, _part_begin, _part_begin, ring_idx))); } template template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::geometry_idx_from_part_idx( - IndexType part_idx) +multipolygon_range:: + geometry_idx_from_part_idx(IndexType part_idx) { return thrust::distance( - _geometry_begin, thrust::prev(thrust::upper_bound(_geometry_begin, _geometry_end, part_idx))); + _geometry_begin, + thrust::prev(thrust::upper_bound(thrust::seq, _geometry_begin, _geometry_end, part_idx))); } template template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::geometry_idx_from_point_idx( - IndexType point_idx) +multipolygon_range:: + geometry_idx_from_segment_idx(IndexType segment_idx) +{ + auto ring_idx = ring_idx_from_point_idx(segment_idx); + if (!is_valid_segment_id(segment_idx, ring_idx)) + return multipolygon_range:: + INVALID_INDEX; + return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx)); +} + +template +template +CUSPATIAL_HOST_DEVICE bool +multipolygon_range::is_valid_segment_id( + IndexType1 point_idx, IndexType2 ring_idx) { - return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx_from_part_idx(point_idx))); + if constexpr (std::is_signed_v) + return point_idx >= 0 && point_idx < (_ring_begin[ring_idx + 1] - 1); + else + return point_idx < (_ring_begin[ring_idx + 1] - 1); } template template CUSPATIAL_HOST_DEVICE auto -multipolygon_range::operator[]( +multipolygon_range::operator[]( IndexType multipolygon_idx) { return multipolygon_begin()[multipolygon_idx]; } +template +template +CUSPATIAL_HOST_DEVICE auto +multipolygon_range::get_segment( + IndexType segment_idx) +{ + return segment{_point_begin[segment_idx], _point_begin[segment_idx + 1]}; +} + } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 331302348..b9efd64e5 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -63,6 +63,8 @@ class multipolygon_range { using point_t = iterator_value_type; using element_t = iterator_vec_base_type; + int64_t static constexpr INVALID_INDEX = -1; + multipolygon_range(GeometryIterator geometry_begin, GeometryIterator geometry_end, PartIterator part_begin, @@ -98,37 +100,52 @@ class multipolygon_range { /// Return the iterator to the one past the last multipolygon in the range. CUSPATIAL_HOST_DEVICE auto end() { return multipolygon_end(); } - - /// Given the index of a point, return the ring index where the point locates. + /// Given the index of a segment, return the geometry (multipolygon) index where the + /// segment locates. + /// Segment index is the index to the starting point of the segment. If the + /// index is the last point of the ring, then it is not a valid index. + /// This function returns multipolygon_range::INVALID_INDEX if the index is invalid. template - CUSPATIAL_HOST_DEVICE auto ring_idx_from_point_idx(IndexType point_idx); + CUSPATIAL_HOST_DEVICE auto geometry_idx_from_segment_idx(IndexType segment_idx); - /// Given the index of a ring, return the part (polygon) index - /// where the polygon locates. + /// Returns the `multipolygon_idx`th multipolygon in the range. template - CUSPATIAL_HOST_DEVICE auto part_idx_from_ring_idx(IndexType ring_idx); + CUSPATIAL_HOST_DEVICE auto operator[](IndexType multipolygon_idx); - /// Given the index of a part (polygon), return the geometry (multipolygon) index - /// where the polygon locates. - template - CUSPATIAL_HOST_DEVICE auto geometry_idx_from_part_idx(IndexType part_idx); + // template + // CUSPATIAL_HOST_DEVICE auto get_point(IndexType point_idx); - /// Given the index of a point, return the geometry (multipolygon) index where the - /// point locates. template - CUSPATIAL_HOST_DEVICE auto geometry_idx_from_point_idx(IndexType point_idx); - - /// Returns the `multipolygon_idx`th multipolygon in the range. - template - CUSPATIAL_HOST_DEVICE auto operator[](IndexType multipolygon_idx); + CUSPATIAL_HOST_DEVICE auto get_segment(IndexType segment_idx); protected: GeometryIterator _geometry_begin; GeometryIterator _geometry_end; PartIterator _part_begin; PartIterator _part_end; + RingIterator _ring_begin; + RingIterator _ring_end; VecIterator _point_begin; VecIterator _point_end; + + private: + /// Given the index of a point, return the ring index + /// where the point locates. + template + CUSPATIAL_HOST_DEVICE auto ring_idx_from_point_idx(IndexType point_idx); + + /// Given the index of a ring, return the part (polygon) index + /// where the ring locates. + template + CUSPATIAL_HOST_DEVICE auto part_idx_from_ring_idx(IndexType ring_idx); + + /// Given the index of a part (polygon), return the geometry (multipolygon) index + /// where the polygon locates. + template + CUSPATIAL_HOST_DEVICE auto geometry_idx_from_part_idx(IndexType part_idx); + + template + CUSPATIAL_HOST_DEVICE bool is_valid_segment_id(IndexType1 segment_idx, IndexType2 ring_idx); }; } // namespace cuspatial From c2740702c2497774d05a377f91a58ee0bec1bee3 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:05:53 -0800 Subject: [PATCH 06/36] update multipoint_range class --- .../experimental/detail/ranges/multipoint_range.cuh | 7 +++++++ .../experimental/ranges/multipoint_range.cuh | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh index 08b3d5d5e..d3d8926d5 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh @@ -133,4 +133,11 @@ multipoint_range::geometry_idx_from_point_idx(Ind thrust::prev(thrust::upper_bound(thrust::seq, _geometry_begin, _geometry_end, idx))); } +template +template +CUSPATIAL_HOST_DEVICE auto multipoint_range::point(IndexType idx) +{ + return _points_begin[idx]; +} + } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh index 366f9c18e..1ee08b5cc 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh @@ -51,6 +51,8 @@ class multipoint_range { using point_t = iterator_value_type; using element_t = iterator_vec_base_type; + int32_t INVALID_IDX = -1; + /** * @brief Construct a new multipoint array object */ @@ -129,6 +131,16 @@ class multipoint_range { template CUSPATIAL_HOST_DEVICE auto operator[](IndexType idx); + /** + * @brief Returns the `idx`th point in the array. + * + * @tparam IndexType type of the index + * @param idx the index to the point + * @return a vec_2d object + */ + template + CUSPATIAL_HOST_DEVICE auto point(IndexType idx); + protected: /// Iterator to the start of the index array of start positions to each multipoint. GeometryIterator _geometry_begin; From 12ffa53f6a917a18bf103fc02c8eb11884b84464 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:06:36 -0800 Subject: [PATCH 07/36] update is_point_in_polygon usage with polygon_ref --- .../detail/is_point_in_polygon.cuh | 104 ------------------ .../detail/pairwise_point_in_polygon.cuh | 4 +- .../experimental/detail/point_in_polygon.cuh | 4 +- 3 files changed, 4 insertions(+), 108 deletions(-) delete mode 100644 cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh diff --git a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh deleted file mode 100644 index b9c7d5ac6..000000000 --- a/cpp/include/cuspatial/experimental/detail/is_point_in_polygon.cuh +++ /dev/null @@ -1,104 +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 - -namespace cuspatial { -namespace detail { - -/** - * @brief Kernel to test if a point is inside a polygon. - * - * Implemented based on Eric Haines's crossings-multiply algorithm: - * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ - * The improvement in addenda is also addopted to remove divisions in this kernel. - * - * TODO: the ultimate goal of refactoring this as independent function is to remove - * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't - * possible today without further work to refactor quadtree_point_in_polygon into header only - * API. - */ -template ::difference_type, - class Cart2dItDiffType = typename std::iterator_traits::difference_type> -__device__ inline bool is_point_in_polygon(Cart2d const& test_point, - OffsetType poly_begin, - OffsetType poly_end, - OffsetIterator ring_offsets_first, - OffsetItDiffType const& num_rings, - Cart2dIt poly_points_first, - Cart2dItDiffType const& num_poly_points) -{ - using T = iterator_vec_base_type; - - bool point_is_within = false; - bool is_colinear = false; - // for each ring - for (auto ring_idx = poly_begin; ring_idx < poly_end; ring_idx++) { - int32_t ring_idx_next = ring_idx + 1; - int32_t ring_begin = ring_offsets_first[ring_idx]; - int32_t ring_end = - (ring_idx_next < num_rings) ? ring_offsets_first[ring_idx_next] : num_poly_points; - - Cart2d b = poly_points_first[ring_end - 1]; - bool y0_flag = b.y > test_point.y; - bool y1_flag; - // for each line segment, including the segment between the last and first vertex - for (auto point_idx = ring_begin; point_idx < ring_end; point_idx++) { - Cart2d const a = poly_points_first[point_idx]; - T run = b.x - a.x; - T rise = b.y - a.y; - - // Points on the line segment are the same, so intersection is impossible. - // This is possible because we allow closed or unclosed polygons. - T constexpr zero = 0.0; - if (float_equal(run, zero) && float_equal(rise, zero)) continue; - - T rise_to_point = test_point.y - a.y; - - // colinearity test - T run_to_point = test_point.x - a.x; - is_colinear = float_equal(run * rise_to_point, run_to_point * rise); - if (is_colinear) { break; } - - y1_flag = a.y > test_point.y; - if (y1_flag != y0_flag) { - // Transform the following inequality to avoid division - // test_point.x < (run / rise) * rise_to_point + a.x - auto lhs = (test_point.x - a.x) * rise; - auto rhs = run * rise_to_point; - if (lhs < rhs != y1_flag) { point_is_within = not point_is_within; } - } - b = a; - y0_flag = y1_flag; - } - if (is_colinear) { - point_is_within = false; - break; - } - } - - return point_is_within; -} -} // namespace detail -} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh index 8753367f6..f4659477a 100644 --- a/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/pairwise_point_in_polygon.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * 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. @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include diff --git a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh index 5232cae1f..58737fe61 100644 --- a/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_in_polygon.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * 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. @@ -17,7 +17,7 @@ #pragma once #include -#include +#include #include #include From 749033340a81e816f30f95ce3a0b401d17ee8eb1 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:07:04 -0800 Subject: [PATCH 08/36] update multilinestring_range --- .../multilinestring_ref.cuh | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh index d1041818f..b8768d18f 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh @@ -1,6 +1,22 @@ +/* + * 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 "cuspatial/cuda_utils.hpp" +#include #include #include @@ -22,6 +38,9 @@ struct to_linestring_functor { CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) { + printf("In to_linestring_functor: %d %d\n", + static_cast(part_begin[i]), + static_cast(part_begin[i + 1])); return linestring_ref{point_begin + part_begin[i], point_begin + part_begin[i + 1]}; } }; From f66528713dcb4eab82984648050c55c1e50d2253 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:07:22 -0800 Subject: [PATCH 09/36] add point to polygon kernel --- .../detail/point_polygon_distance.cuh | 118 +++++++++++++++--- .../experimental/point_polygon_distance.cuh | 2 +- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index b51c252aa..7ab760b0b 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -17,50 +17,108 @@ #pragma once #include +#include #include #include +#include #include -#include -#include +#include +#include #include +#include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include -#include -#include +#include #include namespace cuspatial { namespace detail { +template +struct point_in_multipolygon_test_functor { + MultiPointRange multipoints; + MultiPolygonRange multipolygons; + + point_in_multipolygon_test_functor(MultiPointRange multipoints, MultiPolygonRange multipolygons) + : multipoints(multipoints), multipolygons(multipolygons) + { + } + + template + uint8_t __device__ operator()(IndexType pidx) + { + printf("%d\n", static_cast(pidx)); + + auto point = thrust::raw_reference_cast(multipoints.point(pidx)); + + printf("%f, %f\n", point.x, point.y); + + auto geometry_idx = multipoints.geometry_idx_from_point_idx(pidx); + + printf("%d\n", static_cast(geometry_idx)); + + bool intersects = false; + for (auto polygon : multipolygons[geometry_idx]) { + printf("here\n"); + intersects = intersects || is_point_in_polygon(point, polygon); + } + + return static_cast(intersects); + } +}; + /** * @brief Kernel to compute the distance between pairs of point and polygon. */ -template +template void __global__ pairwise_point_polygon_distance_kernel(MultiPointRange multipoints, MultiPolygonRange multipolygons, + IntersectionRange intersects, OutputIterator distances) { using T = typename MultiPointRange::element_t; + T dist_squared = std::numeric_limits::max(); for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < multipolygons.num_points(); idx += gridDim.x * blockDim.x) { - auto geometry_idx = multipolygons.geometry_idx_from_point_idx(idx); + auto geometry_idx = multipolygons.geometry_idx_from_segment_idx(idx); + if (geometry_idx == MultiPolygonRange::INVALID_INDEX) continue; + + if (intersects[geometry_idx]) { + // TODO: only the leading thread of the pair need to store the result, atomics is not needed. + atomicMin(&distances[geometry_idx], T{0.0}); + continue; + } + + printf("In distance kernel: %d %d %d", + static_cast(idx), + static_cast(geometry_idx), + static_cast(intersects.size())); + + auto [a, b] = multipolygons.get_segment(idx); + for (vec_2d point : multipoints[geometry_idx]) { + dist_squared = min(dist_squared, point_to_segment_distance_squared(point, a, b)); + } + + atomicMin(&distances[geometry_idx], sqrt(dist_squared)); } } } // namespace detail + template OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, - MultiPolygonRange multipoiygons, + MultiPolygonRange multipolygons, OutputIt distances_first, rmm::cuda_stream_view stream) { @@ -76,12 +134,38 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, CUSPATIAL_EXPECTS(multipoints.size() == multipolygons.size(), "Must have the same number of input rows."); - auto [threads_per_block, n_blocks] = grid_id(multipolygons.num_points()); + auto multipoint_intersects = [&]() { + rmm::device_uvector point_intersects(multipoints.num_points(), stream); + + thrust::tabulate(rmm::exec_policy(stream), + point_intersects.begin(), + point_intersects.end(), + detail::point_in_multipolygon_test_functor{multipoints, multipolygons}); + + rmm::device_uvector multipoint_intersects(multipoints.num_multipoints(), stream); + auto offset_as_key_it = detail::make_counting_transform_iterator( + 0, offsets_to_keys_functor{multipoints.offsets_begin(), multipoints.offsets_end()}); - pairwise_point_polygon_distance_kernel<<>>( - multipoints, multipolygons, distances_first); + thrust::reduce_by_key(rmm::exec_policy(stream), + offset_as_key_it, + offset_as_key_it + multipoints.num_points(), + point_intersects.begin(), + thrust::make_discard_iterator(), + multipoint_intersects.begin(), + thrust::logical_or()); + + return multipoint_intersects; + }(); + + auto [threads_per_block, n_blocks] = grid_1d(multipolygons.num_points()); + + detail:: + pairwise_point_polygon_distance_kernel<<>>( + multipoints, multipolygons, multipoint_intersects.begin(), distances_first); CUSPATIAL_CHECK_CUDA(stream.value()); + + return distances_first + multipoints.size(); } } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/point_polygon_distance.cuh index c5394e902..95c3a7d2c 100644 --- a/cpp/include/cuspatial/experimental/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/point_polygon_distance.cuh @@ -33,7 +33,7 @@ namespace cuspatial { */ template OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, - MultiPolygonRangeB multipoiygons, + MultiPolygonRange multipoiygons, OutputIt distances_first, rmm::cuda_stream_view stream = rmm::cuda_stream_default); } // namespace cuspatial From 291f6e679eea9e87fb87646a09593d07bf863326 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:08:10 -0800 Subject: [PATCH 10/36] add segment deduction guide --- cpp/include/cuspatial/experimental/geometry/segment.cuh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpp/include/cuspatial/experimental/geometry/segment.cuh b/cpp/include/cuspatial/experimental/geometry/segment.cuh index 30d9240d4..8667e174c 100644 --- a/cpp/include/cuspatial/experimental/geometry/segment.cuh +++ b/cpp/include/cuspatial/experimental/geometry/segment.cuh @@ -19,6 +19,8 @@ #include #include +#include + #include namespace cuspatial { @@ -61,4 +63,7 @@ class alignas(sizeof(Vertex)) segment { template segment(vec_2d a, vec_2d b) -> segment>; +template +segment(thrust::device_reference> a, thrust::device_reference> b) + -> segment>; } // namespace cuspatial From efa68830493511998064a00fba9f3b293528865b Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:08:30 -0800 Subject: [PATCH 11/36] add owning object type to vector factories --- .../cuspatial_test/vector_factories.cuh | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cpp/include/cuspatial_test/vector_factories.cuh b/cpp/include/cuspatial_test/vector_factories.cuh index 00e4c5e07..7747dfe33 100644 --- a/cpp/include/cuspatial_test/vector_factories.cuh +++ b/cpp/include/cuspatial_test/vector_factories.cuh @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,63 @@ auto make_device_uvector(std::initializer_list inl, return res; } +/** + * @brief Owning object of a multipolygon array following geoarrow layout. + * + * @tparam GeometryArray Array type of geometry offset array + * @tparam PartArray Array type of part offset array + * @tparam RingArray Array type of ring offset array + * @tparam CoordinateArray Array type of coordinate array + */ +template +class multipolygon_array { + public: + multipolygon_array(GeometryArray geometry_offsets_array, + PartArray part_offsets_array, + RingArray ring_offsets_array, + CoordinateArray coordinate_offsets_array) + : _geometry_offsets_array(geometry_offsets_array), + _part_offsets_array(part_offsets_array), + _ring_offsets_array(ring_offsets_array), + _coordinate_offsets_array(coordinate_offsets_array) + { + } + + /// Return the number of multilinestrings + auto size() { return _geometry_offsets_array.size() - 1; } + + /// Return range object of the multilinestring array + auto range() + { + return multipolygon_range(_geometry_offsets_array.begin(), + _geometry_offsets_array.end(), + _part_offsets_array.begin(), + _part_offsets_array.end(), + _ring_offsets_array.begin(), + _ring_offsets_array.end(), + _coordinate_offsets_array.begin(), + _coordinate_offsets_array.end()); + } + + protected: + GeometryArray _geometry_offsets_array; + PartArray _part_offsets_array; + RingArray _ring_offsets_array; + CoordinateArray _coordinate_offsets_array; +}; + +template +auto make_multipolygon_array(std::initializer_list geometry_inl, + std::initializer_list part_inl, + std::initializer_list ring_inl, + std::initializer_list> coord_inl) +{ + return multipolygon_array{make_device_vector(geometry_inl), + make_device_vector(part_inl), + make_device_vector(ring_inl), + make_device_vector(coord_inl)}; +} + /** * @brief Owning object of a multilinestring array following geoarrow layout. * From 23146ef32dbac357085464c11b47c4a73af56b8b Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:08:58 -0800 Subject: [PATCH 12/36] add tests --- cpp/tests/CMakeLists.txt | 3 + .../spatial/point_polygon_distance_test.cu | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 cpp/tests/experimental/spatial/point_polygon_distance_test.cu diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 6182e1238..821c7881b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -133,6 +133,9 @@ ConfigureTest(POINT_DISTANCE_TEST_EXP ConfigureTest(POINT_LINESTRING_DISTANCE_TEST_EXP experimental/spatial/point_linestring_distance_test.cu) +ConfigureTest(POINT_POLYGON_DISTANCE_TEST_EXP + experimental/spatial/point_polygon_distance_test.cu) + ConfigureTest(HAUSDORFF_TEST_EXP experimental/spatial/hausdorff_test.cu) diff --git a/cpp/tests/experimental/spatial/point_polygon_distance_test.cu b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu new file mode 100644 index 000000000..987cefa04 --- /dev/null +++ b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu @@ -0,0 +1,90 @@ +/* + * 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 + +using namespace cuspatial; +using namespace cuspatial::test; + +template +struct PairwisePointPolygonDistanceTest : 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_single(std::initializer_list>> multipoints, + std::initializer_list multipolygon_geometry_offsets, + std::initializer_list multipolygon_part_offsets, + std::initializer_list multipolygon_ring_offsets, + std::initializer_list> multipolygon_coordinates, + std::initializer_list expected) + { + auto d_multipoints = make_multipoints_array(multipoints); + auto d_multipolygons = make_multipolygon_array(multipolygon_geometry_offsets, + multipolygon_part_offsets, + multipolygon_ring_offsets, + multipolygon_coordinates); + + auto got = rmm::device_uvector(d_multipoints.size(), stream()); + + auto ret = pairwise_point_polygon_distance( + d_multipoints.range(), d_multipolygons.range(), 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(PairwisePointPolygonDistanceTest, TestTypes); + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonOneRing) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{0, 0}}}, + {0, 1}, + {0, 1}, + {0, 5}, + {P{-1, -1}, P{1, -1}, P{1, 1}, P{-1, 1}, P{-1, -1}}, + {0.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonOneRing2) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{0, 2}}}, + {0, 1}, + {0, 1}, + {0, 5}, + {P{-1, -1}, P{1, -1}, P{1, 1}, P{-1, 1}, P{-1, -1}}, + {1.0}); +} From ead160a14c111f683c9c550944b95b7db7bfc12a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 7 Mar 2023 11:09:21 -0800 Subject: [PATCH 13/36] add helper files --- .../detail/utility/offset_to_keys.cuh | 51 ++++++++ .../detail/algorithm/is_point_in_polygon.cuh | 112 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 cpp/include/cuspatial/detail/utility/offset_to_keys.cuh create mode 100644 cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh diff --git a/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh b/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh new file mode 100644 index 000000000..70665b4db --- /dev/null +++ b/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh @@ -0,0 +1,51 @@ +/* + * 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 + +/** @brief Given list offset and row `i`, return a unique key that represent the list of `i`. + * + * The key is computed by performing a `upper_bound` search with `i` in the offset array. + * Then subtracts the position with the start of offset array. + * + * Example: + * offset: 0 0 0 1 3 4 4 4 + * i: 0 1 2 3 + * key: 3 4 4 5 + * + * Note that the values of `key`, {offset[3], offset[4], offset[5]} denotes the ending + * position of the first 3 non-empty list. + */ +template +struct offsets_to_keys_functor { + Iterator _offsets_begin; + Iterator _offsets_end; + + offsets_to_keys_functor(Iterator offset_begin, Iterator offset_end) + : _offsets_begin(offset_begin), _offsets_end(offset_end) + { + } + + template + IndexType __device__ operator()(IndexType i) + { + return thrust::distance(_offsets_begin, + thrust::upper_bound(thrust::seq, _offsets_begin, _offsets_end, i)); + } +}; diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh new file mode 100644 index 000000000..e489c22dd --- /dev/null +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace cuspatial { +namespace detail { + +/** + * @brief Kernel to test if a point is inside a polygon. + * + * Implemented based on Eric Haines's crossings-multiply algorithm: + * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ + * The improvement in addenda is also addopted to remove divisions in this kernel. + * + * TODO: the ultimate goal of refactoring this as independent function is to remove + * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't + * possible today without further work to refactor quadtree_point_in_polygon into header only + * API. + */ +template +__device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonRef const& polygon) +{ + bool point_is_within = false; + bool is_colinear = false; + printf("Polygon num rings: %d\n", static_cast(polygon.num_rings())); + for (auto ring : polygon) { + printf("here in ring\n"); + bool y0_flag = ring.segment(ring.num_segments() - 1).v2.y > test_point.y; + bool y1_flag; + for (auto [a, b] : ring) { + printf("here in segment (%f, %f) -> (%f, %f)\n", a.x, a.y, b.x, b.y); + // for each line segment, including the segment between the last and first vertex + T run = b.x - a.x; + T rise = b.y - a.y; + + // Points on the line segment are the same, so intersection is impossible. + // This is possible because we allow closed or unclosed polygons. + T constexpr zero = 0.0; + if (float_equal(run, zero) && float_equal(rise, zero)) continue; + + T rise_to_point = test_point.y - a.y; + + // colinearity test + T run_to_point = test_point.x - a.x; + is_colinear = float_equal(run * rise_to_point, run_to_point * rise); + if (is_colinear) { break; } + + y1_flag = a.y > test_point.y; + if (y1_flag != y0_flag) { + // Transform the following inequality to avoid division + // test_point.x < (run / rise) * rise_to_point + a.x + auto lhs = (test_point.x - a.x) * rise; + auto rhs = run * rise_to_point; + if (lhs < rhs != y1_flag) { point_is_within = not point_is_within; } + } + b = a; + y0_flag = y1_flag; + } + if (is_colinear) { + point_is_within = false; + break; + } + } + + printf("Exiting pip.\n"); + + return point_is_within; +} + +template ::difference_type, + class Cart2dItDiffType = typename std::iterator_traits::difference_type> +__device__ inline bool is_point_in_polygon(Cart2d const& test_point, + OffsetType poly_begin, + OffsetType poly_end, + OffsetIterator ring_offsets_first, + OffsetItDiffType const& num_rings, + Cart2dIt poly_points_first, + Cart2dItDiffType const& num_poly_points) +{ + auto polygon = polygon_ref{ring_offsets_first + poly_begin, + ring_offsets_first + poly_end, + poly_points_first, + poly_points_first + num_poly_points}; + return is_point_in_polygon(test_point, polygon); +} + +} // namespace detail +} // namespace cuspatial From 09bd35f2bd8ead3a6fa525bb28ccfb4cf5981ecd Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 13:15:27 -0800 Subject: [PATCH 14/36] add more tests --- .../spatial/point_polygon_distance_test.cu | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/cpp/tests/experimental/spatial/point_polygon_distance_test.cu b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu index 987cefa04..519ed6819 100644 --- a/cpp/tests/experimental/spatial/point_polygon_distance_test.cu +++ b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -61,6 +62,20 @@ using TestTypes = ::testing::Types; TYPED_TEST_CASE(PairwisePointPolygonDistanceTest, TestTypes); +TYPED_TEST(PairwisePointPolygonDistanceTest, ZeroPairs) +{ + 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{}); +} + TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonOneRing) { using T = TypeParam; @@ -88,3 +103,325 @@ TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonOneRing2) {P{-1, -1}, P{1, -1}, P{1, 1}, P{-1, 1}, P{-1, -1}}, {1.0}); } + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonTwoRings) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{0, 0}}}, + {0, 1}, + {0, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{2, -2}, + P{2, 2}, + P{-2, 2}, + P{-2, -2}, + P{-1, -1}, + P{1, -1}, + P{1, 1}, + P{-1, 1}, + P{-1, -1}, + }, + {1.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonTwoRings2) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{1.5, 0}}}, + {0, 1}, + {0, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{2, -2}, + P{2, 2}, + P{-2, 2}, + P{-2, -2}, + P{-1, -1}, + P{1, -1}, + P{1, 1}, + P{-1, 1}, + P{-1, -1}, + }, + {0.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairOnePolygonTwoRings3) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{3, 0}}}, + {0, 1}, + {0, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{2, -2}, + P{2, 2}, + P{-2, 2}, + P{-2, -2}, + P{-1, -1}, + P{1, -1}, + P{1, 1}, + P{-1, 1}, + P{-1, -1}, + }, + {1.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairTwoPolygonOneRing) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{1, 1}}}, + {0, 2}, + {0, 1, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{0, -2}, + P{0, 0}, + P{-2, 0}, + P{-2, -2}, + P{0, 0}, + P{2, 0}, + P{2, 2}, + P{0, 2}, + P{0, 0}, + }, + {0.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairTwoPolygonOneRing2) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{-1, -1}}}, + {0, 2}, + {0, 1, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{0, -2}, + P{0, 0}, + P{-2, 0}, + P{-2, -2}, + P{0, 0}, + P{2, 0}, + P{2, 2}, + P{0, 2}, + P{0, 0}, + }, + {0.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairTwoPolygonOneRing3) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{-1, 0.5}}}, + {0, 2}, + {0, 1, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{0, -2}, + P{0, 0}, + P{-2, 0}, + P{-2, -2}, + P{0, 0}, + P{2, 0}, + P{2, 2}, + P{0, 2}, + P{0, 0}, + }, + {0.5}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairTwoPolygonOneRing4) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{-0.3, 1}}}, + {0, 2}, + {0, 1, 2}, + {0, 5, 10}, + { + P{-2, -2}, + P{0, -2}, + P{0, 0}, + P{-2, 0}, + P{-2, -2}, + P{0, 0}, + P{2, 0}, + P{2, 2}, + P{0, 2}, + P{0, 0}, + }, + {0.3}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, TwoPairOnePolygonOneRing) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{-0.6, -0.6}}, {P{0, 0}}}, + {0, 1, 2}, + {0, 1, 2}, + {0, 4, 8}, + { + P{-1, -1}, + P{0, 0}, + P{0, 1}, + P{-1, -1}, + P{1, 1}, + P{1, 0}, + P{2, 2}, + P{1, 1}, + }, + {0.0, 1.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, TwoPairTwoPolygonTwoRing) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{2.5, 3}}, {P{-2.5, -1.5}}}, + {0, 1, 2}, + {0, 2, 4}, + {0, 5, 10, 14, 18}, + { + P{0, 0}, + P{3, 0}, + P{3, 3}, + P{0, 3}, + P{0, 0}, + P{1, 1}, + P{2, 1}, + P{2, 2}, + P{1, 2}, + P{1, 1}, + + P{0, 0}, + P{-3, 0}, + P{-3, -3}, + P{0, 0}, + P{-1, -1}, + P{-2, -1}, + P{-2, -2}, + P{-1, -1}, + }, + {0.0, 0.5}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, ThreePolygons) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{1, 1}}, {P{2, 2}}, {P{1.5, 0}}}, + {0, 1, 2, 3}, + {0, 2, 3, 6}, + {0, 4, 8, 12, 17, 22, 27}, + {// POLYGON ((0 1, -1 -1, 1 -1, 0 1), (0 0.5, 0.5 -0.5, -0.5 -0.5, 0 0.5)) + P{0, 1}, + P{-1, -1}, + P{1, -1}, + P{0, 1}, + P{0, 0.5}, + P{0.5, -0.5}, + P{-0.5, -0.5}, + P{0, 0.5}, + // POLYGON ((1 1, 1 2, 2 1, 1 1)) + P{1, 1}, + P{1, 2}, + P{2, 1}, + P{1, 1}, + // POLYGON ( + // (-3 -3, 3 -3, 3 3, -3 3, -3 -3), + // (-2 -2, -1 -2, -1 2, -2 2, -2 -2), + // (2 2, 2 -2, 1 -2, 1 2, 2 2) + // ) + P{-3, -3}, + P{3, -3}, + P{3, 3}, + P{-3, 3}, + P{-3, -3}, + P{-2, -2}, + P{-1, -2}, + P{-1, 2}, + P{-2, 2}, + P{-2, -2}, + P{2, 2}, + P{2, -2}, + P{1, -2}, + P{1, 2}, + P{2, 2}}, + {0.894427190999916, 0.7071067811865476, 0.5}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairMultiPointOnePolygon) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{0, 3}, P{2, 0}}}, + {0, 1}, + {0, 1}, + {0, 5}, + {P{0, 1}, P{-1, -1}, P{1, -1}, P{0, 1}}, + {1.3416407864998738}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, OnePairMultiPointOnePolygon2) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST(this->run_single, + {{P{0, 3}, P{0, 0}}}, + {0, 1}, + {0, 1}, + {0, 5}, + {P{0, 1}, P{-1, -1}, P{1, -1}, P{0, 1}}, + {0.0}); +} + +TYPED_TEST(PairwisePointPolygonDistanceTest, TwoPairMultiPointOnePolygon2) +{ + using T = TypeParam; + using P = vec_2d; + + CUSPATIAL_RUN_TEST( + this->run_single, + {{P{0, 2}, P{0, 0}}, {P{1, 1}, P{-1, -1}}}, + {0, 1, 2}, + {0, 1, 2}, + {0, 5, 9}, + {P{-1, -1}, P{1, -1}, P{1, 1}, P{-1, 1}, P{-1, -1}, P{-1, 1}, P{1, 1}, P{0, -1}, P{-1, 1}}, + {0.0, 0.0}); +} From 92760d13d330f63ca5f4d999220edfa3233e4427 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 13:23:03 -0800 Subject: [PATCH 15/36] bug fixes --- .../detail/algorithm/is_point_in_polygon.cuh | 23 ++++++++++-- .../detail/geometry/linestring_ref.cuh | 14 +++++++- .../detail/point_polygon_distance.cuh | 35 +++++++++++++++---- .../detail/ranges/multipolygon_range.cuh | 8 +++-- .../experimental/geometry/linestring_ref.cuh | 5 +++ .../ranges/multipolygon_range.cuh | 3 -- 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh index e489c22dd..61b80b365 100644 --- a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -45,9 +45,18 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR printf("Polygon num rings: %d\n", static_cast(polygon.num_rings())); for (auto ring : polygon) { printf("here in ring\n"); - bool y0_flag = ring.segment(ring.num_segments() - 1).v2.y > test_point.y; + auto last_segment = ring.segment(ring.num_segments() - 1); + printf("Last segment is: (%f %f)->(%f %f)\n", + last_segment.v1.x, + last_segment.v1.y, + last_segment.v2.x, + last_segment.v2.y); + + auto b = last_segment.v2; + bool y0_flag = b.y > test_point.y; bool y1_flag; - for (auto [a, b] : ring) { + for (auto it = ring.point_begin(); it != ring.point_end(); ++it) { + vec_2d a = *it; printf("here in segment (%f, %f) -> (%f, %f)\n", a.x, a.y, b.x, b.y); // for each line segment, including the segment between the last and first vertex T run = b.x - a.x; @@ -65,7 +74,12 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR is_colinear = float_equal(run * rise_to_point, run_to_point * rise); if (is_colinear) { break; } + // y0_flag = a.y > test_point.y; y1_flag = a.y > test_point.y; + printf("\t y0_flag: %d, y1_flag: %d, point_is_within: %d\n", + static_cast(y0_flag), + static_cast(y1_flag), + static_cast(point_is_within)); if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x @@ -82,11 +96,14 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR } } - printf("Exiting pip.\n"); + printf("Exiting pip. Result: %d\n", static_cast(point_is_within)); return point_is_within; } +/** + * @brief Compatibility layer with non-OOP style input + */ template CUSPATIAL_HOST_DEVICE auto linestring_ref::num_segments() const { // The number of segment equals the number of points minus 1. And the number of points - // is thrust::distance(_point_begin, _point_end) - 1. + // is thrust::distance(_point_begin, _point_end). return thrust::distance(_point_begin, _point_end) - 1; } @@ -70,6 +70,18 @@ CUSPATIAL_HOST_DEVICE auto linestring_ref::segment_end() const return segment_begin() + num_segments(); } +template +CUSPATIAL_HOST_DEVICE auto linestring_ref::point_begin() const +{ + return _point_begin; +} + +template +CUSPATIAL_HOST_DEVICE auto linestring_ref::point_end() const +{ + return _point_end; +} + template template CUSPATIAL_HOST_DEVICE auto linestring_ref::segment(IndexType i) const diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index 7ab760b0b..efd825cb3 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -16,11 +16,14 @@ #pragma once +#include + #include #include #include #include #include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include +#include #include namespace cuspatial { @@ -70,6 +74,7 @@ struct point_in_multipolygon_test_functor { intersects = intersects || is_point_in_polygon(point, polygon); } + printf("Intersect: %d\n", static_cast(intersects)); return static_cast(intersects); } }; @@ -88,25 +93,32 @@ void __global__ pairwise_point_polygon_distance_kernel(MultiPointRange multipoin { using T = typename MultiPointRange::element_t; - T dist_squared = std::numeric_limits::max(); for (auto idx = threadIdx.x + blockIdx.x * blockDim.x; idx < multipolygons.num_points(); idx += gridDim.x * blockDim.x) { auto geometry_idx = multipolygons.geometry_idx_from_segment_idx(idx); if (geometry_idx == MultiPolygonRange::INVALID_INDEX) continue; + printf("Intesects? %d Geometry_idx: %d\n", + static_cast(intersects[geometry_idx]), + static_cast(geometry_idx)); + if (intersects[geometry_idx]) { // TODO: only the leading thread of the pair need to store the result, atomics is not needed. + printf("In intersects, idx: %d\n", static_cast(idx)); atomicMin(&distances[geometry_idx], T{0.0}); continue; } - printf("In distance kernel: %d %d %d", + printf("In distance kernel: point_idx: %d segment_idx: %d\n", static_cast(idx), - static_cast(geometry_idx), - static_cast(intersects.size())); + static_cast(geometry_idx)); - auto [a, b] = multipolygons.get_segment(idx); + T dist_squared = std::numeric_limits::max(); + auto [a, b] = multipolygons.get_segment(idx); for (vec_2d point : multipoints[geometry_idx]) { + printf("point: %f %f\n", point.x, point.y); + printf("segment: (%f %f) -> (%f %f)\n", a.x, a.y, b.x, b.y); + printf("dist: %f\n", point_to_segment_distance_squared(point, a, b)); dist_squared = min(dist_squared, point_to_segment_distance_squared(point, a, b)); } @@ -134,6 +146,8 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, CUSPATIAL_EXPECTS(multipoints.size() == multipolygons.size(), "Must have the same number of input rows."); + if (multipoints.size() == 0) return distances_first; + auto multipoint_intersects = [&]() { rmm::device_uvector point_intersects(multipoints.num_points(), stream); @@ -142,7 +156,10 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, point_intersects.end(), detail::point_in_multipolygon_test_functor{multipoints, multipolygons}); + // TODO: optimize when input is not a multipolygon rmm::device_uvector multipoint_intersects(multipoints.num_multipoints(), stream); + detail::zero_data_async(multipoint_intersects.begin(), multipoint_intersects.end(), stream); + auto offset_as_key_it = detail::make_counting_transform_iterator( 0, offsets_to_keys_functor{multipoints.offsets_begin(), multipoints.offsets_end()}); @@ -157,10 +174,16 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, return multipoint_intersects; }(); + thrust::fill(rmm::exec_policy(stream), + distances_first, + distances_first + multipoints.size(), + std::numeric_limits::max()); auto [threads_per_block, n_blocks] = grid_1d(multipolygons.num_points()); + std::cout << "Size of multipoint intersects: " << multipoint_intersects.size() << std::endl; + detail:: - pairwise_point_polygon_distance_kernel<<>>( + pairwise_point_polygon_distance_kernel<<>>( multipoints, multipolygons, multipoint_intersects.begin(), distances_first); CUSPATIAL_CHECK_CUDA(stream.value()); diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index b44420532..33b7a512e 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -203,8 +203,7 @@ multipolygon_range:: part_idx_from_ring_idx(IndexType ring_idx) { return thrust::distance( - _part_begin, - thrust::prev(thrust::upper_bound(thrust::seq, _part_begin, _part_begin, ring_idx))); + _part_begin, thrust::prev(thrust::upper_bound(thrust::seq, _part_begin, _part_end, ring_idx))); } template :: geometry_idx_from_segment_idx(IndexType segment_idx) { + printf("segment_idx: %d\n", static_cast(segment_idx)); auto ring_idx = ring_idx_from_point_idx(segment_idx); + printf("ring_idx: %d\n", static_cast(ring_idx)); if (!is_valid_segment_id(segment_idx, ring_idx)) return multipolygon_range:: INVALID_INDEX; + + auto part_idx = part_idx_from_ring_idx(ring_idx); + printf("part_idx: %d\n", static_cast(part_idx)); return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx)); } diff --git a/cpp/include/cuspatial/experimental/geometry/linestring_ref.cuh b/cpp/include/cuspatial/experimental/geometry/linestring_ref.cuh index 0ebad40f4..707b9e0cc 100644 --- a/cpp/include/cuspatial/experimental/geometry/linestring_ref.cuh +++ b/cpp/include/cuspatial/experimental/geometry/linestring_ref.cuh @@ -38,6 +38,11 @@ class linestring_ref { /// Return iterator to one past the last segment CUSPATIAL_HOST_DEVICE auto segment_end() const; + /// Return iterator to the first point of the linestring + CUSPATIAL_HOST_DEVICE auto point_begin() const; + /// Return iterator to one past the last point + CUSPATIAL_HOST_DEVICE auto point_end() const; + /// Return iterator to the first segment of the linestring CUSPATIAL_HOST_DEVICE auto begin() const { return segment_begin(); } /// Return iterator to one past the last segment diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index b9efd64e5..8a7300213 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -112,9 +112,6 @@ class multipolygon_range { template CUSPATIAL_HOST_DEVICE auto operator[](IndexType multipolygon_idx); - // template - // CUSPATIAL_HOST_DEVICE auto get_point(IndexType point_idx); - template CUSPATIAL_HOST_DEVICE auto get_segment(IndexType segment_idx); From 8acb5dc48127664d92b5f89370616aa1dfa25b6e Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 13:47:29 -0800 Subject: [PATCH 16/36] cleanups --- .../detail/algorithm/is_point_in_polygon.cuh | 14 --------- .../multilinestring_ref.cuh | 3 -- .../detail/point_polygon_distance.cuh | 30 ++----------------- .../detail/ranges/multipolygon_range.cuh | 27 ++++++++--------- .../ranges/multipolygon_range.cuh | 7 +++++ 5 files changed, 22 insertions(+), 59 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh index 61b80b365..135011122 100644 --- a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -42,22 +42,14 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR { bool point_is_within = false; bool is_colinear = false; - printf("Polygon num rings: %d\n", static_cast(polygon.num_rings())); for (auto ring : polygon) { - printf("here in ring\n"); auto last_segment = ring.segment(ring.num_segments() - 1); - printf("Last segment is: (%f %f)->(%f %f)\n", - last_segment.v1.x, - last_segment.v1.y, - last_segment.v2.x, - last_segment.v2.y); auto b = last_segment.v2; bool y0_flag = b.y > test_point.y; bool y1_flag; for (auto it = ring.point_begin(); it != ring.point_end(); ++it) { vec_2d a = *it; - printf("here in segment (%f, %f) -> (%f, %f)\n", a.x, a.y, b.x, b.y); // for each line segment, including the segment between the last and first vertex T run = b.x - a.x; T rise = b.y - a.y; @@ -76,10 +68,6 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR // y0_flag = a.y > test_point.y; y1_flag = a.y > test_point.y; - printf("\t y0_flag: %d, y1_flag: %d, point_is_within: %d\n", - static_cast(y0_flag), - static_cast(y1_flag), - static_cast(point_is_within)); if (y1_flag != y0_flag) { // Transform the following inequality to avoid division // test_point.x < (run / rise) * rise_to_point + a.x @@ -96,8 +84,6 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR } } - printf("Exiting pip. Result: %d\n", static_cast(point_is_within)); - return point_is_within; } diff --git a/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh index b8768d18f..b4c76ec32 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry_collection/multilinestring_ref.cuh @@ -38,9 +38,6 @@ struct to_linestring_functor { CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) { - printf("In to_linestring_functor: %d %d\n", - static_cast(part_begin[i]), - static_cast(part_begin[i + 1])); return linestring_ref{point_begin + part_begin[i], point_begin + part_begin[i + 1]}; } }; diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index efd825cb3..21bc5a332 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -58,23 +58,13 @@ struct point_in_multipolygon_test_functor { template uint8_t __device__ operator()(IndexType pidx) { - printf("%d\n", static_cast(pidx)); - - auto point = thrust::raw_reference_cast(multipoints.point(pidx)); - - printf("%f, %f\n", point.x, point.y); - + auto point = thrust::raw_reference_cast(multipoints.point(pidx)); auto geometry_idx = multipoints.geometry_idx_from_point_idx(pidx); - printf("%d\n", static_cast(geometry_idx)); - bool intersects = false; for (auto polygon : multipolygons[geometry_idx]) { - printf("here\n"); intersects = intersects || is_point_in_polygon(point, polygon); } - - printf("Intersect: %d\n", static_cast(intersects)); return static_cast(intersects); } }; @@ -98,27 +88,15 @@ void __global__ pairwise_point_polygon_distance_kernel(MultiPointRange multipoin auto geometry_idx = multipolygons.geometry_idx_from_segment_idx(idx); if (geometry_idx == MultiPolygonRange::INVALID_INDEX) continue; - printf("Intesects? %d Geometry_idx: %d\n", - static_cast(intersects[geometry_idx]), - static_cast(geometry_idx)); - if (intersects[geometry_idx]) { - // TODO: only the leading thread of the pair need to store the result, atomics is not needed. - printf("In intersects, idx: %d\n", static_cast(idx)); - atomicMin(&distances[geometry_idx], T{0.0}); + if (multipolygons.is_first_point_of_multipolygon(idx, geometry_idx)) + distances[geometry_idx] = T{0.0}; continue; } - printf("In distance kernel: point_idx: %d segment_idx: %d\n", - static_cast(idx), - static_cast(geometry_idx)); - T dist_squared = std::numeric_limits::max(); auto [a, b] = multipolygons.get_segment(idx); for (vec_2d point : multipoints[geometry_idx]) { - printf("point: %f %f\n", point.x, point.y); - printf("segment: (%f %f) -> (%f %f)\n", a.x, a.y, b.x, b.y); - printf("dist: %f\n", point_to_segment_distance_squared(point, a, b)); dist_squared = min(dist_squared, point_to_segment_distance_squared(point, a, b)); } @@ -180,8 +158,6 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, std::numeric_limits::max()); auto [threads_per_block, n_blocks] = grid_1d(multipolygons.num_points()); - std::cout << "Size of multipoint intersects: " << multipoint_intersects.size() << std::endl; - detail:: pairwise_point_polygon_distance_kernel<<>>( multipoints, multipolygons, multipoint_intersects.begin(), distances_first); diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index 33b7a512e..0228d1de8 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -66,18 +66,6 @@ struct to_multipolygon_functor { CUSPATIAL_HOST_DEVICE auto operator()(difference_type i) { - auto new_part_begin = _part_begin + _geometry_begin[i]; - auto new_part_end = thrust::next(_part_begin, _geometry_begin[i + 1] + 1); - - printf( - "In to_multipolygon_functor: %d %d %d\n\t new_part_begin_val: %d, " - "new_part_end_dist_from_begin: %d\n", - static_cast(i), - static_cast(_geometry_begin[i]), - static_cast(_geometry_begin[i + 1]), - static_cast(*new_part_begin), - static_cast(thrust::distance(new_part_end, new_part_begin))); - return multipolygon_ref{_part_begin + _geometry_begin[i], thrust::next(_part_begin, _geometry_begin[i + 1] + 1), _ring_begin, @@ -229,15 +217,12 @@ CUSPATIAL_HOST_DEVICE auto multipolygon_range:: geometry_idx_from_segment_idx(IndexType segment_idx) { - printf("segment_idx: %d\n", static_cast(segment_idx)); auto ring_idx = ring_idx_from_point_idx(segment_idx); - printf("ring_idx: %d\n", static_cast(ring_idx)); if (!is_valid_segment_id(segment_idx, ring_idx)) return multipolygon_range:: INVALID_INDEX; auto part_idx = part_idx_from_ring_idx(ring_idx); - printf("part_idx: %d\n", static_cast(part_idx)); return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx)); } @@ -280,4 +265,16 @@ multipolygon_range::g return segment{_point_begin[segment_idx], _point_begin[segment_idx + 1]}; } +template +template +CUSPATIAL_HOST_DEVICE bool +multipolygon_range:: + is_first_point_of_multipolygon(IndexType1 point_idx, IndexType2 geometry_idx) +{ + return point_idx == _ring_begin[_part_begin[_geometry_begin[geometry_idx]]]; +} + } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 8a7300213..775c7dba4 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -112,9 +112,16 @@ class multipolygon_range { template CUSPATIAL_HOST_DEVICE auto operator[](IndexType multipolygon_idx); + /// Returns the `segment_idx`th segment in the multipolygon range. template CUSPATIAL_HOST_DEVICE auto get_segment(IndexType segment_idx); + /// Returns `true` if `point_idx`th point is the first point of its + /// multipolygon + template + CUSPATIAL_HOST_DEVICE bool is_first_point_of_multipolygon(IndexType1 point_idx, + IndexType2 geometry_idx); + protected: GeometryIterator _geometry_begin; GeometryIterator _geometry_end; From a2b94fe7410d0e4a32940ee52a13db59955e0e27 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 14:38:32 -0800 Subject: [PATCH 17/36] fix tests --- cpp/tests/experimental/spatial/point_polygon_distance_test.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/tests/experimental/spatial/point_polygon_distance_test.cu b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu index 519ed6819..ce5f78b23 100644 --- a/cpp/tests/experimental/spatial/point_polygon_distance_test.cu +++ b/cpp/tests/experimental/spatial/point_polygon_distance_test.cu @@ -308,7 +308,7 @@ TYPED_TEST(PairwisePointPolygonDistanceTest, TwoPairTwoPolygonTwoRing) using P = vec_2d; CUSPATIAL_RUN_TEST(this->run_single, - {{P{2.5, 3}}, {P{-2.5, -1.5}}}, + {{P{2.5, 3}}, {P{-1.75, -1.5}}}, {0, 1, 2}, {0, 2, 4}, {0, 5, 10, 14, 18}, @@ -333,7 +333,7 @@ TYPED_TEST(PairwisePointPolygonDistanceTest, TwoPairTwoPolygonTwoRing) P{-2, -2}, P{-1, -1}, }, - {0.0, 0.5}); + {0.0, 0.17677669529663687}); } TYPED_TEST(PairwisePointPolygonDistanceTest, ThreePolygons) From 46a67fe5612967a44d7a1708b0b742e0997a7e02 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 14:39:07 -0800 Subject: [PATCH 18/36] optimize single point range input --- .../experimental/detail/point_polygon_distance.cuh | 7 ++++++- .../experimental/detail/ranges/multipoint_range.cuh | 6 ++++++ .../cuspatial/experimental/ranges/multipoint_range.cuh | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index 21bc5a332..b217c5f35 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -126,6 +126,9 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, if (multipoints.size() == 0) return distances_first; + // Compute whether each multipoint intersects with the corresponding multipolygon. + // First, compute the point-multipolygon intersection. Then use reduce-by-key to + // compute the multipoint-multipolygon intersection. auto multipoint_intersects = [&]() { rmm::device_uvector point_intersects(multipoints.num_points(), stream); @@ -134,7 +137,9 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, point_intersects.end(), detail::point_in_multipolygon_test_functor{multipoints, multipolygons}); - // TODO: optimize when input is not a multipolygon + // `multipoints` contains only single points, no need to reduce. + if (multipoints.is_single_point_range()) return point_intersects; + rmm::device_uvector multipoint_intersects(multipoints.num_multipoints(), stream); detail::zero_data_async(multipoint_intersects.begin(), multipoint_intersects.end(), stream); diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh index d3d8926d5..7b7ce5243 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh @@ -140,4 +140,10 @@ CUSPATIAL_HOST_DEVICE auto multipoint_range::poin return _points_begin[idx]; } +template +CUSPATIAL_HOST_DEVICE bool multipoint_range::is_single_point_range() +{ + return num_multipoints() == num_points(); +} + } // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh index 1ee08b5cc..1e2ac7c42 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh @@ -141,6 +141,11 @@ class multipoint_range { template CUSPATIAL_HOST_DEVICE auto point(IndexType idx); + /** + * @brief Returns `true` if the range contains only single points + */ + CUSPATIAL_HOST_DEVICE bool is_single_point_range(); + protected: /// Iterator to the start of the index array of start positions to each multipoint. GeometryIterator _geometry_begin; From b725b52f70067c3171ee67081de9f9b0af6b5280 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 14:51:02 -0800 Subject: [PATCH 19/36] docs, type checks in range ctor --- .../experimental/detail/point_polygon_distance.cuh | 9 --------- .../detail/ranges/multipoint_range.cuh | 2 ++ .../detail/ranges/multipolygon_range.cuh | 2 ++ .../experimental/point_polygon_distance.cuh | 14 +++++++++----- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index b217c5f35..b39ba6dec 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -16,8 +16,6 @@ #pragma once -#include - #include #include #include @@ -114,13 +112,6 @@ OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, { using T = typename MultiPointRange::element_t; - static_assert(is_same_floating_point(), - "Inputs must have same floating point value type."); - - static_assert( - is_same, typename MultiPointRange::point_t, typename MultiPolygonRange::point_t>(), - "Inputs must be cuspatial::vec_2d"); - CUSPATIAL_EXPECTS(multipoints.size() == multipolygons.size(), "Must have the same number of input rows."); diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh index 7b7ce5243..f28ed5ef2 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipoint_range.cuh @@ -63,6 +63,8 @@ multipoint_range::multipoint_range(GeometryIterat _points_begin(points_begin), _points_end(points_end) { + static_assert(is_vec_2d>(), + "Coordinate range must be constructed with iterators to vec_2d."); } template diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index 0228d1de8..7e0e868bf 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -103,6 +103,8 @@ multipolygon_range::m _point_begin(point_begin), _point_end(point_end) { + static_assert(is_vec_2d>(), + "Coordinate range must be constructed with iterators to vec_2d."); } template OutputIt pairwise_point_polygon_distance(MultiPointRange multipoints, From ab59e7d1352ed3b3c843abbfc95f09b06e8ad57f Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 15:13:15 -0800 Subject: [PATCH 20/36] use range based for loop in is_point_in_polygon --- .../experimental/detail/algorithm/is_point_in_polygon.cuh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh index 135011122..5ddf208d7 100644 --- a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -16,6 +16,7 @@ #pragma once +#include "cuspatial/experimental/geometry_collection/multipoint_ref.cuh" #include #include @@ -48,8 +49,8 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR auto b = last_segment.v2; bool y0_flag = b.y > test_point.y; bool y1_flag; - for (auto it = ring.point_begin(); it != ring.point_end(); ++it) { - vec_2d a = *it; + auto ring_points = multipoint_ref{ring.point_begin(), ring.point_end()}; + for (vec_2d a : ring_points) { // for each line segment, including the segment between the last and first vertex T run = b.x - a.x; T rise = b.y - a.y; From ddcd5d2b1ad13276cadb40ebc351c4f5b4e449a3 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 16:37:57 -0800 Subject: [PATCH 21/36] initial column API --- cpp/CMakeLists.txt | 1 + .../ranges/multilinestring_range.cuh | 6 +- .../experimental/ranges/multipoint_range.cuh | 59 +++++++++++++++- .../ranges/multipolygon_range.cuh | 68 +++++++++++++++++++ 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8bd8958bc..7db462828 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -125,6 +125,7 @@ add_library(cuspatial src/spatial/linestring_intersection.cu src/spatial/point_distance.cu src/spatial/point_linestring_distance.cu + src/spatial/point_polygon_distance.cu src/spatial/point_linestring_nearest_points.cu src/spatial/sinusoidal_projection.cu src/trajectory/derive_trajectories.cu diff --git a/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh b/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh index 7f7b629a6..4b881a811 100644 --- a/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh @@ -245,7 +245,7 @@ auto make_multilinestring_range(GeometryColumnView const& linestrings_column) "Must be Linestring geometry type."); auto geometry_iter = thrust::make_counting_iterator(0); auto const& part_offsets = linestrings_column.offsets(); - auto const& points_xy = linestrings_column.child().child(1); + auto const& points_xy = linestrings_column.child().child(); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); @@ -275,8 +275,8 @@ auto make_multilinestring_range(GeometryColumnView const& linestrings_column) "Must be Linestring geometry type."); auto const& geometry_offsets = linestrings_column.offsets(); auto const& parts = linestrings_column.child(); - auto const& part_offsets = parts.child(0); - auto const& points_xy = parts.child(1).child(1); + auto const& part_offsets = parts.offsets(); + auto const& points_xy = parts.child().child(); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); diff --git a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh index 1e2ac7c42..fa72bfe59 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh @@ -18,6 +18,7 @@ #include #include +#include namespace cuspatial { @@ -158,7 +159,7 @@ class multipoint_range { }; /** - * @brief Create a multilinestring_range object of from size and start iterators + * @brief Create a multipoint_range object of from size and start iterators * @ingroup ranges * * @tparam GeometryIteratorDiffType Index type of the size of the geometry array @@ -194,7 +195,7 @@ multipoint_range make_multipoint_range( } /** - * @brief Create multilinestring_range object from offset and point ranges + * @brief Create multipoint_range object from offset and point ranges * * @tparam IntegerRange Range to integers * @tparam PointRange Range to points @@ -210,6 +211,60 @@ auto make_multipoint_range(IntegerRange geometry_offsets, PointRange points) geometry_offsets.begin(), geometry_offsets.end(), points.begin(), points.end()); } +/** + * @ingroup ranges + * @brief Create a range object of multipoints from cuspatial::geometry_column_view. + * Specialization for points column. + * + * @pre points_column must be a cuspatial::geometry_column_view + */ +template +auto make_multipoints_range(GeometryColumnView const& points_column) +{ + CUSPATIAL_EXPECTS(points_column.geometry_type() == geometry_type_id::POINT, + "Must be POINT geometry type."); + auto geometry_iter = thrust::make_counting_iterator(0); + auto const& points_xy = points_column.child(); // Ignores x-y offset {0, 2, 4...} + + auto points_it = make_vec_2d_iterator(points_xy.template begin()); + + return multipoints(geometry_iter, + geometry_iter + points_column.size(), + points_it, + points_it + points_xy.size() / 2); +} + +/** + * @ingroup ranges + * @brief Create a range object of multipoints from cuspatial::geometry_column_view. + * Specialization for multipoints column. + * + * @pre multipoints_column must be a cuspatial::geometry_column_view + */ +template +auto make_multipoint_range(GeometryColumnView const& points_column) +{ + CUSPATIAL_EXPECTS(points_column.geometry_type() == geometry_type_id::POINT, + "Must be POINT geometry type."); + auto const& geometry_offsets = points_column.offsets(); + auto const& points_xy = points_column.child().child(); // Ignores x-y offset {0, 2, 4...} + + auto points_it = make_vec_2d_iterator(points_xy.template begin()); + + return multipoint_range(geometry_offsets.template begin(), + geometry_offsets.template end(), + points_it, + points_it + points_xy.size() / 2); +}; + } // namespace cuspatial #include diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 775c7dba4..20a1b7345 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -152,6 +152,74 @@ class multipolygon_range { CUSPATIAL_HOST_DEVICE bool is_valid_segment_id(IndexType1 segment_idx, IndexType2 ring_idx); }; +/** + * @ingroup ranges + * @brief Create a range object of multipolygon from cuspatial::geometry_column_view. + * Specialization for polygons column. + * + * @pre polygons_column must be a cuspatial::geometry_column_view + */ +template +auto make_multipolygon_range(GeometryColumnView const& polygons_column) +{ + CUSPATIAL_EXPECTS(polygons_column.geometry_type() == geometry_type_id::POLYGON, + "Must be polygon geometry type."); + auto geometry_iter = thrust::make_counting_iterator(0); + auto const& part_offsets = polygons_column.offsets(); + auto const& ring_offsets = polygons_column.child().offsets(); + auto const& points_xy = + polygons_column.child().child().child(); // Ignores x-y offset {0, 2, 4...} + + auto points_it = make_vec_2d_iterator(points_xy.template begin()); + + return multipolygon_range(geometry_iter, + geometry_iter + part_offsets.size(), + part_offsets.template begin(), + part_offsets.template end(), + ring_offsets.template begin(), + ring_offsets.template end(), + points_it, + points_it + points_xy.size() / 2); +} + +/** + * @ingroup ranges + * @brief Create a range object of multipolygon from cuspatial::geometry_column_view. + * Specialization for multipolygons column. + * + * @pre polygon_column must be a cuspatial::geometry_column_view + */ +template +auto make_multipolygon_range(GeometryColumnView const& polygons_column) +{ + CUSPATIAL_EXPECTS(polygons_column.geometry_type() == geometry_type_id::POLYGON, + "Must be polygon geometry type."); + auto const& geometry_offsets = polygons_column.offsets(); + auto const& part_offsets = polygons_column.child().offsets(); + auto const& ring_offsets = polygons_column.child().child().offsets(); + auto const& points_xy = + polygons_column.child().child().child().child(); // Ignores x-y offset {0, 2, 4...} + + auto points_it = make_vec_2d_iterator(points_xy.template begin()); + + return multipolygon_range(geometry_offsets.template begin(), + geometry_offsets.template end(), + part_offsets.template begin(), + part_offsets.template end(), + ring_offsets.template begin(), + ring_offsets.template end(), + points_it, + points_it + points_xy.size() / 2); +}; + } // namespace cuspatial #include From b136c0b3f1707e870af3c4fae2f2dbfebc3f89a5 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 17:49:05 -0800 Subject: [PATCH 22/36] Apply suggestions from code review --- .../experimental/detail/algorithm/is_point_in_polygon.cuh | 3 +-- .../cuspatial/experimental/detail/geometry/polygon_ref.cuh | 2 +- .../cuspatial/experimental/detail/point_polygon_distance.cuh | 1 + .../experimental/detail/ranges/multipolygon_range.cuh | 3 +-- .../cuspatial/experimental/ranges/multipolygon_range.cuh | 5 +++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh index 5ddf208d7..e9f43818d 100644 --- a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -16,7 +16,7 @@ #pragma once -#include "cuspatial/experimental/geometry_collection/multipoint_ref.cuh" +#include #include #include @@ -67,7 +67,6 @@ __device__ inline bool is_point_in_polygon(vec_2d const& test_point, PolygonR is_colinear = float_equal(run * rise_to_point, run_to_point * rise); if (is_colinear) { break; } - // y0_flag = a.y > test_point.y; y1_flag = a.y > test_point.y; if (y1_flag != y0_flag) { // Transform the following inequality to avoid division diff --git a/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh index 02a917cd4..3c1dfeb68 100644 --- a/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.cuh +++ b/cpp/include/cuspatial/experimental/detail/geometry/polygon_ref.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. diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index b39ba6dec..110a16087 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -87,6 +87,7 @@ void __global__ pairwise_point_polygon_distance_kernel(MultiPointRange multipoin if (geometry_idx == MultiPolygonRange::INVALID_INDEX) continue; if (intersects[geometry_idx]) { + // Leading thread of the pair writes to the output if (multipolygons.is_first_point_of_multipolygon(idx, geometry_idx)) distances[geometry_idx] = T{0.0}; continue; diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index 7e0e868bf..452d1be2e 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.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. @@ -224,7 +224,6 @@ multipolygon_range:: return multipolygon_range:: INVALID_INDEX; - auto part_idx = part_idx_from_ring_idx(ring_idx); return geometry_idx_from_part_idx(part_idx_from_ring_idx(ring_idx)); } diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 775c7dba4..7850a48db 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.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. @@ -100,6 +100,7 @@ class multipolygon_range { /// Return the iterator to the one past the last multipolygon in the range. CUSPATIAL_HOST_DEVICE auto end() { return multipolygon_end(); } + /// Given the index of a segment, return the geometry (multipolygon) index where the /// segment locates. /// Segment index is the index to the starting point of the segment. If the @@ -116,7 +117,7 @@ class multipolygon_range { template CUSPATIAL_HOST_DEVICE auto get_segment(IndexType segment_idx); - /// Returns `true` if `point_idx`th point is the first point of its + /// Returns `true` if `point_idx`th point is the first point of `geometry_idx`th /// multipolygon template CUSPATIAL_HOST_DEVICE bool is_first_point_of_multipolygon(IndexType1 point_idx, From 744f32f2e0febf59131ad274bd1282a3998a7b3f Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 17:52:43 -0800 Subject: [PATCH 23/36] add docs --- .../cuspatial/experimental/detail/point_polygon_distance.cuh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh index 110a16087..a3395de97 100644 --- a/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh +++ b/cpp/include/cuspatial/experimental/detail/point_polygon_distance.cuh @@ -43,6 +43,9 @@ namespace cuspatial { namespace detail { +/** + * @brief For each point in the multipoint, compute point-in-multipolygon in corresponding pair. + */ template struct point_in_multipolygon_test_functor { MultiPointRange multipoints; From bb6c63785a12cb42fd9124f1e9cbce64b97e9341 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 8 Mar 2023 17:53:19 -0800 Subject: [PATCH 24/36] style --- .../cuspatial/experimental/ranges/multipolygon_range.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 7850a48db..b9cf666ea 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -100,7 +100,7 @@ class multipolygon_range { /// Return the iterator to the one past the last multipolygon in the range. CUSPATIAL_HOST_DEVICE auto end() { return multipolygon_end(); } - + /// Given the index of a segment, return the geometry (multipolygon) index where the /// segment locates. /// Segment index is the index to the starting point of the segment. If the From ec23d6e58ca99d36fcbd725cb6874525196740d9 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 9 Mar 2023 17:37:01 -0800 Subject: [PATCH 25/36] add column API tests, augment column_factories --- .../distance/point_polygon_distance.hpp | 41 +++ .../ranges/multilinestring_range.cuh | 6 +- .../experimental/ranges/multipoint_range.cuh | 20 +- .../ranges/multipolygon_range.cuh | 10 +- .../cuspatial_test/column_factories.hpp | 285 ++++++++++++++++++ cpp/src/spatial/point_polygon_distance.cu | 123 ++++++++ cpp/tests/CMakeLists.txt | 3 + cpp/tests/column_factory.cpp | 98 ++++++ .../spatial/point_polygon_distance_test.cpp | 220 ++++++++++++++ 9 files changed, 788 insertions(+), 18 deletions(-) create mode 100644 cpp/include/cuspatial/distance/point_polygon_distance.hpp create mode 100644 cpp/include/cuspatial_test/column_factories.hpp create mode 100644 cpp/src/spatial/point_polygon_distance.cu create mode 100644 cpp/tests/column_factory.cpp create mode 100644 cpp/tests/spatial/point_polygon_distance_test.cpp diff --git a/cpp/include/cuspatial/distance/point_polygon_distance.hpp b/cpp/include/cuspatial/distance/point_polygon_distance.hpp new file mode 100644 index 000000000..9d3fc510d --- /dev/null +++ b/cpp/include/cuspatial/distance/point_polygon_distance.hpp @@ -0,0 +1,41 @@ +/* + * 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 { + +/** + * @ingroup distance + * @brief Compute pairwise (multi)point-to-(multi)polygon Cartesian distance + * + * @param multpoints Geometry column of multipoints + * @param multipolygons Geometry column of multipolygons + * @return Column of distances between each pair of input geometries + */ + +std::unique_ptr pairwise_point_polygon_distance( + geometry_column_view const& multipoints, + geometry_column_view const& multipolygons, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +} // namespace cuspatial diff --git a/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh b/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh index 4b881a811..c99b5ca92 100644 --- a/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multilinestring_range.cuh @@ -245,7 +245,7 @@ auto make_multilinestring_range(GeometryColumnView const& linestrings_column) "Must be Linestring geometry type."); auto geometry_iter = thrust::make_counting_iterator(0); auto const& part_offsets = linestrings_column.offsets(); - auto const& points_xy = linestrings_column.child().child(); // Ignores x-y offset {0, 2, 4...} + auto const& points_xy = linestrings_column.child().child(1); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); @@ -275,8 +275,8 @@ auto make_multilinestring_range(GeometryColumnView const& linestrings_column) "Must be Linestring geometry type."); auto const& geometry_offsets = linestrings_column.offsets(); auto const& parts = linestrings_column.child(); - auto const& part_offsets = parts.offsets(); - auto const& points_xy = parts.child().child(); // Ignores x-y offset {0, 2, 4...} + auto const& part_offsets = parts.child(0); + auto const& points_xy = parts.child(1).child(1); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); diff --git a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh index fa72bfe59..b1d6a6f10 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh @@ -221,9 +221,9 @@ auto make_multipoint_range(IntegerRange geometry_offsets, PointRange points) template -auto make_multipoints_range(GeometryColumnView const& points_column) + CUSPATIAL_ENABLE_IF(Type == collection_type_id::SINGLE), + typename GeometryColumnView> +auto make_multipoint_range(GeometryColumnView const& points_column) { CUSPATIAL_EXPECTS(points_column.geometry_type() == geometry_type_id::POINT, "Must be POINT geometry type."); @@ -232,10 +232,10 @@ auto make_multipoints_range(GeometryColumnView const& points_column) auto points_it = make_vec_2d_iterator(points_xy.template begin()); - return multipoints(geometry_iter, - geometry_iter + points_column.size(), - points_it, - points_it + points_xy.size() / 2); + return multipoint_range(geometry_iter, + thrust::next(geometry_iter, points_column.size() + 1), + points_it, + points_it + points_xy.size() / 2); } /** @@ -248,14 +248,14 @@ auto make_multipoints_range(GeometryColumnView const& points_column) template + CUSPATIAL_ENABLE_IF(Type == collection_type_id::MULTI), + typename GeometryColumnView> auto make_multipoint_range(GeometryColumnView const& points_column) { CUSPATIAL_EXPECTS(points_column.geometry_type() == geometry_type_id::POINT, "Must be POINT geometry type."); auto const& geometry_offsets = points_column.offsets(); - auto const& points_xy = points_column.child().child(); // Ignores x-y offset {0, 2, 4...} + auto const& points_xy = points_column.child().child(1); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); diff --git a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh index 20a1b7345..8c72f78c7 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipolygon_range.cuh @@ -170,9 +170,9 @@ auto make_multipolygon_range(GeometryColumnView const& polygons_column) "Must be polygon geometry type."); auto geometry_iter = thrust::make_counting_iterator(0); auto const& part_offsets = polygons_column.offsets(); - auto const& ring_offsets = polygons_column.child().offsets(); + auto const& ring_offsets = polygons_column.child().child(0); auto const& points_xy = - polygons_column.child().child().child(); // Ignores x-y offset {0, 2, 4...} + polygons_column.child().child(1).child(1); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); @@ -203,10 +203,10 @@ auto make_multipolygon_range(GeometryColumnView const& polygons_column) CUSPATIAL_EXPECTS(polygons_column.geometry_type() == geometry_type_id::POLYGON, "Must be polygon geometry type."); auto const& geometry_offsets = polygons_column.offsets(); - auto const& part_offsets = polygons_column.child().offsets(); - auto const& ring_offsets = polygons_column.child().child().offsets(); + auto const& part_offsets = polygons_column.child().child(0); + auto const& ring_offsets = polygons_column.child().child(1).child(0); auto const& points_xy = - polygons_column.child().child().child().child(); // Ignores x-y offset {0, 2, 4...} + polygons_column.child().child(1).child(1).child(1); // Ignores x-y offset {0, 2, 4...} auto points_it = make_vec_2d_iterator(points_xy.template begin()); diff --git a/cpp/include/cuspatial_test/column_factories.hpp b/cpp/include/cuspatial_test/column_factories.hpp new file mode 100644 index 000000000..53643be9f --- /dev/null +++ b/cpp/include/cuspatial_test/column_factories.hpp @@ -0,0 +1,285 @@ +/* + * 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 + +namespace cuspatial { +namespace test { + +using namespace cudf; +using namespace cudf::test; + +std::unique_ptr coords_offsets(size_type num_points, rmm::cuda_stream_view stream) +{ + auto zero = make_fixed_width_scalar(0, stream); + auto two = make_fixed_width_scalar(2, stream); + + return sequence(num_points + 1, *zero, *two); +} + +template +std::unique_ptr make_non_nullable_lists_column(std::unique_ptr offset, + std::unique_ptr child) +{ + auto size = offset->size() - 1; + return make_lists_column(size, std::move(offset), std::move(child), 0, {}); +} + +template +std::unique_ptr make_non_nullable_lists_column(std::initializer_list offset, + std::unique_ptr child) +{ + auto d_offset = fixed_width_column_wrapper(offset).release(); + return make_non_nullable_lists_column(std::move(d_offset), std::move(child)); +} + +template +std::unique_ptr make_non_nullable_lists_column(std::unique_ptr offset, + std::initializer_list child) +{ + auto d_child = fixed_width_column_wrapper(child).release(); + return make_non_nullable_lists_column(std::move(offset), std::move(d_child)); +} + +/** + * @brief helper function to make a point column + * + * A point column has cudf type LIST + * + * Example: + * [POINT (0 0), POINT (1 1), POINT (2 2)] + * Offset 0 2 4 6 + * Child 0 0 1 1 2 2 + * + * @tparam T Coordinate value type + * @param point_coords interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return Intersection Result + * @return A cudf LIST column with point data + */ +template +std::pair> make_point_column( + std::initializer_list&& point_coords, rmm::cuda_stream_view stream) +{ + auto num_points = point_coords.size() / 2; + + return {collection_type_id::SINGLE, + make_non_nullable_lists_column(coords_offsets(num_points, stream), point_coords)}; +} + +/** + * @brief helper function to make a multipoint column + * + * A multipoint column has cudf type LIST> + * + * Example: + * [MULTIPOINT (POINT (0 0), POINT (1 1)), MULTIPOINT (POINT (2 2), POINT (3 3))] + * Offset 0 2 5 + * Offset 0 2 4 6 8 + * Child 0 0 1 1 2 2 3 3 + * + * @tparam T Coordinate value type + * @param multipoint_offsets Offset to the starting position for each multipoint + * @param point_coords Interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return A cudf LIST column with multipoint data + */ +template +std::pair> make_point_column( + std::initializer_list&& multipoint_offsets, + std::initializer_list point_coords, + rmm::cuda_stream_view stream) +{ + auto num_points = point_coords.size() / 2; + + return {collection_type_id::MULTI, + make_non_nullable_lists_column( + multipoint_offsets, + make_non_nullable_lists_column(coords_offsets(num_points, stream), point_coords))}; +} + +/** + * @brief helper function to make a linestring column + * + * A multipoint column has cudf type LIST> + * + * Example: + * [LINESTRING (0 0, 1 1, 2 2), LINESTRING (3 3, 4 4)] + * Offset 0 3 5 + * Offset 0 2 4 6 8 + * Child 0 0 1 1 2 2 3 3 4 4 + * + * @tparam T Coordinate value type + * @param linestring_offsets Offset to the starting position for each linestring + * @param point_coords Interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return A cudf LIST column with linestring data + */ +template +std::pair> make_linestring_column( + std::initializer_list&& linestring_offsets, + std::initializer_list&& linestring_coords, + rmm::cuda_stream_view stream) +{ + auto num_points = linestring_coords.size() / 2; + + return { + collection_type_id::SINGLE, + make_non_nullable_lists_column( + linestring_offsets, + make_non_nullable_lists_column(coords_offsets(num_points, stream), linestring_coords))}; +} + +/** + * @brief helper function to make a multilinestring column + * + * A multilinestring column has cudf type LIST>> + * + * Example: + * [ + * MULTILINESTRING (LINESTRING (0 0, 1 1), LINESTRING (2 2, 3 3)), + * MULTILINESTRING (LINESTRING (4 4, 5 5)) + * ] + * Offset 0 2 3 + * Offset 0 2 4 6 + * Offset 0 2 4 6 8 10 + * Child 0 0 1 1 2 2 3 3 4 4 5 5 + * + * @tparam T Coordinate value type + * @param multilinestring_offsets Offset to the starting position for each multilinestring + * @param linestring_offsets Offset to the starting position for each linestring + * @param point_coords Interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return A cudf LIST column with multilinestring data + */ +template +std::pair> make_linestring_column( + std::initializer_list&& multilinestring_offsets, + std::initializer_list&& linestring_offsets, + std::initializer_list linestring_coords, + rmm::cuda_stream_view stream) +{ + auto num_points = linestring_coords.size() / 2; + return { + collection_type_id::MULTI, + make_non_nullable_lists_column( + multilinestring_offsets, + make_non_nullable_lists_column( + linestring_offsets, + make_non_nullable_lists_column(coords_offsets(num_points, stream), linestring_coords)))}; +} + +/** + * @brief helper function to make a polygon column + * + * A multilinestring column has cudf type LIST>> + * + * Example: + * [ + * POLYGON ((0 0, 1 1, 0 1, 0 0), (0 0, -1 0, -1 -1, 0 0)), + * POLYGON ((3 3, 4 4, 3 4, 3 3)) + * ] + * Offset 0 2 3 + * Offset 0 4 8 12 + * Offset 0 2 4 6 8 10 12 14 16 18 20 22 24 + * Child 0 0 1 1 0 1 0 0 0 0 -1 0 -1 -1 0 0 3 3 4 4 3 4 3 3 + * + * @tparam T Coordinate value type + * @param polygon_offsets Offset to the starting position for each polygon + * @param ring_offsets Offset to the starting position for each ring + * @param point_coords Interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return A cudf LIST column with polygon data + */ +template +std::pair> make_polygon_column( + std::initializer_list&& polygon_offsets, + std::initializer_list&& ring_offsets, + std::initializer_list polygon_coords, + rmm::cuda_stream_view stream) +{ + auto num_points = polygon_coords.size() / 2; + return { + collection_type_id::SINGLE, + make_non_nullable_lists_column( + polygon_offsets, + make_non_nullable_lists_column( + ring_offsets, + make_non_nullable_lists_column(coords_offsets(num_points, stream), polygon_coords)))}; +} + +/** + * @brief helper function to make a polygon column + * + * A multilinestring column has cudf type LIST>> + * + * Example: + * [ + * MULTIPOLYGON (POLYGON (0 0, 1 1, 0 1, 0 0), POLYGON (0 0, -1 0, -1 -1, 0 0)), + * MULTIPOLYGON (POLYGON ((3 3, 4 4, 3 4, 3 3)) + * ] + * + * Offset 0 1 2 3 + * Offset 0 4 8 12 + * Offset 0 2 4 6 8 10 12 14 16 18 20 22 24 + * Child 0 0 1 1 0 1 0 0 0 0 -1 0 -1 -1 0 0 3 3 4 4 3 4 3 3 + * + * @tparam T Coordinate value type + * @param multipolygon_offsets Offset to the starting position for each multipolygon + * @param polygon_offsets Offset to the starting position for each polygon + * @param ring_offsets Offset to the starting position for each ring + * @param point_coords Interleaved x-y coordinates of the points + * @param stream The CUDA stream on which to perform computations + * + * @return A cudf LIST column with multipolygon data + */ +template +std::pair> make_polygon_column( + std::initializer_list&& multipolygon_offsets, + std::initializer_list&& polygon_offsets, + std::initializer_list&& ring_offsets, + std::initializer_list polygon_coords, + rmm::cuda_stream_view stream) +{ + auto num_points = polygon_coords.size() / 2; + return { + collection_type_id::MULTI, + make_non_nullable_lists_column( + multipolygon_offsets, + make_non_nullable_lists_column( + polygon_offsets, + make_non_nullable_lists_column( + ring_offsets, + make_non_nullable_lists_column(coords_offsets(num_points, stream), polygon_coords))))}; +} + +} // namespace test +} // namespace cuspatial diff --git a/cpp/src/spatial/point_polygon_distance.cu b/cpp/src/spatial/point_polygon_distance.cu new file mode 100644 index 000000000..798ee86ad --- /dev/null +++ b/cpp/src/spatial/point_polygon_distance.cu @@ -0,0 +1,123 @@ +/* + * 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/iterator.hpp" +#include "../utility/multi_geometry_dispatch.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace cuspatial { + +namespace detail { + +namespace { + +template +struct pairwise_point_polygon_distance_impl { + using SizeType = cudf::device_span::size_type; + + template )> + std::unique_ptr operator()(geometry_column_view const& multipoints, + geometry_column_view const& multipolygons, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + auto multipoints_range = make_multipoint_range(multipoints); + auto multipolygons_range = + make_multipolygon_range(multipolygons); + + auto output = cudf::make_numeric_column( + multipoints.coordinate_type(), multipoints.size(), cudf::mask_state::UNALLOCATED, stream, mr); + + cuspatial::pairwise_point_polygon_distance( + multipoints_range, multipolygons_range, output->mutable_view().begin(), stream); + return output; + } + + template ), typename... Args> + std::unique_ptr operator()(Args&&...) + + { + CUSPATIAL_FAIL("Point-polygon distance API only supports floating point coordinates."); + } +}; + +} // namespace + +template +struct pairwise_point_polygon_distance { + std::unique_ptr operator()(geometry_column_view const& multipoints, + geometry_column_view const& multipolygons, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) + { + return cudf::type_dispatcher( + multipoints.coordinate_type(), + pairwise_point_polygon_distance_impl{}, + multipoints, + multipolygons, + stream, + mr); + } +}; + +} // namespace detail + +std::unique_ptr pairwise_point_polygon_distance( + geometry_column_view const& multipoints, + geometry_column_view const& multipolygons, + rmm::mr::device_memory_resource* mr) +{ + CUSPATIAL_EXPECTS(multipoints.geometry_type() == geometry_type_id::POINT && + multipolygons.geometry_type() == geometry_type_id::POLYGON, + "Unexpected input geometry types."); + + CUSPATIAL_EXPECTS(multipoints.coordinate_type() == multipolygons.coordinate_type(), + "Input geometries must have the same coordinate data types."); + + return multi_geometry_double_dispatch( + multipoints.collection_type(), + multipolygons.collection_type(), + multipoints, + multipolygons, + rmm::cuda_stream_default, + mr); +} + +} // namespace cuspatial diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 821c7881b..26af8594a 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -94,6 +94,9 @@ ConfigureTest(POINT_LINESTRING_DISTANCE_TEST ConfigureTest(LINESTRING_DISTANCE_TEST spatial/linestring_distance_test.cpp) +ConfigureTest(POINT_POLYGON_DISTANCE_TEST + spatial/point_polygon_distance_test.cpp) + ConfigureTest(LINESTRING_INTERSECTION_TEST spatial/linestring_intersection_test.cpp) diff --git a/cpp/tests/column_factory.cpp b/cpp/tests/column_factory.cpp new file mode 100644 index 000000000..6b23903a7 --- /dev/null +++ b/cpp/tests/column_factory.cpp @@ -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 + +namespace cuspatial { +namespace test { + +using namespace cudf; +using namespace cudf::test; + +std::unique_ptr coords_offset(size_type num_points, rmm::cuda_stream_view stream) +{ + auto zero = make_fixed_width_scalar(0, stream); + auto two = make_fixed_width_scalar(2, stream); + + return cudf::sequence(num_points, *zero, *two); +} + +// helper function to make a point column +template +std::pair> make_point_column( + std::initializer_list&& point_coords, rmm::cuda_stream_view stream) +{ + auto num_points = point_coords.size() / 2; + auto size = point_offsets.size() - 1; + + auto zero = make_fixed_width_scalar(0, stream); + auto two = make_fixed_width_scalar(2, stream); + auto offsets_column = wrapper(point_offsets).release(); + auto coords_offset = cudf::sequence(num_points + 1, *zero, *two); + auto coords_column = wrapper(point_coords).release(); + + return {collection_type_id::SINGLE, + cudf::make_lists_column( + size, + std::move(offsets_column), + cudf::make_lists_column( + num_points, std::move(coords_offset), std::move(coords_column), 0, {}), + 0, + {})}; +} + +// helper function to make a multipoint column +template +std::pair> make_point_column( + std::initializer_list&& multipoint_offsets, + std::initializer_list&& point_offsets, + std::initializer_list point_coords, + rmm::cuda_stream_view stream) +{ + auto geometry_size = multipoint_offsets.size() - 1; + auto part_size = point_offsets.size() - 1; + auto num_points = point_coords.size() / 2; + + auto zero = make_fixed_width_scalar(0, stream); + auto two = make_fixed_width_scalar(2, stream); + auto geometry_column = wrapper(multipoint_offsets).release(); + auto part_column = wrapper(point_offsets).release(); + auto coords_offset = cudf::sequence(num_points + 1, *zero, *two); + auto coord_column = wrapper(point_coords).release(); + + return {collection_type_id::MULTI, + cudf::make_lists_column( + geometry_size, + std::move(geometry_column), + cudf::make_lists_column( + part_size, + std::move(part_column), + cudf::make_lists_column( + num_points, std::move(coords_offset), std::move(coord_column), 0, {}), + 0, + {}), + 0, + {})}; +} + +} // namespace test +} // namespace cuspatial diff --git a/cpp/tests/spatial/point_polygon_distance_test.cpp b/cpp/tests/spatial/point_polygon_distance_test.cpp new file mode 100644 index 000000000..e881d2490 --- /dev/null +++ b/cpp/tests/spatial/point_polygon_distance_test.cpp @@ -0,0 +1,220 @@ +/* + * 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 + +using namespace cuspatial; +using namespace cuspatial::test; + +using namespace cudf; +using namespace cudf::test; + +template +struct PairwisePointPolygonDistanceTest : public ::testing::Test { + rmm::cuda_stream_view stream() { return rmm::cuda_stream_default; } + + void run_single(geometry_column_view points, + geometry_column_view polygons, + std::initializer_list expected) + { + auto got = pairwise_point_polygon_distance(points, polygons); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*got, fixed_width_column_wrapper(expected)); + } +}; + +struct PairwisePointPolygonDistanceTestUntyped : public ::testing::Test { + rmm::cuda_stream_view stream() { return rmm::cuda_stream_default; } +}; + +using TestTypes = ::testing::Types; + +TYPED_TEST_CASE(PairwisePointPolygonDistanceTest, TestTypes); + +TYPED_TEST(PairwisePointPolygonDistanceTest, SingleToSingleEmpty) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column(std::initializer_list{}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0}, {0}, std::initializer_list{}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, SingleToMultiEmpty) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column(std::initializer_list{}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0}, {0}, {0}, std::initializer_list{}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, MultiToSingleEmpty) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0}, std::initializer_list{}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0}, {0}, std::initializer_list{}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, MultiToMultiEmpty) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0}, std::initializer_list{}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0}, {0}, {0}, std::initializer_list{}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, SingleToSingleOnePair) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {1.4142135623730951}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, SingleToMultiOnePair) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {1.4142135623730951}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, MultiToSingleOnePair) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0, 1}, {0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {1.4142135623730951}); +}; + +TYPED_TEST(PairwisePointPolygonDistanceTest, MultiToMultiOnePair) +{ + using T = TypeParam; + + auto [ptype, points] = make_point_column({0, 1}, {0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + CUSPATIAL_RUN_TEST(this->run_single, + geometry_column_view(points->view(), ptype, geometry_type_id::POINT), + geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON), + {1.4142135623730951}); +}; + +TEST_F(PairwisePointPolygonDistanceTestUntyped, SizeMismatch) +{ + auto [ptype, points] = make_point_column({0, 1, 2}, {0.0, 0.0, 1.0, 1.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + auto points_view = geometry_column_view(points->view(), ptype, geometry_type_id::POINT); + auto polygons_view = geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON); + + EXPECT_THROW(pairwise_point_polygon_distance(points_view, polygons_view), cuspatial::logic_error); +}; + +TEST_F(PairwisePointPolygonDistanceTestUntyped, TypeMismatch) +{ + auto [ptype, points] = make_point_column({0, 1}, {0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + auto points_view = geometry_column_view(points->view(), ptype, geometry_type_id::POINT); + auto polygons_view = geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON); + + EXPECT_THROW(pairwise_point_polygon_distance(points_view, polygons_view), cuspatial::logic_error); +}; + +TEST_F(PairwisePointPolygonDistanceTestUntyped, WrongGeometryType) +{ + auto [ltype, lines] = make_linestring_column({0, 1}, {0, 1}, {0.0, 0.0}, this->stream()); + + auto [polytype, polygons] = + make_polygon_column({0, 1}, {0, 1}, {0, 4}, {1, 1, 1, 2, 2, 2, 1, 1}, this->stream()); + + auto lines_view = geometry_column_view(lines->view(), ltype, geometry_type_id::LINESTRING); + auto polygons_view = geometry_column_view(polygons->view(), polytype, geometry_type_id::POLYGON); + + EXPECT_THROW(pairwise_point_polygon_distance(lines_view, polygons_view), cuspatial::logic_error); +}; From 8319e7c0f387ee86c16a5968d64d25976a0f2e02 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 10 Mar 2023 09:55:35 -0800 Subject: [PATCH 26/36] fix bug in PiP tests --- .../detail/algorithm/is_point_in_polygon.cuh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh index e9f43818d..c59322803 100644 --- a/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh +++ b/cpp/include/cuspatial/experimental/detail/algorithm/is_point_in_polygon.cuh @@ -33,6 +33,13 @@ namespace detail { * See "Crossings test" section of http://erich.realtimerendering.com/ptinpoly/ * The improvement in addenda is also addopted to remove divisions in this kernel. * + * @tparam T type of coordinate + * @tparam PolygonRef polygon_ref type + * @param test_point point to test for point in polygon + * @param polygon polygon to test for point in polygon + * @return boolean to indicate if point is inside the polygon. + * `false` if point is on the edge of the polygon. + * * TODO: the ultimate goal of refactoring this as independent function is to remove * src/utility/point_in_polygon.cuh and its usage in quadtree_point_in_polygon.cu. It isn't * possible today without further work to refactor quadtree_point_in_polygon into header only @@ -104,10 +111,10 @@ __device__ inline bool is_point_in_polygon(Cart2d const& test_point, Cart2dIt poly_points_first, Cart2dItDiffType const& num_poly_points) { - auto polygon = polygon_ref{ring_offsets_first + poly_begin, - ring_offsets_first + poly_end, + auto polygon = polygon_ref{thrust::next(ring_offsets_first, poly_begin), + thrust::next(ring_offsets_first, poly_end + 1), poly_points_first, - poly_points_first + num_poly_points}; + thrust::next(poly_points_first, num_poly_points)}; return is_point_in_polygon(test_point, polygon); } From 43cc02a8f581461ad126aa16b6a672090fe9b666 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 10 Mar 2023 10:29:44 -0800 Subject: [PATCH 27/36] add validation checks --- .../cuspatial/detail/utility/validation.hpp | 28 +++++++++++++++++++ .../detail/ranges/multipolygon_range.cuh | 6 +++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cpp/include/cuspatial/detail/utility/validation.hpp b/cpp/include/cuspatial/detail/utility/validation.hpp index 41c07bf77..3eb0e7060 100644 --- a/cpp/include/cuspatial/detail/utility/validation.hpp +++ b/cpp/include/cuspatial/detail/utility/validation.hpp @@ -48,3 +48,31 @@ "Each polygon must have at least one (1) ring"); \ CUSPATIAL_EXPECTS(num_poly_points >= 4 * (num_poly_ring_offsets - 1), \ "Each ring must have at least four (4) vertices"); + +/** + * @brief Macro for validating the data array sizes for a multipolygon. + * + * Raises an exception if any of the following are false: + * - The number of multipolygon offsets is greater than zero. + * - The number of polygon offsets is greater than zero. + * - The number of ring offsets is greater than zero. + * - There is at least one ring offset per polygon offset. + * - There are at least four vertices per ring offset. + * + * MultiPolygons follow [GeoArrow data layout][1]. Offsets arrays (polygons and rings) have one more + * element than the number of items in the array. The last offset is always the sum of the previous + * offset and the size of that element. For example the last value in the ring offsets array is the + * last ring offset plus the number of rings in the last polygon. See + * [Arrow Variable-Size Binary layout](2). Note that an empty list still has one offset: {0}. + * + * Rings are assumed to be closed (closed means the first and last vertices of + * each ring are equal). Therefore rings must have at least 4 vertices. + * + * [1]: https://github.com/geoarrow/geoarrow/blob/main/format.md + * [2]: https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-layout + */ +#define CUSPATIAL_EXPECTS_VALID_MULTIPOLYGON_SIZES( \ + num_poly_points, num_multipoly_offsets, num_poly_offsets, num_poly_ring_offsets) \ + CUSPATIAL_EXPECTS(num_multipoly_offsets > 0, \ + "Multipolygon offsets must contain at least one (1) value"); \ + CUSPATIAL_EXPECTS_VALID_POLYGON_SIZES(num_poly_points, num_poly_offsets, num_poly_ring_offsets); diff --git a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh index 452d1be2e..d964ac847 100644 --- a/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh +++ b/cpp/include/cuspatial/experimental/detail/ranges/multipolygon_range.cuh @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -104,7 +105,10 @@ multipolygon_range::m _point_end(point_end) { static_assert(is_vec_2d>(), - "Coordinate range must be constructed with iterators to vec_2d."); + "Point iterator must be iterators to floating point vec_2d types."); + + CUSPATIAL_EXPECTS_VALID_MULTIPOLYGON_SIZES( + num_points(), num_multipolygons() + 1, num_polygons() + 1, num_rings() + 1); } template Date: Fri, 10 Mar 2023 12:05:11 -0800 Subject: [PATCH 28/36] add cython API --- .../cpp/distance/point_polygon_distance.pxd | 17 +++++++ python/cuspatial/cuspatial/_lib/distance.pyx | 44 ++++++++++++++++++- python/cuspatial/cuspatial/_lib/types.pxd | 7 +++ python/cuspatial/cuspatial/_lib/types.pyx | 7 +++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 python/cuspatial/cuspatial/_lib/cpp/distance/point_polygon_distance.pxd diff --git a/python/cuspatial/cuspatial/_lib/cpp/distance/point_polygon_distance.pxd b/python/cuspatial/cuspatial/_lib/cpp/distance/point_polygon_distance.pxd new file mode 100644 index 000000000..020bd4574 --- /dev/null +++ b/python/cuspatial/cuspatial/_lib/cpp/distance/point_polygon_distance.pxd @@ -0,0 +1,17 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.cpp.column.column cimport column + +from cuspatial._lib.cpp.column.geometry_column_view cimport ( + geometry_column_view, +) + + +cdef extern from "cuspatial/distance/point_polygon_distance.hpp" \ + namespace "cuspatial" nogil: + cdef unique_ptr[column] pairwise_point_polygon_distance( + const geometry_column_view & multipoints, + const geometry_column_view & multipolygons + ) except + diff --git a/python/cuspatial/cuspatial/_lib/distance.pyx b/python/cuspatial/cuspatial/_lib/distance.pyx index 9319278ee..f3d68717e 100644 --- a/python/cuspatial/cuspatial/_lib/distance.pyx +++ b/python/cuspatial/cuspatial/_lib/distance.pyx @@ -1,12 +1,15 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. -from libcpp.memory cimport unique_ptr +from libcpp.memory cimport make_shared, shared_ptr, unique_ptr from libcpp.utility cimport move from cudf._lib.column cimport Column from cudf._lib.cpp.column.column cimport column from cudf._lib.cpp.column.column_view cimport column_view +from cuspatial._lib.cpp.column.geometry_column_view cimport ( + geometry_column_view, +) from cuspatial._lib.cpp.distance.linestring_distance cimport ( pairwise_linestring_distance as c_pairwise_linestring_distance, ) @@ -16,7 +19,12 @@ from cuspatial._lib.cpp.distance.point_distance cimport ( from cuspatial._lib.cpp.distance.point_linestring_distance cimport ( pairwise_point_linestring_distance as c_pairwise_point_linestring_distance, ) +from cuspatial._lib.cpp.distance.point_polygon_distance cimport ( + pairwise_point_polygon_distance as c_pairwise_point_polygon_distance, +) from cuspatial._lib.cpp.optional cimport optional +from cuspatial._lib.cpp.types cimport collection_type_id, geometry_type_id +from cuspatial._lib.types cimport collection_type_py_to_c from cuspatial._lib.utils cimport unwrap_pyoptcol @@ -102,3 +110,35 @@ def pairwise_point_linestring_distance( c_linestring_points_xy, )) return Column.from_unique_ptr(move(c_result)) + + +def pairwise_point_polygon_distance( + point_collection_type, + Column multipoints, + Column multipolygons +): + + cdef collection_type_id point_multi_type = collection_type_py_to_c( + point_collection_type + ) + + cdef shared_ptr[geometry_column_view] c_multipoints = \ + make_shared[geometry_column_view]( + multipoints.view(), + point_multi_type, + geometry_type_id.POINT) + + cdef shared_ptr[geometry_column_view] c_multipolygons = \ + make_shared[geometry_column_view]( + multipolygons.view(), + collection_type_id.MULTI, + geometry_type_id.POLYGON) + + cdef unique_ptr[column] c_result + + with nogil: + c_result = move(c_pairwise_point_polygon_distance( + c_multipoints.get()[0], c_multipolygons.get()[0] + )) + + return Column.from_unique_ptr(move(c_result)) diff --git a/python/cuspatial/cuspatial/_lib/types.pxd b/python/cuspatial/cuspatial/_lib/types.pxd index 710724f47..6d3d1b3f9 100644 --- a/python/cuspatial/cuspatial/_lib/types.pxd +++ b/python/cuspatial/cuspatial/_lib/types.pxd @@ -2,6 +2,13 @@ from libc.stdint cimport uint8_t +from cuspatial._lib.cpp.types cimport collection_type_id, geometry_type_id + ctypedef uint8_t underlying_geometry_type_id_t ctypedef uint8_t underlying_collection_type_id_t + + +cdef geometry_type_id geometry_type_py_to_c(typ) except* + +cdef collection_type_id collection_type_py_to_c(typ) except* diff --git a/python/cuspatial/cuspatial/_lib/types.pyx b/python/cuspatial/cuspatial/_lib/types.pyx index 03354b5f1..103342cb2 100644 --- a/python/cuspatial/cuspatial/_lib/types.pyx +++ b/python/cuspatial/cuspatial/_lib/types.pyx @@ -28,3 +28,10 @@ class CollectionType(IntEnum): MULTI = ( collection_type_id.MULTI ) + + +cdef geometry_type_id geometry_type_py_to_c(typ : GeometryType): + return ( (typ.value)) + +cdef collection_type_id collection_type_py_to_c(typ : CollectionType): + return ( (typ.value)) From 3d5964baea3405920fcadbae639ce8761c7e6801 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 10 Mar 2023 15:47:46 -0800 Subject: [PATCH 29/36] add python API and tests --- python/cuspatial/cuspatial/__init__.py | 1 + .../cuspatial/core/spatial/__init__.py | 4 +- .../cuspatial/core/spatial/distance.py | 101 +++++++++++++ python/cuspatial/cuspatial/tests/conftest.py | 12 +- .../test_pairwise_point_polygon_distance.py | 142 ++++++++++++++++++ 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 python/cuspatial/cuspatial/tests/spatial/distance/test_pairwise_point_polygon_distance.py diff --git a/python/cuspatial/cuspatial/__init__.py b/python/cuspatial/cuspatial/__init__.py index bb9a237c9..2f3a27267 100644 --- a/python/cuspatial/cuspatial/__init__.py +++ b/python/cuspatial/cuspatial/__init__.py @@ -10,6 +10,7 @@ pairwise_point_distance, pairwise_point_linestring_distance, pairwise_point_linestring_nearest_points, + pairwise_point_polygon_distance, point_in_polygon, points_in_spatial_window, polygon_bounding_boxes, diff --git a/python/cuspatial/cuspatial/core/spatial/__init__.py b/python/cuspatial/cuspatial/core/spatial/__init__.py index b269707d6..ee07838f9 100644 --- a/python/cuspatial/cuspatial/core/spatial/__init__.py +++ b/python/cuspatial/cuspatial/core/spatial/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. from .bounding import linestring_bounding_boxes, polygon_bounding_boxes from .distance import ( @@ -7,6 +7,7 @@ pairwise_linestring_distance, pairwise_point_distance, pairwise_point_linestring_distance, + pairwise_point_polygon_distance, ) from .filtering import points_in_spatial_window from .indexing import quadtree_on_points @@ -26,6 +27,7 @@ "sinusoidal_projection", "pairwise_point_distance", "pairwise_linestring_distance", + "pairwise_point_polygon_distance", "pairwise_point_linestring_distance", "pairwise_point_linestring_nearest_points", "polygon_bounding_boxes", diff --git a/python/cuspatial/cuspatial/core/spatial/distance.py b/python/cuspatial/cuspatial/core/spatial/distance.py index 9bf9afcdb..56eac324b 100644 --- a/python/cuspatial/cuspatial/core/spatial/distance.py +++ b/python/cuspatial/cuspatial/core/spatial/distance.py @@ -10,16 +10,19 @@ pairwise_linestring_distance as cpp_pairwise_linestring_distance, pairwise_point_distance as cpp_pairwise_point_distance, pairwise_point_linestring_distance as c_pairwise_point_linestring_distance, + pairwise_point_polygon_distance as c_pairwise_point_polygon_distance, ) from cuspatial._lib.hausdorff import ( directed_hausdorff_distance as cpp_directed_hausdorff_distance, ) from cuspatial._lib.spatial import haversine_distance as cpp_haversine_distance +from cuspatial._lib.types import CollectionType from cuspatial.core.geoseries import GeoSeries from cuspatial.utils.column_utils import ( contains_only_linestrings, contains_only_multipoints, contains_only_points, + contains_only_polygons, ) @@ -382,6 +385,104 @@ def pairwise_point_linestring_distance( ) +def pairwise_point_polygon_distance(points: GeoSeries, polygons: GeoSeries): + """Compute distance between pairs of (multi)points and (multi)polygons + + The distance between a (multi)point and a (multi)polygons + is defined as the shortest distance between every point in the + multipoint and every edge of the (multi)polygon. If the multipoint and + multipolygon intersects, the distance is 0. + + This algorithm computes distance pairwise. The ith row in the result is + the distance between the ith (multi)point in `points` and the ith + (multi)polygon in `polygons`. + + Parameters + ---------- + points : GeoSeries + The (multi)points to compute the distance from. + polygons : GeoSeries + The (multi)polygons to compute the distance from. + + Returns + ------- + distance : cudf.Series + + Notes + ----- + The input `GeoSeries` must contain a single type geometry. + For example, `points` series cannot contain both points and polygons. + Currently, it is unsupported that `points` contains both points and + multipoints. + + Examples + -------- + Compute distance between a point and a polygon: + >>> from shapely.geometry import Point + >>> points = cuspatial.GeoSeries([Point(0, 0)]) + >>> polygons = cuspatial.GeoSeries([Point(1, 1).buffer(0.5)]) + >>> cuspatial.pairwise_point_polygon_distance(points, polygons) + 0 0.914214 + dtype: float64 + + Compute distance between a multipoint and a multipolygon + + >>> from shapely.geometry import MultiPoint + >>> mpoints = cuspatial.GeoSeries([MultiPoint([Point(0, 0), Point(1, 1)])]) + >>> mpolys = cuspatial.GeoSeries([ + ... MultiPoint([Point(2, 2), Point(1, 2)]).buffer(0.5)]) + >>> cuspatial.pairwise_point_polygon_distance(mpoints, mpolys) + 0 0.5 + dtype: float64 + """ + + if len(points) != len(polygons): + raise ValueError("Unmatched input geoseries length.") + + if len(points) == 0: + return cudf.Series(dtype=points.points.xy.dtype) + + if not contains_only_points(points): + raise ValueError("`points` array must contain only points") + + if not contains_only_polygons(polygons): + raise ValueError("`linestrings` array must contain only linestrings") + + if len(points.points.xy) > 0 and len(points.multipoints.xy) > 0: + raise NotImplementedError( + "Mixing point and multipoint geometries is not supported" + ) + + point_collection_type = ( + CollectionType.SINGLE + if len(points.points.xy > 0) + else CollectionType.MULTI + ) + + # Handle slicing in geoseries + points_column = ( + points._column.points._column + if point_collection_type == CollectionType.SINGLE + else points._column.mpoints._column + ) + points_column = points_column.take( + points._column._meta.union_offsets._column + ) + + polygon_column = polygons._column.polygons._column + polygon_column = polygon_column.take( + polygons._column._meta.union_offsets._column + ) + + return Series._from_data( + { + None: c_pairwise_point_polygon_distance( + point_collection_type, points_column, polygon_column + ) + } + ) + + def _flatten_point_series( points: GeoSeries, ) -> Tuple[ diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index 4cb492e53..1c37cee77 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021, NVIDIA CORPORATION. +# Copyright (c) 2020-2023, NVIDIA CORPORATION. import geopandas as gpd import numpy as np @@ -305,3 +305,13 @@ def factory(length): return mask return factory + + +@pytest.fixture +def naturalearth_cities(): + return gpd.read_file(gpd.datasets.get_path("naturalearth_cities")) + + +@pytest.fixture +def naturalearth_lowres(): + return gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) diff --git a/python/cuspatial/cuspatial/tests/spatial/distance/test_pairwise_point_polygon_distance.py b/python/cuspatial/cuspatial/tests/spatial/distance/test_pairwise_point_polygon_distance.py new file mode 100644 index 000000000..199c41208 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/spatial/distance/test_pairwise_point_polygon_distance.py @@ -0,0 +1,142 @@ +import geopandas as gpd +import pytest +from shapely.geometry import MultiPoint, MultiPolygon, Point, Polygon + +import cudf +from cudf.testing import assert_series_equal + +import cuspatial + + +def test_point_polygon_empty(): + lhs = cuspatial.GeoSeries.from_points_xy([]) + rhs = cuspatial.GeoSeries.from_polygons_xy([], [0], [0], [0]) + + got = cuspatial.pairwise_point_polygon_distance(lhs, rhs) + + expect = cudf.Series([], dtype="f8") + + assert_series_equal(got, expect) + + +def test_multipoint_polygon_empty(): + lhs = cuspatial.GeoSeries.from_multipoints_xy([], [0]) + rhs = cuspatial.GeoSeries.from_polygons_xy([], [0], [0], [0]) + + got = cuspatial.pairwise_point_polygon_distance(lhs, rhs) + + expect = cudf.Series([], dtype="f8") + + assert_series_equal(got, expect) + + +@pytest.mark.parametrize( + "points", [[Point(0, 0)], [MultiPoint([(1, 1), (2, 2)])]] +) +@pytest.mark.parametrize( + "polygons", + [ + [Polygon([(0, 1), (1, 0), (-1, 0), (0, 1)])], + [ + MultiPolygon( + [ + Polygon([(-2, 0), (-1, 0), (-1, -1), (-2, 0)]), + Polygon([(1, 0), (2, 0), (1, -1), (1, 0)]), + ] + ) + ], + ], +) +def test_one_pair(points, polygons): + lhs = gpd.GeoSeries(points) + rhs = gpd.GeoSeries(polygons) + + dlhs = cuspatial.GeoSeries(points) + drhs = cuspatial.GeoSeries(polygons) + + expect = lhs.distance(rhs) + got = cuspatial.pairwise_point_polygon_distance(dlhs, drhs) + + assert_series_equal(got, cudf.Series(expect)) + + +@pytest.mark.parametrize( + "points", + [ + [Point(0, 0), Point(3, -3)], + [MultiPoint([(1, 1), (2, 2)]), MultiPoint([(3, 3), (4, 4)])], + ], +) +@pytest.mark.parametrize( + "polygons", + [ + [ + Polygon([(0, 1), (1, 0), (-1, 0), (0, 1)]), + Polygon([(-4, -4), (-4, -5), (-5, -5), (-5, -4), (-5, -5)]), + ], + [ + MultiPolygon( + [ + Polygon([(0, 1), (1, 0), (-1, 0), (0, 1)]), + Polygon([(0, 1), (1, 0), (0, -1), (-1, 0), (0, 1)]), + ] + ), + MultiPolygon( + [ + Polygon( + [(-4, -4), (-4, -5), (-5, -5), (-5, -4), (-5, -5)] + ), + Polygon([(-2, 0), (-2, -2), (0, -2), (0, 0), (-2, 0)]), + ] + ), + ], + ], +) +def test_two_pair(points, polygons): + lhs = gpd.GeoSeries(points) + rhs = gpd.GeoSeries(polygons) + + dlhs = cuspatial.GeoSeries(points) + drhs = cuspatial.GeoSeries(polygons) + + expect = lhs.distance(rhs) + got = cuspatial.pairwise_point_polygon_distance(dlhs, drhs) + + assert_series_equal(got, cudf.Series(expect)) + + +def test_point_polygon_large(point_generator, polygon_generator): + N = 100 + points = gpd.GeoSeries(point_generator(N)) + polygons = gpd.GeoSeries(polygon_generator(N, 1.0, 1.5)) + + dpoints = cuspatial.from_geopandas(points) + dpolygons = cuspatial.from_geopandas(polygons) + + expect = points.distance(polygons) + got = cuspatial.pairwise_point_polygon_distance(dpoints, dpolygons) + + assert_series_equal(got, cudf.Series(expect)) + + +def test_point_polygon_geocities(naturalearth_cities, naturalearth_lowres): + N = 100 + gpu_cities = cuspatial.from_geopandas(naturalearth_cities.geometry) + gpu_countries = cuspatial.from_geopandas(naturalearth_lowres.geometry) + + print( + len(naturalearth_lowres), + len(naturalearth_lowres[: len(naturalearth_cities)]), + len(gpu_countries), + len(gpu_countries[: len(naturalearth_cities)]), + ) + + expect = naturalearth_cities.geometry[:N].distance( + naturalearth_lowres.geometry[:N] + ) + + got = cuspatial.pairwise_point_polygon_distance( + gpu_cities[:N], gpu_countries[:N] + ) + + assert_series_equal(cudf.Series(expect), got) From baed5a3f1a997f28f4cb12679a182c67da9d7873 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 10 Mar 2023 15:48:06 -0800 Subject: [PATCH 30/36] add user guide --- .../user_guide/cuspatial_api_examples.ipynb | 239 +++++++++++++++++- 1 file changed, 231 insertions(+), 8 deletions(-) diff --git a/docs/source/user_guide/cuspatial_api_examples.ipynb b/docs/source/user_guide/cuspatial_api_examples.ipynb index 56b5b4410..1f082b4ed 100644 --- a/docs/source/user_guide/cuspatial_api_examples.ipynb +++ b/docs/source/user_guide/cuspatial_api_examples.ipynb @@ -889,6 +889,229 @@ "print(gpu_polygons.head())" ] }, + { + "cell_type": "markdown", + "id": "f9724827-5cba-44c7-ae5a-47bf0b620ae2", + "metadata": {}, + "source": [ + "### cuspatial.pairwise_point_polygon_distance\n", + "\n", + "Using WGS 84 Pseudo-Mercator, distances are in meters." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "df5e6111-0c18-4452-b99d-b24e87523949", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "

\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
pop_estcontinentnameiso_a3gdp_md_estgeometrydistance_fromdistance
0889953.0OceaniaFijiFJI5496MULTIPOLYGON (((20037508.343 -1812498.413, 200...Vatican City1.969350e+07
158005463.0AfricaTanzaniaTZA63177POLYGON ((3774143.866 -105758.362, 3792946.708...San Marino5.929777e+06
2603253.0AfricaW. SaharaESH907POLYGON ((-964649.018 3205725.605, -964597.245...Vaduz3.421172e+06
337589262.0North AmericaCanadaCAN1736425MULTIPOLYGON (((-13674486.249 6274861.394, -13...Lobamba1.296059e+07
4328239523.0North AmericaUnited States of AmericaUSA21433226MULTIPOLYGON (((-13674486.249 6274861.394, -13...Luxembourg8.174897e+06
\n", + "
" + ], + "text/plain": [ + " pop_est continent name iso_a3 gdp_md_est \\\n", + "0 889953.0 Oceania Fiji FJI 5496 \n", + "1 58005463.0 Africa Tanzania TZA 63177 \n", + "2 603253.0 Africa W. Sahara ESH 907 \n", + "3 37589262.0 North America Canada CAN 1736425 \n", + "4 328239523.0 North America United States of America USA 21433226 \n", + "\n", + " geometry distance_from \\\n", + "0 MULTIPOLYGON (((20037508.343 -1812498.413, 200... Vatican City \n", + "1 POLYGON ((3774143.866 -105758.362, 3792946.708... San Marino \n", + "2 POLYGON ((-964649.018 3205725.605, -964597.245... Vaduz \n", + "3 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Lobamba \n", + "4 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Luxembourg \n", + "\n", + " distance \n", + "0 1.969350e+07 \n", + "1 5.929777e+06 \n", + "2 3.421172e+06 \n", + "3 1.296059e+07 \n", + "4 8.174897e+06 \n", + "(GPU)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cities = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_cities\")).to_crs(3857)\n", + "countries = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\")).to_crs(3857)\n", + "\n", + "gpu_cities = cuspatial.from_geopandas(cities)\n", + "gpu_countries = cuspatial.from_geopandas(countries)\n", + "\n", + "dist = cuspatial.pairwise_point_polygon_distance(\n", + " gpu_cities.geometry[:len(gpu_countries)], gpu_countries.geometry\n", + ")\n", + "\n", + "gpu_countries[\"distance_from\"] = cities.name\n", + "gpu_countries[\"distance\"] = dist\n", + "\n", + "gpu_countries.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6262aec6-753b-4c9c-938c-818b46899fc7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (1386304.644 5146502.579)\n", + "1 POINT (1385011.523 5455558.181)\n", + "2 POINT (1059390.803 5963928.580)\n", + "3 POINT (3473167.790 -3056995.462)\n", + "4 POINT (682388.790 6379291.919)\n", + " ... \n", + "238 POINT (-4810350.913 -2620812.957)\n", + "239 POINT (-5190490.090 -2699486.457)\n", + "240 POINT (16832903.820 -4011543.664)\n", + "241 POINT (11560960.460 144168.711)\n", + "242 POINT (12710800.486 2548415.574)\n", + "Name: geometry, Length: 243, dtype: geometry" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cities.geometry" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "46ce6936-7bb6-4daf-8f98-4ad79c963022", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 MULTIPOLYGON (((20037508.343 -1812498.413, 200...\n", + "1 POLYGON ((3774143.866 -105758.362, 3792946.708...\n", + "2 POLYGON ((-964649.018 3205725.605, -964597.245...\n", + "3 MULTIPOLYGON (((-13674486.249 6274861.394, -13...\n", + "4 MULTIPOLYGON (((-13674486.249 6274861.394, -13...\n", + " ... \n", + "172 POLYGON ((2096126.508 5765757.958, 2096127.988...\n", + "173 POLYGON ((2234260.104 5249565.284, 2204305.520...\n", + "174 POLYGON ((2292095.761 5139344.949, 2284604.344...\n", + "175 POLYGON ((-6866186.192 1204901.071, -6802177.4...\n", + "176 POLYGON ((3432408.751 390883.649, 3334408.389 ...\n", + "Name: geometry, Length: 177, dtype: geometry" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "countries.geometry" + ] + }, { "attachments": { "351aea0c-f37e-4ab9-bad2-c67bce69b5c3.png": { @@ -910,7 +1133,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 20, "id": "d1ade9da-c9e2-45c4-9685-dffeda3fd358", "metadata": { "tags": [] @@ -976,7 +1199,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 21, "id": "bf7b2256", "metadata": { "tags": [] @@ -993,7 +1216,7 @@ "dtype: int64" ] }, - "execution_count": 18, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1078,7 +1301,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 22, "id": "e3a0a9a3-0bdd-4f05-bcb5-7db4b99a44a3", "metadata": { "tags": [] @@ -1142,7 +1365,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 23, "id": "023bd25a-35be-435d-ab0b-ecbd7a47e147", "metadata": { "tags": [] @@ -1201,7 +1424,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 24, "id": "784aff8e-c9ed-4a81-aa87-bf301b3b90af", "metadata": { "tags": [] @@ -1216,7 +1439,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 25, "id": "fea24c78-cf5c-45c6-b860-338238e61323", "metadata": { "tags": [] @@ -1301,7 +1524,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.10.9" }, "vscode": { "interpreter": { From 402a8e336254cabbbe0c13e724938a97707eba54 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 20 Mar 2023 17:40:10 -0700 Subject: [PATCH 31/36] remove unused code pieces and files --- .../detail/utility/offset_to_keys.cuh | 51 ---------- .../experimental/ranges/multipoint_range.cuh | 2 - cpp/tests/column_factory.cpp | 98 ------------------- 3 files changed, 151 deletions(-) delete mode 100644 cpp/include/cuspatial/detail/utility/offset_to_keys.cuh delete mode 100644 cpp/tests/column_factory.cpp diff --git a/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh b/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh deleted file mode 100644 index 70665b4db..000000000 --- a/cpp/include/cuspatial/detail/utility/offset_to_keys.cuh +++ /dev/null @@ -1,51 +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 - -/** @brief Given list offset and row `i`, return a unique key that represent the list of `i`. - * - * The key is computed by performing a `upper_bound` search with `i` in the offset array. - * Then subtracts the position with the start of offset array. - * - * Example: - * offset: 0 0 0 1 3 4 4 4 - * i: 0 1 2 3 - * key: 3 4 4 5 - * - * Note that the values of `key`, {offset[3], offset[4], offset[5]} denotes the ending - * position of the first 3 non-empty list. - */ -template -struct offsets_to_keys_functor { - Iterator _offsets_begin; - Iterator _offsets_end; - - offsets_to_keys_functor(Iterator offset_begin, Iterator offset_end) - : _offsets_begin(offset_begin), _offsets_end(offset_end) - { - } - - template - IndexType __device__ operator()(IndexType i) - { - return thrust::distance(_offsets_begin, - thrust::upper_bound(thrust::seq, _offsets_begin, _offsets_end, i)); - } -}; diff --git a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh index b1d6a6f10..1d7e0a36c 100644 --- a/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh +++ b/cpp/include/cuspatial/experimental/ranges/multipoint_range.cuh @@ -52,8 +52,6 @@ class multipoint_range { using point_t = iterator_value_type; using element_t = iterator_vec_base_type; - int32_t INVALID_IDX = -1; - /** * @brief Construct a new multipoint array object */ diff --git a/cpp/tests/column_factory.cpp b/cpp/tests/column_factory.cpp deleted file mode 100644 index 6b23903a7..000000000 --- a/cpp/tests/column_factory.cpp +++ /dev/null @@ -1,98 +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 - -namespace cuspatial { -namespace test { - -using namespace cudf; -using namespace cudf::test; - -std::unique_ptr coords_offset(size_type num_points, rmm::cuda_stream_view stream) -{ - auto zero = make_fixed_width_scalar(0, stream); - auto two = make_fixed_width_scalar(2, stream); - - return cudf::sequence(num_points, *zero, *two); -} - -// helper function to make a point column -template -std::pair> make_point_column( - std::initializer_list&& point_coords, rmm::cuda_stream_view stream) -{ - auto num_points = point_coords.size() / 2; - auto size = point_offsets.size() - 1; - - auto zero = make_fixed_width_scalar(0, stream); - auto two = make_fixed_width_scalar(2, stream); - auto offsets_column = wrapper(point_offsets).release(); - auto coords_offset = cudf::sequence(num_points + 1, *zero, *two); - auto coords_column = wrapper(point_coords).release(); - - return {collection_type_id::SINGLE, - cudf::make_lists_column( - size, - std::move(offsets_column), - cudf::make_lists_column( - num_points, std::move(coords_offset), std::move(coords_column), 0, {}), - 0, - {})}; -} - -// helper function to make a multipoint column -template -std::pair> make_point_column( - std::initializer_list&& multipoint_offsets, - std::initializer_list&& point_offsets, - std::initializer_list point_coords, - rmm::cuda_stream_view stream) -{ - auto geometry_size = multipoint_offsets.size() - 1; - auto part_size = point_offsets.size() - 1; - auto num_points = point_coords.size() / 2; - - auto zero = make_fixed_width_scalar(0, stream); - auto two = make_fixed_width_scalar(2, stream); - auto geometry_column = wrapper(multipoint_offsets).release(); - auto part_column = wrapper(point_offsets).release(); - auto coords_offset = cudf::sequence(num_points + 1, *zero, *two); - auto coord_column = wrapper(point_coords).release(); - - return {collection_type_id::MULTI, - cudf::make_lists_column( - geometry_size, - std::move(geometry_column), - cudf::make_lists_column( - part_size, - std::move(part_column), - cudf::make_lists_column( - num_points, std::move(coords_offset), std::move(coord_column), 0, {}), - 0, - {}), - 0, - {})}; -} - -} // namespace test -} // namespace cuspatial From 3f1da026ce7119cee0b6725a8f400ef9afbbd32c Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 21 Mar 2023 10:18:39 -0700 Subject: [PATCH 32/36] address docs review --- .../cuspatial/distance/point_polygon_distance.hpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuspatial/distance/point_polygon_distance.hpp b/cpp/include/cuspatial/distance/point_polygon_distance.hpp index 9d3fc510d..ee50150bf 100644 --- a/cpp/include/cuspatial/distance/point_polygon_distance.hpp +++ b/cpp/include/cuspatial/distance/point_polygon_distance.hpp @@ -28,9 +28,14 @@ namespace cuspatial { * @ingroup distance * @brief Compute pairwise (multi)point-to-(multi)polygon Cartesian distance * - * @param multpoints Geometry column of multipoints + * @param multipoints Geometry column of multipoints * @param multipolygons Geometry column of multipolygons - * @return Column of distances between each pair of input geometries + * @param mr Device memory resource used to allocate the returned column. + * @return Column of distances between each pair of input geometries, same type as input coordinate types. + * + * @throw cuspatial::logic_error if `multipoints` and `multipolygons` has different coordinate types. + * @throw cuspatial::logic_error if `multipoints` is not a point column and `multipolygons` is not a polygon column. + * @throw cuspatial::logic_error if input column sizes mismatch. */ std::unique_ptr pairwise_point_polygon_distance( From 50b6c6bef953a22e32b9903b3de46b3ce86459e7 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 21 Mar 2023 10:29:49 -0700 Subject: [PATCH 33/36] style --- .../cuspatial/distance/point_polygon_distance.hpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuspatial/distance/point_polygon_distance.hpp b/cpp/include/cuspatial/distance/point_polygon_distance.hpp index ee50150bf..97276b581 100644 --- a/cpp/include/cuspatial/distance/point_polygon_distance.hpp +++ b/cpp/include/cuspatial/distance/point_polygon_distance.hpp @@ -31,10 +31,13 @@ namespace cuspatial { * @param multipoints Geometry column of multipoints * @param multipolygons Geometry column of multipolygons * @param mr Device memory resource used to allocate the returned column. - * @return Column of distances between each pair of input geometries, same type as input coordinate types. + * @return Column of distances between each pair of input geometries, same type as input coordinate + * types. * - * @throw cuspatial::logic_error if `multipoints` and `multipolygons` has different coordinate types. - * @throw cuspatial::logic_error if `multipoints` is not a point column and `multipolygons` is not a polygon column. + * @throw cuspatial::logic_error if `multipoints` and `multipolygons` has different coordinate + * types. + * @throw cuspatial::logic_error if `multipoints` is not a point column and `multipolygons` is not a + * polygon column. * @throw cuspatial::logic_error if input column sizes mismatch. */ From fcc438bcdc0a1cfbe06019a9bebd7a7baaabebc0 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 22 Mar 2023 11:20:52 -0700 Subject: [PATCH 34/36] revert user_guide --- .../user_guide/cuspatial_api_examples.ipynb | 3135 ++++++++--------- 1 file changed, 1456 insertions(+), 1679 deletions(-) diff --git a/docs/source/user_guide/cuspatial_api_examples.ipynb b/docs/source/user_guide/cuspatial_api_examples.ipynb index aa8b6e14d..761b11831 100644 --- a/docs/source/user_guide/cuspatial_api_examples.ipynb +++ b/docs/source/user_guide/cuspatial_api_examples.ipynb @@ -1,1711 +1,1488 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "c5fdf490-fa77-4e56-92d1-53101fff75ba", - "metadata": {}, - "source": [ - "# cuSpatial Python User's Guide\n", - "\n", - "cuSpatial is a GPU-accelerated Python library for spatial data analysis including distance and \n", - "trajectory computations, spatial data indexing and spatial join operations. cuSpatial's \n", - "Python API provides an accessible interface to high-performance spatial algorithms accelerated\n", - "by CUDA-enabled GPUs." - ] - }, - { - "cell_type": "markdown", - "id": "caadf3ca-be3c-4523-877c-4c35dd25093a", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "This guide provides a working example for all of the python API components of cuSpatial. \n", - "The following list links to each subsection.\n", - "\n", - "* [Installing cuSpatial](#Installing-cuspatial)\n", - "* [GPU accelerated memory layout](#GPU-accelerated-memory-layout)\n", - "* [Input / Output](#Input-/-Output)\n", - "* [Geopandas and cuDF integration](#Geopandas-and-cuDF-integration)\n", - "* [Trajectories](#Trajectories)\n", - "* [Bounding](#Bounding)\n", - "* [Projection](#Projection)\n", - "* [Distance](#Distance)\n", - "* [Filtering](#Filtering)\n", - "* [Spatial joins](#Spatial-joins)" - ] - }, - { - "cell_type": "markdown", - "id": "115c8382-f83f-476f-9a26-a64a45b3a8da", - "metadata": {}, - "source": [ - "## Installing cuSpatial\n", - "Read the [RAPIDS Quickstart Guide]( https://rapids.ai/start.html ) to learn more about installing all RAPIDS libraries, including cuSpatial.\n", - "\n", - "If you are working on a system with a CUDA-enabled GPU and have CUDA installed, uncomment the \n", - "following cell and install cuSpatial:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7265f9d2-9203-4da2-bbb2-b35c7f933641", - "metadata": {}, - "outputs": [], - "source": [ - "# !conda create -n rapids-22.08 -c rapidsai -c conda-forge -c nvidia \\ \n", - "# cuspatial=22.08 python=3.9 cudatoolkit=11.5 " - ] - }, - { - "cell_type": "markdown", - "id": "051b6e68-9ffd-473a-89e2-313fe1c59d18", - "metadata": {}, - "source": [ - "For other options to create a RAPIDS environment, such as docker or build from source, see \n", - "[RAPIDS Release Selector]( https://rapids.ai/start.html#get-rapids). \n", - "\n", - "If you wish to contribute to cuSpatial, you should create a source build using the excellent [rapids-compose](https://github.com/trxcllnt/rapids-compose)" - ] - }, - { - "cell_type": "markdown", - "id": "7b770cb4-793e-467a-a306-2d3409545748", - "metadata": {}, - "source": [ - "## GPU accelerated memory layout\n", - "\n", - "cuSpatial uses `GeoArrow` buffers, a GPU-friendly data format for geometric data that is well \n", - "suited for massively parallel programming. See [I/O](#io) on the fastest methods to get your \n", - "data into cuSpatial. GeoArrow extends [PyArrow](\n", - "https://arrow.apache.org/docs/python/index.html ) bindings and introduces several new types suited \n", - "for geometry applications. GeoArrow supports [ListArrays](\n", - "https://arrow.apache.org/docs/python/data.html#arrays) for `Points`, `MultiPoints`, \n", - "`LineStrings`, `MultiLineStrings`, `Polygons`, and `MultiPolygons`. Using an Arrow [DenseArray](\n", - "https://arrow.apache.org/docs/python/data.html#union-arrays), \n", - "GeoArrow stores heterogeneous types of Features. DataFrames of geometry objects and their \n", - "metadata can be loaded and transformed in a method similar to those in [GeoPandas.GeoSeries](\n", - "https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.html)." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "88d05bb9-c924-4d0b-8736-cd5183602d76", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Imports used throughout this notebook.\n", - "import cuspatial\n", - "import cudf\n", - "import cupy\n", - "import geopandas\n", - "import numpy as np\n", - "from shapely.geometry import *" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4937d4aa-4e32-49ab-a22e-96dfb0098d07", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# For deterministic result\n", - "np.random.seed(0)\n", - "cupy.random.seed(0)" - ] - }, - { - "cell_type": "markdown", - "id": "4b1251d1-558a-4899-8e7a-8066db0ad091", - "metadata": {}, - "source": [ - "## Input / Output\n", - "\n", - "The primary method of loading features into cuSpatial is using [cuspatial.from_geopandas](\n", - "https://docs.rapids.ai/api/cuspatial/stable/api_docs/io.html?highlight=from_geopandas#cuspatial.from_geopandas).\n", - "\n", - "One can also create feature geometries directly using any Python buffer that supports \n", - "`__array_interface__` for coordinates and their feature offsets." - ] - }, - { - "cell_type": "markdown", - "id": "11b973bd-87e1-4b67-ab8c-23c3b8291335", - "metadata": {}, - "source": [ - "### [cuspatial.from_geopandas](https://docs.rapids.ai/api/cuspatial/stable/api_docs/io.html?highlight=from_geopandas#cuspatial.from_geopandas)\n", - "\n", - "The easiest way to get data into cuSpatial is via `cuspatial.from_geopandas`." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "255fbfbe-8be1-498c-9a26-f4a3f31bdded", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " pop_est continent name iso_a3 gdp_md_est \\\n", - "0 889953.0 Oceania Fiji FJI 5496 \n", - "1 58005463.0 Africa Tanzania TZA 63177 \n", - "2 603253.0 Africa W. Sahara ESH 907 \n", - "3 37589262.0 North America Canada CAN 1736425 \n", - "4 328239523.0 North America United States of America USA 21433226 \n", - "\n", - " geometry \n", - "0 MULTIPOLYGON (((180.00000 -16.06713, 180.00000... \n", - "1 POLYGON ((33.90371 -0.95000, 34.07262 -1.05982... \n", - "2 POLYGON ((-8.66559 27.65643, -8.66512 27.58948... \n", - "3 MULTIPOLYGON (((-122.84000 49.00000, -122.9742... \n", - "4 MULTIPOLYGON (((-122.84000 49.00000, -120.0000... \n", - "(GPU)\n", - "\n" - ] - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\n", - " \"naturalearth_lowres\"\n", - "))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "print(gpu_dataframe.head())" - ] - }, - { - "cell_type": "markdown", - "id": "da5c775b-7458-4e3c-a573-e1bd060e3365", - "metadata": {}, - "source": [ - "## Geopandas and cuDF integration\n", - "\n", - "A cuSpatial [GeoDataFrame](\n", - "https://docs.rapids.ai/api/cuspatial/stable/api_docs/geopandas_compatibility.html#cuspatial.GeoDataFrame ) is a collection of [cudf](\n", - "https://docs.rapids.ai/api/cudf/stable/ ) [Series](\n", - "https://docs.rapids.ai/api/cudf/stable/api_docs/series.html ) and\n", - "[cuspatial.GeoSeries](\n", - "https://docs.rapids.ai/api/cuspatial/stable/api_docs/geopandas_compatibility.html#cuspatial.GeoSeries ) `\"geometry\"` objects. \n", - "Both types of series are stored on the GPU, and\n", - "`GeoSeries` is represented internally using `GeoArrow` data layout.\n", - "\n", - "One of the most important features of cuSpatial is that it is highly integrated with `cuDF`. \n", - "You can use any `cuDF` operation on cuSpatial non-feature columns, and most operations will work \n", - "with a `geometry` column. Operations that reduce or collate the number of rows in your DataFrame, \n", - "for example `groupby`, are not supported at this time." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "956451e2-a520-441d-a939-575ed179917b", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " pop_est continent name iso_a3 gdp_md_est \\\n", - "103 38041754.0 Asia Afghanistan AFG 19291 \n", - "125 2854191.0 Europe Albania ALB 15279 \n", - "82 43053054.0 Africa Algeria DZA 171091 \n", - "74 31825295.0 Africa Angola AGO 88815 \n", - "159 4490.0 Antarctica Antarctica ATA 898 \n", - ".. ... ... ... ... ... \n", - "2 603253.0 Africa W. Sahara ESH 907 \n", - "157 29161922.0 Asia Yemen YEM 22581 \n", - "70 17861030.0 Africa Zambia ZMB 23309 \n", - "48 14645468.0 Africa Zimbabwe ZWE 21440 \n", - "73 1148130.0 Africa eSwatini SWZ 4471 \n", - "\n", - " geometry \n", - "103 POLYGON ((66.51861 37.36278, 67.07578 37.35614... \n", - "125 POLYGON ((21.02004 40.84273, 20.99999 40.58000... \n", - "82 POLYGON ((-8.68440 27.39574, -8.66512 27.58948... \n", - "74 MULTIPOLYGON (((12.99552 -4.78110, 12.63161 -4... \n", - "159 MULTIPOLYGON (((-48.66062 -78.04702, -48.15140... \n", - ".. ... \n", - "2 POLYGON ((-8.66559 27.65643, -8.66512 27.58948... \n", - "157 POLYGON ((52.00001 19.00000, 52.78218 17.34974... \n", - "70 POLYGON ((30.74001 -8.34001, 31.15775 -8.59458... \n", - "48 POLYGON ((31.19141 -22.25151, 30.65987 -22.151... \n", - "73 POLYGON ((32.07167 -26.73382, 31.86806 -27.177... \n", - "\n", - "[177 rows x 6 columns]\n", - "(GPU)\n", - "\n" - ] - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "continents_dataframe = gpu_dataframe.sort_values(\"name\")\n", - "print(continents_dataframe)" - ] - }, - { - "cell_type": "markdown", - "id": "5453f308-317f-4775-ba3c-ff0633755bc4", - "metadata": {}, - "source": [ - "You can also convert between GPU-backed `cuspatial.GeoDataFrame` and CPU-backed \n", - "`geopandas.GeoDataFrame` with `from_geopandas` and `to_geopandas`, enabling you to \n", - "take advantage of any native GeoPandas operation. Note, however, that GeoPandas runs on \n", - " the CPU and therefore will not have as high performance as cuSpatial operations. The following \n", - "example displays the `Polygon` associated with the first item in the dataframe sorted \n", - "alphabetically by name." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "cd8d1c39-b44f-4d06-9e11-04c2dca4cf15", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "sorted_dataframe = gpu_dataframe.sort_values(\"name\")\n", - "host_dataframe = sorted_dataframe.to_geopandas()\n", - "host_dataframe['geometry'].iloc[0]" - ] - }, - { - "attachments": { - "046b885c-ab14-4c44-bd23-daebad76ebae.png": { - "image/png": "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CICA6wlQfpYkSeHh4W+88cb7779fVVV1//33f/bZZ6IoqqqKSUquhz4tJUKApwUzKgEBEACBOyDA87xWq1Xsx/Lly0tLS2fPnv3www+//fbbgiDQQtN3UDwedQ8BCLB7uKNWEAABELhdAjQqLMtybGzsO++88+KLLz7//PMvvPCCLMu0yvTtFoj73UtA497qUTsIgAAIgMDECVCONCnus88+m52dvXnz5ra2trfffpuWrhRFceKl4U73EkAE7F7+qB0EQAAEbpsAqawkSatWrfr444//+Mc/bt682Wq18jyP8eDbpum+ByDA7mOPmkEABEBgsgRof0OLxTJv3rxDhw5VVFQ89dRTqqpCgydL1A3PQYDdAB1VggAIgMCdE+B5XqfTWa3W2bNnHz58+MiRI9u3bycBxqrRd453GkqAAE8DZFQBAiAAAlNCwFGD//Vf//XnP//5vn37aIelKakPhbqUAATYpThRGAiAAAhMOwGtVitJ0je/+c2nn376hz/8YVNTE22vNO2GoMLbIwABvj1euBsEQAAEPI0ApUYrivLjH/84Ojp6165diqLwPI+OaE/zlJM9EGAnIPgIAiAAAt5HQBAERVGMRuO2bds++uijixcv0q5K3tcSf7IYAuxP3kZbQQAEfJcALYm1YsWKzMzMTz75hOM4nud9t7m+0DIIsC94EW0AARAAAcp/njFjRk5OTlVV1cjICK0UDTIeSwAC7LGugWEgAAIgcBsESIB5nk9KSurt7bVYLLfxMG51BwEIsDuoo04QAAEQmAIClHUVExNz69at4eFhjuOQhzUFmF1WJATYZShREAiAAAi4lwAN+g4MDAQGBur1egwDu9cdf7N2CPDfRIQbQAAEQMALCKiqSrnQjY2NcXFxwcHBCH893G0QYA93EMwDARAAgQkRUBRFEIT29vYTJ04UFhbqdDpszDAhcO67CQLsPvaoGQRAAARcR8Bms/E8/9577ymKsnHjRioYM5FcB9j1JWE/YNczRYkgAAIgMM0ELBZLQEDAyZMnd+7cuWvXruzsbEmSNBp8w0+zH26vOkTAt8cLd4MACICARxFQVXVkZCQgIKCuru673/3uhg0bNm3apCiKRqNB+OtRnhptDAR4NBNcAQEQAAHvICDLsiRJer3+1KlTGzZsmDdv3s9+9jNafwPq6/kuhAB7vo9gIQiAAAg4E1BV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" - } - }, - "cell_type": "markdown", - "id": "56b80722-38b6-457c-a7f0-591af6efd3ff", - "metadata": {}, - "source": [ - "## Trajectories\n", - "\n", - "A trajectory is a `LineString` coupled with a time sample for each point in the `LineString`. \n", - "Use `cuspatial.trajectory.derive_trajectories` to group trajectory datasets and sort by time.\n", - "\n", - "\n", - "\n", - "### [cuspatial.derive_trajectories](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.derive_trajectories)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "cb5acdad-53aa-418f-9948-8445515bd2b2", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " object_id x y timestamp\n", - "0 1 0.680146 0.874341 1970-01-01 00:00:00.125\n", - "1 1 0.843522 0.044402 1970-01-01 00:00:00.834\n", - "2 1 0.837039 0.351025 1970-01-01 00:00:01.335\n", - "3 1 0.946184 0.479038 1970-01-01 00:00:01.791\n", - "4 1 0.117322 0.182117 1970-01-01 00:00:02.474\n", - "0 0\n", - "1 2455\n", - "2 4899\n", - "3 7422\n", - "4 9924\n", - " ... \n", - "394 987408\n", - "395 989891\n", - "396 992428\n", - "397 994975\n", - "398 997448\n", - "Length: 399, dtype: int32\n" - ] - } - ], - "source": [ - "# 1m random trajectory samples\n", - "ids = cupy.random.randint(1, 400, 1000000)\n", - "timestamps = cupy.random.random(1000000)*1000000\n", - "xy= cupy.random.random(2000000)\n", - "trajs = cuspatial.GeoSeries.from_points_xy(xy)\n", - "sorted_trajectories, trajectory_offsets = \\\n", - " cuspatial.core.trajectory.derive_trajectories(ids, trajs, timestamps)\n", - "# sorted_trajectories is a DataFrame containing all trajectory samples\n", - "# sorted first by `object_id` and then by `timestamp`.\n", - "print(sorted_trajectories.head())\n", - "# trajectory_offsets is a Series containing the start position of each\n", - "# trajectory in sorted_trajectories.\n", - "print(trajectory_offsets)" - ] - }, - { - "cell_type": "markdown", - "id": "3c4a90f9-8661-4fda-9026-473e6ce87bd2", - "metadata": {}, - "source": [ - "`derive_trajectories` sorts the trajectories by `object_id`, then `timestamp`, and returns a \n", - "tuple containing the sorted trajectory data frame in the first index position and the offsets \n", - "buffer defining the start and stop of each trajectory in the second index position. \n", - "\n", - "### [cuspatial.trajectory_distances_and_speeds](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.trajectory_distances_and_speeds)\n", - "\n", - "Use `trajectory_distance_and_speed` to calculate the overall distance travelled in meters and \n", - "the speed of a set of trajectories with the same format as the result returned by `derive_trajectories`." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "03b75847-090d-40f8-8147-cb10b900d6ec", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " distance speed\n", - "trajectory_id \n", - "0 1.278996e+06 1280.320089\n", - "1 1.267179e+06 1268.370390\n", - "2 1.294437e+06 1295.905261\n", - "3 1.323413e+06 1323.956714\n", - "4 1.309590e+06 1311.561012\n" - ] - } - ], - "source": [ - "trajs = cuspatial.GeoSeries.from_points_xy(\n", - " sorted_trajectories[[\"x\", \"y\"]].interleave_columns()\n", - ")\n", - "d_and_s = cuspatial.core.trajectory.trajectory_distances_and_speeds(\n", - " len(cudf.Series(ids).unique()),\n", - " sorted_trajectories['object_id'],\n", - " trajs,\n", - " sorted_trajectories['timestamp']\n", - ")\n", - "print(d_and_s.head())" - ] - }, - { - "cell_type": "markdown", - "id": "eeb0da7c-0bdf-49ec-8244-4165acc96074", - "metadata": {}, - "source": [ - "Finally, compute the bounding boxes of trajectories that follow the format of the above two \n", - "examples:" - ] - }, - { - "attachments": { - "8c5d8b90-2241-45c2-b98c-20d48d1ee6b7.png": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlgAAAHCCAYAAAAzc7dkAAAgAElEQVR4AeydB3xUxfbHf7PpIZSEANJ7EQFRVEwBNoAoKrYnVuw+u2IF6wN774oNsVf8+95TnwUFFkjAhh1FlCJdkN4CSXb+n3M3d/duDJCyd/fu3t98Pjr3zs6dOfOdS/bszJlzACYSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAES2AsBtZfP+XFsCTQEcASAvgCSYisKeycBEiABEnAJgfkAPgaw2iXjtWWYVLBswRqRRkW5+gzAIRFpjY2QAAmQAAmQQM0JrAAwEMCimj/CmlYCHusNrx1F4DgqVzGYj/QMYN9eQEpKDDpnlyRAAiTgGAKtAVzsGGniUBAqWM6dtF7OFS1BJVMK6NoDyMkF9u8HNMhK0IFyWCRAAiRQIwK9a1SLlaolQAWrWiyOKOT2bbSnoXVboFHjQK9p6UCfA4Dc5tGWgv2RAAmQgFMIUEeox0wk1+NZPhp9An4Am6LfrQt6zGqYhHYdGoWN1JMEdO8JZGWVYsniHYAO+5g3JEACJJBABGTJnrYREZxQKlgRhBmFpv4E0CoK/biri+HD07Blx1fQMJbD+/TcF5u3bMGSZcsDHFq3S0frtlOA8lEoKdniLjgcLQmQgEsIfAhguEvGGpVhcvkvKpjZiaMJbNpxr6lcNWrYEI/ceRvenvQshgwcYBFbHQOkfIMBQ3paCnlJAiRAAiRAAtUSoIJVLRYWuoZAofcIKFxhjveGKy9D82a5yMzIwCN33oqrLr4AHk/QHK4L/BWfI7/oWLM+cxIgARIgARKojgAVrOqosMwdBAoLs6ExEYChQR0+2IsRhw8Ljl0phfNOPxUP3DoOGenpZnlDKP1vFHrHm8+ZHzAnARIgARIgAZMAFSyTBHP3EdDJjwEQXy/IzcnBzddcWS2DYUWD8NozT6J1y5bm5woa41DgfQP9RmSahcxJgARIgARIwCRABcskwdxdBAqKTgIwSgYtK1V33nQ9shtXumiohkS3zp3w1sSn0b/fAdZPT0b6lhLkD25vLeQ1CZAACZAACVDB4jvgPgIDBrQE9ARz4P8YcRQK+h9s3u42b9K4EZ596H5j29BSqS+U/2sMKBpkKeMlCZAACZCAywlQ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552jJgESIAESIAESsJEAFSwb4bJpEiABEiABEiABdxKgguXOeeeoSYAESIAESIAEbCRABctGuGyaBEiABEiABEjAnQSoYLlz3jlqEiABEiABEiABGwlQwbIRLpsmARIgARIgARJwJwEqWO6cd46aBEiABEiABEjARgJUsGyEy6ZJgARIgARIgATcSYAKljvnnaMmARIgARIgARKwkQAVLBvhsmkSIAESIAESIAF3EqCC5c5556hJgARIgARIgARsJEAFy0a4bJoESIAESIAESMCdBKhguXPeOWoSIAESIAESIAEbCVDBshEumyYBEiABEiABEnAnASpY7px3jpoESIAESIAESMBGAlSwbITLpkmABEiABEiABNxJgAqWO+edoyYBEiABEiABErCRABUsG+GyaRIgARIgARIgAXcSoILlznnnqEmABEiABEiABGwkQAXLRrhsmgRIgARIgARIwJ0EqGC5c945ahIgARIgARIgARsJUMGyES6bJgESIAESIAEScCcBKljunHeOmgRIgARIgARIwEYCVLBshMumSYAESIAESIAE3EmACpY7552jJgESIAESIAESsJEAFSwb4bJpEiABEiABEiABdxKgguXOeeeoSYAESIAESIAEbCRABctGuGyaBEiABEiABEjAnQSoYLlz3jlqEiABEiABEiABGwlQwbIRLpsmARIgARIgARJwJwEqWO6cd46aBEiABEiABEjARgJUsGyEy6ZJgARIgARIgATcSYAKljvnnaMmARIgARIgARKwkQAVLBvhsmkSIAESIAESIAF3EqCC5c5556hJgARIgARIgARsJEAFy0a4bJoESIAESIAESMCdBKhguXPeOWoSIAESIAESIAEbCVDBshEumyYBEiABEiABEnAnASpY7px3jpoESIAESIAESMBGAlSwbITLpkmABEiABEiABNxJgAqWO+edoyYBEiABEiABErCRABUsG+GyaRIgARIgARIgAXcSoILlznnnqEmABEiABEiABGwkQAXLRrhsmgRIgARIgARIwJ0Ekt05bI46igQUCgubQKc3hEdnoaK8ATwePzS2wq+2YpdnK+Z+timK8rArEiABEiABErCdABUs2xG7pIOCoe3gr9gXHt0bwH4AegHYF0ADaGFQDvgBKAVoowDwaCDdDxR4pcIGAD8BmAdl5hXzMGvWWpcQ5DBJ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" - } - }, - "cell_type": "markdown", - "id": "12a3ce7e-82b7-4b51-8227-5e157a48701c", - "metadata": { - "tags": [] - }, - "source": [ - "## Bounding\n", - "\n", - "Compute the bounding boxes of `n` polygons or linestrings:\n", - "\n", - "\n", - "\n", - "### [cuspatial.trajectory_bounding_boxes](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.trajectory_bounding_boxes)\n", - "\n", - "`trajectory_bounding_boxes` works out of the box with the values returned by `derive_trajectories`. \n", - "Its arguments are the number of incoming objects, the offsets of those objects, and x and y point buffers." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "452f60cb-28cc-4ad8-8aa2-9d73e3d56ec6", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " x_min y_min x_max y_max\n", - "0 0.000098 0.000243 0.999422 0.999068\n", - "1 0.000663 0.000414 0.999813 0.998456\n", - "2 0.000049 0.000220 0.999748 0.999220\n", - "3 0.000006 0.000303 0.999729 0.999762\n", - "4 0.001190 0.000074 0.999299 0.999858\n" - ] - } - ], - "source": [ - "bounding_boxes = cuspatial.core.trajectory.trajectory_bounding_boxes(\n", - " len(cudf.Series(ids, dtype=\"int32\").unique()),\n", - " sorted_trajectories['object_id'],\n", - " trajs\n", - ")\n", - "print(bounding_boxes.head())" - ] - }, - { - "cell_type": "markdown", - "id": "a56dfe17-1739-4b20-85c9-fcb5902c1585", - "metadata": {}, - "source": [ - "### [cuspatial.polygon_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.polygon_bounding_boxes)\n", - "\n", - "`polygon_bounding_boxes` supports more complex geometry objects such as `Polygon`s with multiple \n", - "rings. The combination of `part_offset` and `ring_offset` allows the function to use only the \n", - "exterior ring for computing the bounding box." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "9266aac4-f925-4fb7-b287-5f0b795d5756", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " minx miny maxx maxy\n", - "0 29.339998 -11.720938 40.316590 -0.950000\n", - "1 -17.063423 20.999752 -8.665124 27.656426\n", - "2 46.466446 40.662325 87.359970 55.385250\n", - "3 55.928917 37.144994 73.055417 45.586804\n", - "4 12.182337 -13.257227 31.174149 5.256088\n" - ] - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "single_polygons = cuspatial.from_geopandas(\n", - " host_dataframe['geometry'][host_dataframe['geometry'].type == \"Polygon\"]\n", - ")\n", - "bounding_box_polygons = cuspatial.core.spatial.bounding.polygon_bounding_boxes(\n", - " single_polygons\n", - ")\n", - "print(bounding_box_polygons.head())" - ] - }, - { - "cell_type": "markdown", - "id": "85197478-801c-4d2d-8b10-c1136d7bb15c", - "metadata": {}, - "source": [ - "### [cuspatial.linestring_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.linestring_bounding_boxes)\n", - "\n", - "Equivalently, we can treat trajectories as Linestrings and compute the same bounding boxes from \n", - "the above trajectory calculation more generally:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "15b5bb38-702f-4360-b48c-2e49ffd650d7", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " minx miny maxx maxy\n", - "0 -0.000002 0.000143 0.999522 0.999168\n", - "1 0.000563 0.000314 0.999913 0.998556\n", - "2 -0.000051 0.000120 0.999848 0.999320\n", - "3 -0.000094 0.000203 0.999829 0.999862\n", - "4 0.001090 -0.000026 0.999399 0.999958\n" - ] - } - ], - "source": [ - "lines = cuspatial.GeoSeries.from_linestrings_xy(\n", - " trajs.points.xy, trajectory_offsets, cupy.arange(len(trajectory_offsets))\n", - ")\n", - "trajectory_bounding_boxes = cuspatial.core.spatial.bounding.linestring_bounding_boxes(\n", - " lines, 0.0001\n", - ")\n", - "print(trajectory_bounding_boxes.head())" - ] - }, - { - "cell_type": "markdown", - "id": "81c4d3ca-5d3f-4ae1-ae8e-ac1e252f3e17", - "metadata": {}, - "source": [ - "## Projection\n", - "\n", - "cuSpatial provides a simple sinusoidal longitude / latitude to Cartesian coordinate transform. \n", - "This function requires an origin point to determine the scaling parameters for the lonlat inputs. \n", - "\n", - "### [cuspatial.sinusoidal_projection](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.sinusoidal_projection)\n", - "\n", - "The following cell converts the lonlat coordinates of the country of Afghanistan to Cartesian \n", - "coordinates in km, centered around the center of the country, suitable for graphing and display." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "a7a870dd-c0ae-41c1-a66c-cff4bd2db0ec", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 POINT (112.174 -281.590)\n", - "1 POINT (62.152 -280.852)\n", - "2 POINT (-5.573 -257.391)\n", - "3 POINT (-33.071 -243.849)\n", - "4 POINT (-98.002 -279.540)\n", - "dtype: geometry\n" - ] - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "afghanistan = gpu_dataframe['geometry'][gpu_dataframe['name'] == 'Afghanistan']\n", - "points = cuspatial.GeoSeries.from_points_xy(afghanistan.polygons.xy)\n", - "projected = cuspatial.sinusoidal_projection(\n", - " afghanistan.polygons.x.mean(),\n", - " afghanistan.polygons.y.mean(),\n", - " points\n", - ")\n", - "print(projected.head())" - ] - }, - { - "attachments": { - "image.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "c9085e80-e7ba-48e9-8c50-12e544e3af46", - "metadata": {}, - "source": [ - "## Distance\n", - "cuSpatial provides a growing suite of distance computation functions. Parallel distance functions \n", - "come in two main forms: pairwise, which computes a distance for each corresponding pair of input \n", - "geometries; and all-pairs, which computes a distance for the each element of the Cartesian product \n", - "of input geometries (for each input geometry in A, compute the distance from A to each input\n", - "geometry in B).\"\n", - " \n", - "Two pairwise distance functions are included in cuSpatial: `haversine` and `pairwise_linestring`. \n", - "The `hausdorff` clustering distances algorithm is also available, computing the hausdorff \n", - "distance across the cartesian product of its single input.\n", - "\n", - "### [cuspatial.directed_hausdorff_distance](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.directed_hausdorff_distance)\n", - "\n", - "The directed Hausdorff distance from one space to another is the greatest of all the distances \n", - "between any point in the first space to the closet point in the second. This is especially useful \n", - "as a similarity metric between trajectories.\n", - "\n", - "\n", - "\n", - "[Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "e75b0352-0f80-404d-a113-f301601cd5a3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 0 1 2 3 4 5 6 \\\n", - "0 0.000000 0.034755 0.031989 0.031959 0.031873 0.038674 0.029961 \n", - "1 0.030328 0.000000 0.038672 0.032086 0.031049 0.032170 0.032275 \n", - "2 0.027640 0.030539 0.000000 0.036737 0.033055 0.043447 0.028812 \n", - "3 0.031497 0.033380 0.035224 0.000000 0.032581 0.035484 0.030339 \n", - "4 0.031079 0.032256 0.035731 0.039084 0.000000 0.036416 0.031369 \n", - "\n", - " 7 8 9 ... 388 389 390 391 \\\n", - "0 0.029117 0.040962 0.033259 ... 0.031614 0.036447 0.035548 0.028233 \n", - "1 0.030215 0.034443 0.032998 ... 0.030594 0.035665 0.031473 0.031916 \n", - "2 0.031807 0.039269 0.033250 ... 0.031998 0.033636 0.034646 0.032615 \n", - "3 0.034792 0.045755 0.031810 ... 0.033623 0.031359 0.034923 0.032287 \n", - "4 0.030388 0.033751 0.034029 ... 0.030705 0.040339 0.034328 0.029027 \n", - "\n", - " 392 393 394 395 396 397 \n", - "0 0.034176 0.030057 0.033863 0.031111 0.034590 0.033850 \n", - "1 0.037483 0.033489 0.041403 0.029784 0.035374 0.038179 \n", - "2 0.036681 0.030642 0.038432 0.032481 0.034810 0.036695 \n", - "3 0.032808 0.029771 0.040891 0.030802 0.032279 0.038443 \n", - "4 0.035645 0.027703 0.037529 0.029356 0.031260 0.035501 \n", - "\n", - "[5 rows x 398 columns]\n" - ] - } - ], - "source": [ - "coordinates = sorted_trajectories[['x', 'y']].interleave_columns()\n", - "spaces = cuspatial.GeoSeries.from_multipoints_xy(\n", - " coordinates, trajectory_offsets\n", - ")\n", - "hausdorff_distances = cuspatial.core.spatial.distance.directed_hausdorff_distance(\n", - " spaces\n", - ")\n", - "print(hausdorff_distances.head())" - ] - }, - { - "attachments": { - "f73d72ad-8832-476e-9712-0676a4bbad10.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "993d7566-203e-4b87-adad-088e2fd92eed", - "metadata": {}, - "source": [ - "### [cuspatial.haversine_distance](https://docs.rapids.ai/api/cuspatial/stable/api_docs/gis.html#cuspatial.haversine_distance)\n", - "\n", - "Haversine distance is the great circle distance between longitude and latitude pairs. cuSpatial \n", - "uses the `lon/lat` ordering to better reflect the cartesian coordinates of great circle \n", - "coordinates: `x/y`.\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "70f66319-c4d2-4a93-ab98-0debcce4a719", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "0 9959.695143\n", - "1 9803.166859\n", - "2 9876.857085\n", - "3 9925.097106\n", - "4 9927.268486\n", - "Name: None, dtype: float64" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "polygons_first = gpu_dataframe['geometry'][0:10]\n", - "polygons_second = gpu_dataframe['geometry'][10:20]\n", - "\n", - "points_first = polygons_first.polygons.xy[0:1000]\n", - "points_second = polygons_second.polygons.xy[0:1000]\n", - "\n", - "first = cuspatial.GeoSeries.from_points_xy(points_first)\n", - "second = cuspatial.GeoSeries.from_points_xy(points_second)\n", - "\n", - "# The number of coordinates in two sets of polygons vary, so\n", - "# we'll just compare the first set of 1000 values here.\n", - "distances_in_meters = cuspatial.haversine_distance(\n", - " first, second\n", - ")\n", - "cudf.Series(distances_in_meters).head()" - ] - }, - { - "cell_type": "markdown", - "id": "7f2239c5-58d0-4912-9bd7-246cc6741c0a", - "metadata": {}, - "source": [ - "### Pairwise distance\n", - "\n", - "`pairwise_linestring_distance` computes the distance between a `GeoSeries` of Linestrings of \n", - "length `n` and a corresponding `GeoSeries` of Linestrings of `n` length. It returns the \n", - "minimum distance from any point in the first linestring of the pair to the nearest segment \n", - "or point within the second Linestring of the pair.\n", - "\n", - "The input accepts a pair of geoseries as input sequences of linestring arrays.\n", - "\n", - "The below example uses the polygons from `naturalearth_lowres` and treats them as linestrings. \n", - "The first example computes the distances between all polygons and themselves, while the second \n", - "example computes the distance between the first 50 polygons and the second 50 polygons.\n", - "\n", - "### [cuspatial.pairwise_linestring_distance](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.pairwise_linestring_distance)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "35dfb7c9-1914-488a-b22e-8d0067ea7a8b", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 0.0\n", - "1 0.0\n", - "2 0.0\n", - "3 0.0\n", - "4 0.0\n", - "dtype: float64\n", - "0 152.200610\n", - "1 44.076445\n", - "2 2.417269\n", - "3 44.197151\n", - "4 75.821029\n", - "dtype: float64\n" - ] - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "\n", - "gpu_boundaries = cuspatial.from_geopandas(host_dataframe.geometry.boundary)\n", - "zeros = cuspatial.pairwise_linestring_distance(\n", - " gpu_boundaries[0:50],\n", - " gpu_boundaries[0:50]\n", - ")\n", - "print(zeros.head())\n", - "lines1 = gpu_boundaries[0:50]\n", - "lines2 = gpu_boundaries[50:100]\n", - "distances = cuspatial.core.spatial.distance.pairwise_linestring_distance(\n", - " lines1, lines2\n", - ")\n", - "print(distances.head())" - ] - }, - { - "cell_type": "markdown", - "id": "de6b73ac-1b48-422c-8463-37367ad73507", - "metadata": {}, - "source": [ - "`pairwise_point_linestring_distance` computes the distance between pairs of points and \n", - "linestrings. It can be used with polygons treated as linestrings as well. In the following \n", - "example the minimum distance from a country's center to it's border is computed.\n", - "\n", - "### [cuspatial.pairwise_point_linestring_distance](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.pairwise_point_linestring_distance)\n", - "\n", - "Using WGS 84 Pseudo-Mercator, distances are in meters." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "40e9a41e-21af-47cc-a142-b19a67941f7f", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " pop_est continent name iso_a3 gdp_md_est \\\n", - "1 58005463.0 Africa Tanzania TZA 63177 \n", - "2 603253.0 Africa W. Sahara ESH 907 \n", - "5 18513930.0 Asia Kazakhstan KAZ 181665 \n", - "6 33580650.0 Asia Uzbekistan UZB 57921 \n", - "11 86790567.0 Africa Dem. Rep. Congo COD 50400 \n", - "\n", - " geometry border_distance \n", - "1 POLYGON ((3774143.866 -105758.362, 3792946.708... 8047.288391 \n", - "2 POLYGON ((-964649.018 3205725.605, -964597.245... 593137.492497 \n", - "5 POLYGON ((9724867.413 6311418.173, 9640131.701... 37091.213890 \n", - "6 POLYGON ((6230350.563 5057973.384, 6225978.591... 278633.467299 \n", - "11 POLYGON ((3266113.592 -501451.658, 3286149.877... 35812.988244 \n", - "(GPU)\n", - "\n" - ] - } - ], - "source": [ - "# Convert input dataframe to Pseudo-Mercator projection.\n", - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\")).to_crs(3857)\n", - "polygons = host_dataframe[host_dataframe['geometry'].type == \"Polygon\"]\n", - "gpu_polygons = cuspatial.from_geopandas(polygons)\n", - "# Extract mean_x and mean_y from each country\n", - "mean_x = [gpu_polygons['geometry'].iloc[[ix]].polygons.x.mean() for ix in range(len(gpu_polygons))]\n", - "mean_y = [gpu_polygons['geometry'].iloc[[ix]].polygons.y.mean() for ix in range(len(gpu_polygons))]\n", - "# Convert mean_x/mean_y values into Points for use in API.\n", - "points = cuspatial.GeoSeries([Point(point) for point in zip(mean_x, mean_y)])\n", - "# Convert Polygons into Linestrings for use in API.\n", - "linestring_df = cuspatial.from_geopandas(geopandas.geoseries.GeoSeries(\n", - " [MultiLineString(mapping(polygons['geometry'].iloc[ix])[\"coordinates\"]) for ix in range(len(polygons))]\n", - "))\n", - "gpu_polygons['border_distance'] = cuspatial.pairwise_point_linestring_distance(\n", - " points, linestring_df\n", - ")\n", - "print(gpu_polygons.head())" - ] - }, - { - "cell_type": "markdown", - "id": "f9724827-5cba-44c7-ae5a-47bf0b620ae2", - "metadata": {}, - "source": [ - "### cuspatial.pairwise_point_polygon_distance\n", - "\n", - "Using WGS 84 Pseudo-Mercator, distances are in meters." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "df5e6111-0c18-4452-b99d-b24e87523949", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
pop_estcontinentnameiso_a3gdp_md_estgeometrydistance_fromdistance
0889953.0OceaniaFijiFJI5496MULTIPOLYGON (((20037508.343 -1812498.413, 200...Vatican City1.969350e+07
158005463.0AfricaTanzaniaTZA63177POLYGON ((3774143.866 -105758.362, 3792946.708...San Marino5.929777e+06
2603253.0AfricaW. SaharaESH907POLYGON ((-964649.018 3205725.605, -964597.245...Vaduz3.421172e+06
337589262.0North AmericaCanadaCAN1736425MULTIPOLYGON (((-13674486.249 6274861.394, -13...Lobamba1.296059e+07
4328239523.0North AmericaUnited States of AmericaUSA21433226MULTIPOLYGON (((-13674486.249 6274861.394, -13...Luxembourg8.174897e+06
\n", - "
" - ], - "text/plain": [ - " pop_est continent name iso_a3 gdp_md_est \\\n", - "0 889953.0 Oceania Fiji FJI 5496 \n", - "1 58005463.0 Africa Tanzania TZA 63177 \n", - "2 603253.0 Africa W. Sahara ESH 907 \n", - "3 37589262.0 North America Canada CAN 1736425 \n", - "4 328239523.0 North America United States of America USA 21433226 \n", - "\n", - " geometry distance_from \\\n", - "0 MULTIPOLYGON (((20037508.343 -1812498.413, 200... Vatican City \n", - "1 POLYGON ((3774143.866 -105758.362, 3792946.708... San Marino \n", - "2 POLYGON ((-964649.018 3205725.605, -964597.245... Vaduz \n", - "3 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Lobamba \n", - "4 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Luxembourg \n", - "\n", - " distance \n", - "0 1.969350e+07 \n", - "1 5.929777e+06 \n", - "2 3.421172e+06 \n", - "3 1.296059e+07 \n", - "4 8.174897e+06 \n", - "(GPU)" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cities = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_cities\")).to_crs(3857)\n", - "countries = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\")).to_crs(3857)\n", - "\n", - "gpu_cities = cuspatial.from_geopandas(cities)\n", - "gpu_countries = cuspatial.from_geopandas(countries)\n", - "\n", - "dist = cuspatial.pairwise_point_polygon_distance(\n", - " gpu_cities.geometry[:len(gpu_countries)], gpu_countries.geometry\n", - ")\n", - "\n", - "gpu_countries[\"distance_from\"] = cities.name\n", - "gpu_countries[\"distance\"] = dist\n", - "\n", - "gpu_countries.head()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "6262aec6-753b-4c9c-938c-818b46899fc7", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "0 POINT (1386304.644 5146502.579)\n", - "1 POINT (1385011.523 5455558.181)\n", - "2 POINT (1059390.803 5963928.580)\n", - "3 POINT (3473167.790 -3056995.462)\n", - "4 POINT (682388.790 6379291.919)\n", - " ... \n", - "238 POINT (-4810350.913 -2620812.957)\n", - "239 POINT (-5190490.090 -2699486.457)\n", - "240 POINT (16832903.820 -4011543.664)\n", - "241 POINT (11560960.460 144168.711)\n", - "242 POINT (12710800.486 2548415.574)\n", - "Name: geometry, Length: 243, dtype: geometry" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cities.geometry" - ] - }, + "cells": [ + { + "cell_type": "markdown", + "id": "c5fdf490-fa77-4e56-92d1-53101fff75ba", + "metadata": {}, + "source": [ + "# cuSpatial Python User's Guide\n", + "\n", + "cuSpatial is a GPU-accelerated Python library for spatial data analysis including distance and \n", + "trajectory computations, spatial data indexing and spatial join operations. cuSpatial's \n", + "Python API provides an accessible interface to high-performance spatial algorithms accelerated\n", + "by CUDA-enabled GPUs." + ] + }, + { + "cell_type": "markdown", + "id": "caadf3ca-be3c-4523-877c-4c35dd25093a", + "metadata": {}, + "source": [ + "## Contents\n", + "\n", + "This guide provides a working example for all of the python API components of cuSpatial. \n", + "The following list links to each subsection.\n", + "\n", + "* [Installing cuSpatial](#Installing-cuspatial)\n", + "* [GPU accelerated memory layout](#GPU-accelerated-memory-layout)\n", + "* [Input / Output](#Input-/-Output)\n", + "* [Geopandas and cuDF integration](#Geopandas-and-cuDF-integration)\n", + "* [Trajectories](#Trajectories)\n", + "* [Bounding](#Bounding)\n", + "* [Projection](#Projection)\n", + "* [Distance](#Distance)\n", + "* [Filtering](#Filtering)\n", + "* [Spatial joins](#Spatial-joins)" + ] + }, + { + "cell_type": "markdown", + "id": "115c8382-f83f-476f-9a26-a64a45b3a8da", + "metadata": {}, + "source": [ + "## Installing cuSpatial\n", + "Read the [RAPIDS Quickstart Guide]( https://rapids.ai/start.html ) to learn more about installing all RAPIDS libraries, including cuSpatial.\n", + "\n", + "If you are working on a system with a CUDA-enabled GPU and have CUDA installed, uncomment the \n", + "following cell and install cuSpatial:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7265f9d2-9203-4da2-bbb2-b35c7f933641", + "metadata": {}, + "outputs": [], + "source": [ + "# !conda create -n rapids-22.08 -c rapidsai -c conda-forge -c nvidia \\ \n", + "# cuspatial=22.08 python=3.9 cudatoolkit=11.5 " + ] + }, + { + "cell_type": "markdown", + "id": "051b6e68-9ffd-473a-89e2-313fe1c59d18", + "metadata": {}, + "source": [ + "For other options to create a RAPIDS environment, such as docker or build from source, see \n", + "[RAPIDS Release Selector]( https://rapids.ai/start.html#get-rapids). \n", + "\n", + "If you wish to contribute to cuSpatial, you should create a source build using the excellent [rapids-compose](https://github.com/trxcllnt/rapids-compose)" + ] + }, + { + "cell_type": "markdown", + "id": "7b770cb4-793e-467a-a306-2d3409545748", + "metadata": {}, + "source": [ + "## GPU accelerated memory layout\n", + "\n", + "cuSpatial uses `GeoArrow` buffers, a GPU-friendly data format for geometric data that is well \n", + "suited for massively parallel programming. See [I/O](#io) on the fastest methods to get your \n", + "data into cuSpatial. GeoArrow extends [PyArrow](\n", + "https://arrow.apache.org/docs/python/index.html ) bindings and introduces several new types suited \n", + "for geometry applications. GeoArrow supports [ListArrays](\n", + "https://arrow.apache.org/docs/python/data.html#arrays) for `Points`, `MultiPoints`, \n", + "`LineStrings`, `MultiLineStrings`, `Polygons`, and `MultiPolygons`. Using an Arrow [DenseArray](\n", + "https://arrow.apache.org/docs/python/data.html#union-arrays), \n", + "GeoArrow stores heterogeneous types of Features. DataFrames of geometry objects and their \n", + "metadata can be loaded and transformed in a method similar to those in [GeoPandas.GeoSeries](\n", + "https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "88d05bb9-c924-4d0b-8736-cd5183602d76", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Imports used throughout this notebook.\n", + "import cuspatial\n", + "import cudf\n", + "import cupy\n", + "import geopandas\n", + "import numpy as np\n", + "from shapely.geometry import *" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4937d4aa-4e32-49ab-a22e-96dfb0098d07", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# For deterministic result\n", + "np.random.seed(0)\n", + "cupy.random.seed(0)" + ] + }, + { + "cell_type": "markdown", + "id": "4b1251d1-558a-4899-8e7a-8066db0ad091", + "metadata": {}, + "source": [ + "## Input / Output\n", + "\n", + "The primary method of loading features into cuSpatial is using [cuspatial.from_geopandas](\n", + "https://docs.rapids.ai/api/cuspatial/stable/api_docs/io.html?highlight=from_geopandas#cuspatial.from_geopandas).\n", + "\n", + "One can also create feature geometries directly using any Python buffer that supports \n", + "`__array_interface__` for coordinates and their feature offsets." + ] + }, + { + "cell_type": "markdown", + "id": "11b973bd-87e1-4b67-ab8c-23c3b8291335", + "metadata": {}, + "source": [ + "### [cuspatial.from_geopandas](https://docs.rapids.ai/api/cuspatial/stable/api_docs/io.html?highlight=from_geopandas#cuspatial.from_geopandas)\n", + "\n", + "The easiest way to get data into cuSpatial is via `cuspatial.from_geopandas`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "255fbfbe-8be1-498c-9a26-f4a3f31bdded", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 19, - "id": "46ce6936-7bb6-4daf-8f98-4ad79c963022", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "0 MULTIPOLYGON (((20037508.343 -1812498.413, 200...\n", - "1 POLYGON ((3774143.866 -105758.362, 3792946.708...\n", - "2 POLYGON ((-964649.018 3205725.605, -964597.245...\n", - "3 MULTIPOLYGON (((-13674486.249 6274861.394, -13...\n", - "4 MULTIPOLYGON (((-13674486.249 6274861.394, -13...\n", - " ... \n", - "172 POLYGON ((2096126.508 5765757.958, 2096127.988...\n", - "173 POLYGON ((2234260.104 5249565.284, 2204305.520...\n", - "174 POLYGON ((2292095.761 5139344.949, 2284604.344...\n", - "175 POLYGON ((-6866186.192 1204901.071, -6802177.4...\n", - "176 POLYGON ((3432408.751 390883.649, 3334408.389 ...\n", - "Name: geometry, Length: 177, dtype: geometry" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "countries.geometry" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " pop_est continent name iso_a3 gdp_md_est \\\n", + "0 889953.0 Oceania Fiji FJI 5496 \n", + "1 58005463.0 Africa Tanzania TZA 63177 \n", + "2 603253.0 Africa W. Sahara ESH 907 \n", + "3 37589262.0 North America Canada CAN 1736425 \n", + "4 328239523.0 North America United States of America USA 21433226 \n", + "\n", + " geometry \n", + "0 MULTIPOLYGON (((180.00000 -16.06713, 180.00000... \n", + "1 POLYGON ((33.90371 -0.95000, 34.07262 -1.05982... \n", + "2 POLYGON ((-8.66559 27.65643, -8.66512 27.58948... \n", + "3 MULTIPOLYGON (((-122.84000 49.00000, -122.9742... \n", + "4 MULTIPOLYGON (((-122.84000 49.00000, -120.0000... \n", + "(GPU)\n", + "\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\n", + " \"naturalearth_lowres\"\n", + "))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "print(gpu_dataframe.head())" + ] + }, + { + "cell_type": "markdown", + "id": "da5c775b-7458-4e3c-a573-e1bd060e3365", + "metadata": {}, + "source": [ + "## Geopandas and cuDF integration\n", + "\n", + "A cuSpatial [GeoDataFrame](\n", + "https://docs.rapids.ai/api/cuspatial/stable/api_docs/geopandas_compatibility.html#cuspatial.GeoDataFrame ) is a collection of [cudf](\n", + "https://docs.rapids.ai/api/cudf/stable/ ) [Series](\n", + "https://docs.rapids.ai/api/cudf/stable/api_docs/series.html ) and\n", + "[cuspatial.GeoSeries](\n", + "https://docs.rapids.ai/api/cuspatial/stable/api_docs/geopandas_compatibility.html#cuspatial.GeoSeries ) `\"geometry\"` objects. \n", + "Both types of series are stored on the GPU, and\n", + "`GeoSeries` is represented internally using `GeoArrow` data layout.\n", + "\n", + "One of the most important features of cuSpatial is that it is highly integrated with `cuDF`. \n", + "You can use any `cuDF` operation on cuSpatial non-feature columns, and most operations will work \n", + "with a `geometry` column. Operations that reduce or collate the number of rows in your DataFrame, \n", + "for example `groupby`, are not supported at this time." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "956451e2-a520-441d-a939-575ed179917b", + "metadata": { + "tags": [] + }, + "outputs": [ { - "attachments": { - "351aea0c-f37e-4ab9-bad2-c67bce69b5c3.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "f5f27dc3-46ee-4a62-82de-20f76744382f", - "metadata": {}, - "source": [ - "## Filtering\n", - "\n", - "The filtering module contains `points_in_spatial_window`, which returns from a set of points only those points that fall within a spatial window defined by four bounding coordinates: `min_x`, `max_x`, `min_y`, and `max_y`. The following example finds only the points of polygons that fall within 1 standard deviation of the mean of all of the polygons.\n", - "\n", - "\n", - "\n", - "### [cuspatial.points_in_spatial_window](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.points_in_spatial_window)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " pop_est continent name iso_a3 gdp_md_est \\\n", + "103 38041754.0 Asia Afghanistan AFG 19291 \n", + "125 2854191.0 Europe Albania ALB 15279 \n", + "82 43053054.0 Africa Algeria DZA 171091 \n", + "74 31825295.0 Africa Angola AGO 88815 \n", + "159 4490.0 Antarctica Antarctica ATA 898 \n", + ".. ... ... ... ... ... \n", + "2 603253.0 Africa W. Sahara ESH 907 \n", + "157 29161922.0 Asia Yemen YEM 22581 \n", + "70 17861030.0 Africa Zambia ZMB 23309 \n", + "48 14645468.0 Africa Zimbabwe ZWE 21440 \n", + "73 1148130.0 Africa eSwatini SWZ 4471 \n", + "\n", + " geometry \n", + "103 POLYGON ((66.51861 37.36278, 67.07578 37.35614... \n", + "125 POLYGON ((21.02004 40.84273, 20.99999 40.58000... \n", + "82 POLYGON ((-8.68440 27.39574, -8.66512 27.58948... \n", + "74 MULTIPOLYGON (((12.99552 -4.78110, 12.63161 -4... \n", + "159 MULTIPOLYGON (((-48.66062 -78.04702, -48.15140... \n", + ".. ... \n", + "2 POLYGON ((-8.66559 27.65643, -8.66512 27.58948... \n", + "157 POLYGON ((52.00001 19.00000, 52.78218 17.34974... \n", + "70 POLYGON ((30.74001 -8.34001, 31.15775 -8.59458... \n", + "48 POLYGON ((31.19141 -22.25151, 30.65987 -22.151... \n", + "73 POLYGON ((32.07167 -26.73382, 31.86806 -27.177... \n", + "\n", + "[177 rows x 6 columns]\n", + "(GPU)\n", + "\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "continents_dataframe = gpu_dataframe.sort_values(\"name\")\n", + "print(continents_dataframe)" + ] + }, + { + "cell_type": "markdown", + "id": "5453f308-317f-4775-ba3c-ff0633755bc4", + "metadata": {}, + "source": [ + "You can also convert between GPU-backed `cuspatial.GeoDataFrame` and CPU-backed \n", + "`geopandas.GeoDataFrame` with `from_geopandas` and `to_geopandas`, enabling you to \n", + "take advantage of any native GeoPandas operation. Note, however, that GeoPandas runs on \n", + " the CPU and therefore will not have as high performance as cuSpatial operations. The following \n", + "example displays the `Polygon` associated with the first item in the dataframe sorted \n", + "alphabetically by name." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cd8d1c39-b44f-4d06-9e11-04c2dca4cf15", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 20, - "id": "d1ade9da-c9e2-45c4-9685-dffeda3fd358", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 POINT (33.90371 -0.95000)\n", - "1 POINT (34.07262 -1.05982)\n", - "2 POINT (37.69869 -3.09699)\n", - "3 POINT (37.76690 -3.67712)\n", - "4 POINT (39.20222 -4.67677)\n", - "dtype: geometry\n" - ] - } + "data": { + "image/svg+xml": [ + "" ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", - "geometry = gpu_dataframe['geometry']\n", - "points = cuspatial.GeoSeries.from_points_xy(geometry.polygons.xy)\n", - "mean_x, std_x = (geometry.polygons.x.mean(), geometry.polygons.x.std())\n", - "mean_y, std_y = (geometry.polygons.y.mean(), geometry.polygons.y.std())\n", - "avg_points = cuspatial.points_in_spatial_window(\n", - " points,\n", - " mean_x - std_x,\n", - " mean_x + std_x,\n", - " mean_y - std_y,\n", - " mean_y + std_y\n", - ")\n", - "print(avg_points.head())" + "text/plain": [ + "" ] - }, + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "sorted_dataframe = gpu_dataframe.sort_values(\"name\")\n", + "host_dataframe = sorted_dataframe.to_geopandas()\n", + "host_dataframe['geometry'].iloc[0]" + ] + }, + { + "attachments": { + "046b885c-ab14-4c44-bd23-daebad76ebae.png": { + "image/png": "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CICA6wlQfpYkSeHh4W+88cb7779fVVV1//33f/bZZ6IoqqqKSUquhz4tJUKApwUzKgEBEACBOyDA87xWq1Xsx/Lly0tLS2fPnv3www+//fbbgiDQQtN3UDwedQ8BCLB7uKNWEAABELhdAjQqLMtybGzsO++88+KLLz7//PMvvPCCLMu0yvTtFoj73UtA497qUTsIgAAIgMDECVCONCnus88+m52dvXnz5ra2trfffpuWrhRFceKl4U73EkAE7F7+qB0EQAAEbpsAqawkSatWrfr444//+Mc/bt682Wq18jyP8eDbpum+ByDA7mOPmkEABEBgsgRof0OLxTJv3rxDhw5VVFQ89dRTqqpCgydL1A3PQYDdAB1VggAIgMCdE+B5XqfTWa3W2bNnHz58+MiRI9u3bycBxqrRd453GkqAAE8DZFQBAiAAAlNCwFGD//Vf//XnP//5vn37aIelKakPhbqUAATYpThRGAiAAAhMOwGtVitJ0je/+c2nn376hz/8YVNTE22vNO2GoMLbIwABvj1euBsEQAAEPI0ApUYrivLjH/84Ojp6165diqLwPI+OaE/zlJM9EGAnIPgIAiAAAt5HQBAERVGMRuO2bds++uijixcv0q5K3tcSf7IYAuxP3kZbQQAEfJcALYm1YsWKzMzMTz75hOM4nud9t7m+0DIIsC94EW0AARAAAcp/njFjRk5OTlVV1cjICK0UDTIeSwAC7LGugWEgAAIgcBsESIB5nk9KSurt7bVYLLfxMG51BwEIsDuoo04QAAEQmAIClHUVExNz69at4eFhjuOQhzUFmF1WJATYZShREAiAAAi4lwAN+g4MDAQGBur1egwDu9cdf7N2CPDfRIQbQAAEQMALCKiqSrnQjY2NcXFxwcHBCH893G0QYA93EMwDARAAgQkRUBRFEIT29vYTJ04UFhbqdDpszDAhcO67CQLsPvaoGQRAAARcR8Bms/E8/9577ymKsnHjRioYM5FcB9j1JWE/YNczRYkgAAIgMM0ELBZLQEDAyZMnd+7cuWvXruzsbEmSNBp8w0+zH26vOkTAt8cLd4MACICARxFQVXVkZCQgIKCuru673/3uhg0bNm3apCiKRqNB+OtRnhptDAR4NBNcAQEQAAHvICDLsiRJer3+1KlTGzZsmDdv3s9+9jNafwPq6/kuhAB7vo9gIQiAAAg4E1BV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" + } + }, + "cell_type": "markdown", + "id": "56b80722-38b6-457c-a7f0-591af6efd3ff", + "metadata": {}, + "source": [ + "## Trajectories\n", + "\n", + "A trajectory is a `LineString` coupled with a time sample for each point in the `LineString`. \n", + "Use `cuspatial.trajectory.derive_trajectories` to group trajectory datasets and sort by time.\n", + "\n", + "\n", + "\n", + "### [cuspatial.derive_trajectories](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.derive_trajectories)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cb5acdad-53aa-418f-9948-8445515bd2b2", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "5027a3dd-78bb-4d17-af94-506d0ed689c8", - "metadata": {}, - "source": [ - "With some careful grouping, one can reconstruct the original complete polygons that fall within the range." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " object_id x y timestamp\n", + "0 1 0.680146 0.874341 1970-01-01 00:00:00.125\n", + "1 1 0.843522 0.044402 1970-01-01 00:00:00.834\n", + "2 1 0.837039 0.351025 1970-01-01 00:00:01.335\n", + "3 1 0.946184 0.479038 1970-01-01 00:00:01.791\n", + "4 1 0.117322 0.182117 1970-01-01 00:00:02.474\n", + "0 0\n", + "1 2455\n", + "2 4899\n", + "3 7422\n", + "4 9924\n", + " ... \n", + "394 987408\n", + "395 989891\n", + "396 992428\n", + "397 994975\n", + "398 997448\n", + "Length: 399, dtype: int32\n" + ] + } + ], + "source": [ + "# 1m random trajectory samples\n", + "ids = cupy.random.randint(1, 400, 1000000)\n", + "timestamps = cupy.random.random(1000000)*1000000\n", + "xy= cupy.random.random(2000000)\n", + "trajs = cuspatial.GeoSeries.from_points_xy(xy)\n", + "sorted_trajectories, trajectory_offsets = \\\n", + " cuspatial.core.trajectory.derive_trajectories(ids, trajs, timestamps)\n", + "# sorted_trajectories is a DataFrame containing all trajectory samples\n", + "# sorted first by `object_id` and then by `timestamp`.\n", + "print(sorted_trajectories.head())\n", + "# trajectory_offsets is a Series containing the start position of each\n", + "# trajectory in sorted_trajectories.\n", + "print(trajectory_offsets)" + ] + }, + { + "cell_type": "markdown", + "id": "3c4a90f9-8661-4fda-9026-473e6ce87bd2", + "metadata": {}, + "source": [ + "`derive_trajectories` sorts the trajectories by `object_id`, then `timestamp`, and returns a \n", + "tuple containing the sorted trajectory data frame in the first index position and the offsets \n", + "buffer defining the start and stop of each trajectory in the second index position. \n", + "\n", + "### [cuspatial.trajectory_distances_and_speeds](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.trajectory_distances_and_speeds)\n", + "\n", + "Use `trajectory_distance_and_speed` to calculate the overall distance travelled in meters and \n", + "the speed of a set of trajectories with the same format as the result returned by `derive_trajectories`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "03b75847-090d-40f8-8147-cb10b900d6ec", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "3b33ce2b-965f-42a1-a89e-66d7ca80d907", - "metadata": {}, - "source": [ - "## Set Operations" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " distance speed\n", + "trajectory_id \n", + "0 1.278996e+06 1280.320089\n", + "1 1.267179e+06 1268.370390\n", + "2 1.294437e+06 1295.905261\n", + "3 1.323413e+06 1323.956714\n", + "4 1.309590e+06 1311.561012\n" + ] + } + ], + "source": [ + "trajs = cuspatial.GeoSeries.from_points_xy(\n", + " sorted_trajectories[[\"x\", \"y\"]].interleave_columns()\n", + ")\n", + "d_and_s = cuspatial.core.trajectory.trajectory_distances_and_speeds(\n", + " len(cudf.Series(ids).unique()),\n", + " sorted_trajectories['object_id'],\n", + " trajs,\n", + " sorted_trajectories['timestamp']\n", + ")\n", + "print(d_and_s.head())" + ] + }, + { + "cell_type": "markdown", + "id": "eeb0da7c-0bdf-49ec-8244-4165acc96074", + "metadata": {}, + "source": [ + "Finally, compute the bounding boxes of trajectories that follow the format of the above two \n", + "examples:" + ] + }, + { + "attachments": { + "8c5d8b90-2241-45c2-b98c-20d48d1ee6b7.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlgAAAHCCAYAAAAzc7dkAAAgAElEQVR4AeydB3xUxfbHf7PpIZSEANJ7EQFRVEwBNoAoKrYnVuw+u2IF6wN774oNsVf8+95TnwUFFkjAhh1FlCJdkN4CSXb+n3M3d/duDJCyd/fu3t98Pjr3zs6dOfOdS/bszJlzACYSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAES2AsBtZfP+XFsCTQEcASAvgCSYisKeycBEiABEnAJgfkAPgaw2iXjtWWYVLBswRqRRkW5+gzAIRFpjY2QAAmQAAmQQM0JrAAwEMCimj/CmlYCHusNrx1F4DgqVzGYj/QMYN9eQEpKDDpnlyRAAiTgGAKtAVzsGGniUBAqWM6dtF7OFS1BJVMK6NoDyMkF9u8HNMhK0IFyWCRAAiRQIwK9a1SLlaolQAWrWiyOKOT2bbSnoXVboFHjQK9p6UCfA4Dc5tGWgv2RAAmQgFMIUEeox0wk1+NZPhp9An4Am6LfrQt6zGqYhHYdGoWN1JMEdO8JZGWVYsniHYAO+5g3JEACJJBABGTJnrYREZxQKlgRhBmFpv4E0CoK/biri+HD07Blx1fQMJbD+/TcF5u3bMGSZcsDHFq3S0frtlOA8lEoKdniLjgcLQmQgEsIfAhguEvGGpVhcvkvKpjZiaMJbNpxr6lcNWrYEI/ceRvenvQshgwcYBFbHQOkfIMBQ3paCnlJAiRAAiRAAtUSoIJVLRYWuoZAofcIKFxhjveGKy9D82a5yMzIwCN33oqrLr4AHk/QHK4L/BWfI7/oWLM+cxIgARIgARKojgAVrOqosMwdBAoLs6ExEYChQR0+2IsRhw8Ljl0phfNOPxUP3DoOGenpZnlDKP1vFHrHm8+ZHzAnARIgARIgAZMAFSyTBHP3EdDJjwEQXy/IzcnBzddcWS2DYUWD8NozT6J1y5bm5woa41DgfQP9RmSahcxJgARIgARIwCRABcskwdxdBAqKTgIwSgYtK1V33nQ9shtXumiohkS3zp3w1sSn0b/fAdZPT0b6lhLkD25vLeQ1CZAACZAACVDB4jvgPgIDBrQE9ARz4P8YcRQK+h9s3u42b9K4EZ596H5j29BSqS+U/2sMKBpkKeMlCZAACZCAywlQ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552jJgESIAESIAESsJEAFSwb4bJpEiABEiABEiABdxKgguXOeeeoSYAESIAESIAEbCRABctGuGyaBEiABEiABEjAnQSoYLlz3jlqEiABEiABEiABGwlQwbIRLpsmARIgARIgARJwJwEqWO6cd46aBEiABEiABEjARgJUsGyEy6ZJgARIgARIgATcSYAKljvnnaMmARIgARIgARKwkQAVLBvhsmkSIAESIAESIAF3EqCC5c5556hJgARIgARIgARsJEAFy0a4bJoESIAESIAESMCdBKhguXPeOWoSIAESIAESIAEbCVDBshEumyYBEiABEiABEnAnASpY7px3jpoESIAESIAESMBGAlSwbITLpkmABEiABEiABNxJgAqWO+edoyYBEiABEiABErCRABUsG+GyaRIgARIgARIgAXcSoILlznnnqEmABEiABEiABGwkQAXLRrhsmgRIgARIgARIwJ0EqGC5c945ahIgARIgARIgARsJUMGyES6bJgESIAESIAEScCcBKljunHeOmgRIgARIgARIwEYCVLBshMumSYAESIAESIAE3EmACpY7552jJgESIAESIAESsJEAFSwb4bJpEiABEiABEiABdxKgguXOeeeoSYAESIAESIAEbCRABctGuGyaBEiABEiABEjAnQSoYLlz3jlqEiABEiABEiABGwlQwbIRLpsmARIgARIgARJwJwEqWO6cd46aBEiABEiABEjARgJUsGyEy6ZJgARIgARIgATcSYAKljvnnaMmARIgARIgARKwkQAVLBvhsmkSIAESIAESIAF3EqCC5c5556hJgARIgARIgARsJEAFy0a4bJoESIAESIAESMCdBKhguXPeOWoSIAESIAESIAEbCVDBshEumyYBEiABEiABEnAnASpY7px3jpoESIAESIAESMBGAlSwbITLpkmABEiABEiABNxJgAqWO+edoyYBEiABEiABErCRABUsG+GyaRIgARIgARIgAXcSoILlznnnqEmABEiABEiABGwkQAXLRrhsmgRIgARIgARIwJ0Ekt05bI46igQUCgubQKc3hEdnoaK8ATwePzS2wq+2YpdnK+Z+timK8rArEiABEiABErCdABUs2xG7pIOCoe3gr9gXHt0bwH4AegHYF0ADaGFQDvgBKAVoowDwaCDdDxR4pcIGAD8BmAdl5hXzMGvWWpcQ5DBJ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" + } + }, + "cell_type": "markdown", + "id": "12a3ce7e-82b7-4b51-8227-5e157a48701c", + "metadata": { + "tags": [] + }, + "source": [ + "## Bounding\n", + "\n", + "Compute the bounding boxes of `n` polygons or linestrings:\n", + "\n", + "\n", + "\n", + "### [cuspatial.trajectory_bounding_boxes](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.trajectory_bounding_boxes)\n", + "\n", + "`trajectory_bounding_boxes` works out of the box with the values returned by `derive_trajectories`. \n", + "Its arguments are the number of incoming objects, the offsets of those objects, and x and y point buffers." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "452f60cb-28cc-4ad8-8aa2-9d73e3d56ec6", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "d73548f3-c9bb-43ff-9788-858f3b7d08e4", - "metadata": {}, - "source": [ - "### Linestring Intersections\n", - "\n", - "cuSpatial provides a linestring-linestring intersection algorithm to compute the overlapping geometries between two linestrings.\n", - "The API also returns the ids for each returned geometry to help user to trace back the source geometry." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " x_min y_min x_max y_max\n", + "0 0.000098 0.000243 0.999422 0.999068\n", + "1 0.000663 0.000414 0.999813 0.998456\n", + "2 0.000049 0.000220 0.999748 0.999220\n", + "3 0.000006 0.000303 0.999729 0.999762\n", + "4 0.001190 0.000074 0.999299 0.999858\n" + ] + } + ], + "source": [ + "bounding_boxes = cuspatial.core.trajectory.trajectory_bounding_boxes(\n", + " len(cudf.Series(ids, dtype=\"int32\").unique()),\n", + " sorted_trajectories['object_id'],\n", + " trajs\n", + ")\n", + "print(bounding_boxes.head())" + ] + }, + { + "cell_type": "markdown", + "id": "a56dfe17-1739-4b20-85c9-fcb5902c1585", + "metadata": {}, + "source": [ + "### [cuspatial.polygon_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.polygon_bounding_boxes)\n", + "\n", + "`polygon_bounding_boxes` supports more complex geometry objects such as `Polygon`s with multiple \n", + "rings. The combination of `part_offset` and `ring_offset` allows the function to use only the \n", + "exterior ring for computing the bounding box." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9266aac4-f925-4fb7-b287-5f0b795d5756", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 18, - "id": "cc72a44d-a9bf-4432-9898-de899ac45869", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from cuspatial.core.binops.intersection import pairwise_linestring_intersection\n", - "\n", - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "usa_boundary = cuspatial.from_geopandas(host_dataframe[host_dataframe.name == \"United States of America\"].geometry.boundary)\n", - "canada_boundary = cuspatial.from_geopandas(host_dataframe[host_dataframe.name == \"Canada\"].geometry.boundary)\n", - "\n", - "list_offsets, geometries, look_back_ids = pairwise_linestring_intersection(usa_boundary, canada_boundary)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " minx miny maxx maxy\n", + "0 29.339998 -11.720938 40.316590 -0.950000\n", + "1 -17.063423 20.999752 -8.665124 27.656426\n", + "2 46.466446 40.662325 87.359970 55.385250\n", + "3 55.928917 37.144994 73.055417 45.586804\n", + "4 12.182337 -13.257227 31.174149 5.256088\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "single_polygons = cuspatial.from_geopandas(\n", + " host_dataframe['geometry'][host_dataframe['geometry'].type == \"Polygon\"]\n", + ")\n", + "bounding_box_polygons = cuspatial.core.spatial.bounding.polygon_bounding_boxes(\n", + " single_polygons\n", + ")\n", + "print(bounding_box_polygons.head())" + ] + }, + { + "cell_type": "markdown", + "id": "85197478-801c-4d2d-8b10-c1136d7bb15c", + "metadata": {}, + "source": [ + "### [cuspatial.linestring_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.linestring_bounding_boxes)\n", + "\n", + "Equivalently, we can treat trajectories as Linestrings and compute the same bounding boxes from \n", + "the above trajectory calculation more generally:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "15b5bb38-702f-4360-b48c-2e49ffd650d7", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 19, - "id": "1125fd17-afe1-4b9c-b48d-8842dd3700b3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "\n", - "[\n", - " 0,\n", - " 144\n", - "]\n", - "dtype: int32" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The first integer series shows that the result contains 1 row (since we only have 1 pair of linestrings as input).\n", - "# This row contains 144 geometires.\n", - "list_offsets" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " minx miny maxx maxy\n", + "0 -0.000002 0.000143 0.999522 0.999168\n", + "1 0.000563 0.000314 0.999913 0.998556\n", + "2 -0.000051 0.000120 0.999848 0.999320\n", + "3 -0.000094 0.000203 0.999829 0.999862\n", + "4 0.001090 -0.000026 0.999399 0.999958\n" + ] + } + ], + "source": [ + "lines = cuspatial.GeoSeries.from_linestrings_xy(\n", + " trajs.points.xy, trajectory_offsets, cupy.arange(len(trajectory_offsets))\n", + ")\n", + "trajectory_bounding_boxes = cuspatial.core.spatial.bounding.linestring_bounding_boxes(\n", + " lines, 0.0001\n", + ")\n", + "print(trajectory_bounding_boxes.head())" + ] + }, + { + "cell_type": "markdown", + "id": "81c4d3ca-5d3f-4ae1-ae8e-ac1e252f3e17", + "metadata": {}, + "source": [ + "## Projection\n", + "\n", + "cuSpatial provides a simple sinusoidal longitude / latitude to Cartesian coordinate transform. \n", + "This function requires an origin point to determine the scaling parameters for the lonlat inputs. \n", + "\n", + "### [cuspatial.sinusoidal_projection](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.sinusoidal_projection)\n", + "\n", + "The following cell converts the lonlat coordinates of the country of Afghanistan to Cartesian \n", + "coordinates in km, centered around the center of the country, suitable for graphing and display." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a7a870dd-c0ae-41c1-a66c-cff4bd2db0ec", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 20, - "id": "b281e3bb-42d4-4d60-9cb2-b7dcc20b4776", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "0 POINT (-130.53611 54.80275)\n", - "1 POINT (-130.53611 54.80278)\n", - "2 POINT (-130.53611 54.80275)\n", - "3 POINT (-129.98000 55.28500)\n", - "4 POINT (-130.53611 54.80278)\n", - " ... \n", - "139 LINESTRING (-113.00000 49.00000, -113.00000 49...\n", - "140 LINESTRING (-83.89077 46.11693, -83.61613 46.1...\n", - "141 LINESTRING (-116.04818 49.00000, -116.04818 49...\n", - "142 LINESTRING (-120.00000 49.00000, -117.03121 49...\n", - "143 LINESTRING (-122.84000 49.00000, -120.00000 49...\n", - "Length: 144, dtype: geometry" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The second element is a geoseries that contains the intersecting geometries, with 144 rows, including points and linestrings.\n", - "geometries" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "0 POINT (112.174 -281.590)\n", + "1 POINT (62.152 -280.852)\n", + "2 POINT (-5.573 -257.391)\n", + "3 POINT (-33.071 -243.849)\n", + "4 POINT (-98.002 -279.540)\n", + "dtype: geometry\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "afghanistan = gpu_dataframe['geometry'][gpu_dataframe['name'] == 'Afghanistan']\n", + "points = cuspatial.GeoSeries.from_points_xy(afghanistan.polygons.xy)\n", + "projected = cuspatial.sinusoidal_projection(\n", + " afghanistan.polygons.x.mean(),\n", + " afghanistan.polygons.y.mean(),\n", + " points\n", + ")\n", + "print(projected.head())" + ] + }, + { + "attachments": { + "image.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAF3CAYAAABewAv+AAAgAElEQVR4AexdB5gcxdF9vXeSQAKEBIicMyIaAQKETicRBYhgEDkbbJONCSZJIxFtEQwYk4PBZGPzm5x0dxI55yQwGQQiCxTvtv7v9Xbv9czt3m2avd29ru+TZnpmOsybvZqe6qpXgBePgEfAI+AR8Ah4BDwCHgGPgEfAI+AR8Ah4BDwCHgGPgEfAI+AR8Ah4BDwCHgGPgEfAI+AR8Ah4BDwCHgGPgEfAI+AR8Ah4BHoIAiKytYjcKiJ7Vuoti8hKInKtiFwtIotW6jj9uDwCHgGPQBoBEVlZRA4QkT+KyB9EZI30ySJ2RGQzETlcRE4Rkb1FpLfbnIjUi8jfRCQp7TLavaZS9kXkX+1DlA9FZK1KGZsfh0fAI1ClCIhIL6N8jxaRDUt1GyKiROQcEZnvKC67e6eI9CmkLxHZWEResg052+dFZEG2KSJ1InIbz7333ntyyy232MsmFNJn3HVE5LlffvlFzjvvPGltbeVYp3sFHzfqvn2PQA0jYBTwfVbzmVnuxaW4ZRE5je22tbXJ7bffLn/605/k7LPPlk8++cR2d2K+/YhIXxGZkUwm5bnnnpN77rlH/v73v8sjjzxi29yBbYrIBTxAxT506FD58ccfWZwhIsvm22c5rheRiznAf/zjH3LggQcK709E3hWR/uXo3/fhEfAI1BgCZsYu33zzjYwbN05eeOEFKhXKwcXcqogsLyJzf/75Zxk2bJjstttu8u2338qyyy4rm266qelC7s+3DxFZgu+LTz/9VPbbbz9Zeumlhbr8b3+j9UXLjiIyjNew73XXXVeeffZZe26nfPsr1/V86YjIDxzonnvuqWfwZtA3lGsMvh+PgEeghhAQkRuoRI477jitJDfccEOrCB8o5jZF5Fw2dPLJJ0t9fb3MmDFDZs6cKYMGDZKGhgbbx2WF9CEil9oGDj/8cD3uV1991R5Km2yOP/54+f3vf2+P31tIX+WsIyKHcbBffPGFDBw4UF5//XUWOYXfopzj8H15BDwCNYCAiGxHDTJ16lRtvrjuuuusMrylmNsTkTfZ0ODBg3W7ttFZs2bJ/PnaBE87yWqF9GHs6d+zzU022UQGDBigTT8iMktExvA4TT8LL7ywfP755yxSQa5bSF/lriMi93PAJ554ouy8887cpTxc7nH4/jwCHoEiETAeHYsV2UxR1UXk/4wSsRtqxNULbVREFqFZhI0tv/zy2sxgGzbb/9F0UkT7y4jIj1yA7NWrl+y00062+YdE5B4WOGOnecPIo4X2Ve56IsJ7m/XGG2+IUkrefFO/I3kbIe+Z+nIPzPfnEejpCHCBEsBIAHsB2AjAQABJAF8D+B7AdwBmA+B1XNzbAADtyFcCOBfAsQD2BbAwgA8APA2gBcB00+76ANYB0BfAfAA/RzD/AsB9AP6hlJobOdehKCL7AYj6VT/EvkWEi5NHAKBZYAaAdwG0AVjI1KHrIft/BsClSin2TTkIQII7v/zyCxZZZJHUUYAmlDcAbK2UIh45iXnR7AaAs++1AQwGsOAzzzyD+fPnY6uttrLt0JR0/rx583D77bfjiiuusMevtjt2KyK8h98DGA6AXju8v2/Ms+Ez2wYAFzPfAjBOKTXZ1s1ly5c2gOPNs2Q7nxiM/mPr8yUIYHfT/21KqZ+IoYjcPnjw4EM22mgjXH/99bjgggtYZW8Aga3rtx4Bj0AZEaBng4g8bqeL3bzl7HjTzm6fE9wsY6QZY2aWc9kOc7bPWSd92dPSr18/oV08Iv/tbFzuORHZRkR+idTXxTPOOEPb25966imW54rIgdx54IEH9KyXC7jmOBV5WkRkkIi8phvJ7T/akfjCzknojikikzM0TVzHshER+ZWIfOxc87SI6BeiiBzL4yeccIKstdZa9hK+5L14BDwC3YGAiNzBv8S33npLu7Odcsop2nZK17YjjjgiZHvmHy5NFn379pUll1zS/gHr7dZbb629ShZccEHZfffdQ+e4GMlFSdqZr7nmGnnsscdku+2206aJLbbYQrbddlt55plnbB3aoDnTzygichkvpJJcYYUV9FjYhiunn366HgsXRffZZx+ZPXu2XiQ96KCDZOzYsbLyyivLRRddZKvQx1Eb1A8++GDhfSQSCV2f+3vssYfce++9vJYmm+jXQocxigj97rV5h/W22WYbOfroo2XXXXeVxx9/XLbccks95rlzqde1aWkcd2iSWWONNeyYmtyGRWQBEdEuQe+8847Gly8f2u6vuuoqXWfSpEnyu9/9Tt8f79n4nb/sttPZvohczobo0cN73mWXXbSJyKw1cOV3gIh8xmvOPPNMvd5hBqtfxnyRsEz3Ub4HvvrqKxbnWR/+zvr25zwCHoESI2Bd9L777jtZccUVraeD+ZsVuf/++7Wio883hduWlhY9w4wqd7rwXXrppfoPO6rcf/rpJzn11FP1OSok+o4bpaHbvf7667V3Cn2/jTDYhyagDsKAJc5s6VP9yiuvCF8mUeXOth988EHdH18ihx12mKuMtN86FdC///1v219ols1FzR122EGIi71381XQr8OAnAMi0mgV+x//+EdZZZVV5KOPPtJ9cExU9LS3NzbyMi2HiAgDo/QxKlUjFzrNcsZ8HI9zwXX77beX6dMZKyRCbxvauPfff3+55JJL9DG+JHhv5suAs+7QF4Dbrt0XkaX5gpszZ47svffe8v3338trr72m2+GL2Czuav//zz77TPdpXyo2ctco/+S7777r1mPdkgWY2fH6rUfAI9AFAiIyin99//d//yfrrLOOVg7R/zjTNMEpP4nIdzy/2GKLuTN3mkK0jYGzSioWR7lTM2iTz6OPPqrPDRkyxHbxs4jcyEVGHmCduro6dwavA3sy3QK9FW0jHLej3Dm71TNhnl9ooYW0Mr3pppvs5Xr2+/LLL+ux8CvFCF0gr7cFKnfOXI3wnmmuoD07q5iI2Wmsc/HFF+uXFV8+RvQ09pxzztH9cuZrhH71U7i/9tprC18IRn7jdiQiNH/IAQccIB9/3G4Vofsh8V5kkUXEfAnoa+iTTxdOg23Gl2Sk/d/w4iuuuEL/FrhPzJyXBA/dxf/omcTjJraAvwltlmF7DLhiv9x1PJh+bftKX2gP+K1HwCMQGwL672327Nl466238Pe//71DR0cccQSU0vrhPQCP8IIFF9TR8vbaVwDo4KF+/TpMbD8FcCD/2AcO5HofsMoqq9h6f1FKsR4X53DkkUeira0N48aNs+d3sTsZtlyte5HHe/Xq5Z5+Tyk1EcCrPNi/f3+9eDl6tKZm4cKjJuHicQoXTo2wkcPNArA9Zrd/VkqNVEp15b3CYKPVpk+fjjPOOANjxozBBhtw3RlcMNVBPd98wyEAW2yhXcC5sPwZAO11xEXWQYMG2T6/sjtmu9oPP/yA5ZZbDiussAIPfcn/3n//fX16u+22Q+/eXCcGbrrpJjz77LNYaCE9YT+DM3t9ovP/dKOvvfYadtopFTN177336kXlTTbZhDXnANAP96GHHsLCCy9s7+1VpRQX3q3MYL99+/aFvVcAA+xJr9wtEn7rEYgfgTepeHfYYQcsu+yyOOqoo7DiiitqRfvII4+AHhwHHkjdrIWeLx9xb4EFFjCH9OYXpRQV/0cZlPtXSqnP6dFh6zjKuJm1lVKPA/jf0KFDUV9fjylTpti217M70a1RKFqzsY4j1vPlWR7r06eP/rfYYlp/8mUQ8naZM4c6S8vPSil61OgXhj1otrzvXETPUM855xz90hg7Vq9Bsh69ibSWf+WVV5BIJMB7Je4AqJHrWKBytxgBmBnp8DN670ycyPcWqPhv5s7TT6fWK3feeWd7OTGlrz89bXZXSuUacMWXMC699FI9vrlz54JKfMSIEfqZGM+iDfnyfeyxx/T4De5Rm/5PbIfK38GWHlJavHK3SPitRyBmBIwb4LNUHI8++qj+o/3kk0+0Ox5ng0sttRRuvfVWOwouJi7DAmfuIukJod3hLFSLc46KnaKsErbbiDvkx3wx8B+VQmtrK+ukpvqp+pn+p2umVj58CRmhayBFzyY5m3VeJnyRcKqe1uj8YjHyrd3h1hk/iz+65zrZH0Xld8stt6Curg477rgjL/1CKfUkgA05Rs6o11lnHSy6qF6XpWI/HUDa9TOZTE+Co6aU4xOJxEv19fXU5mx4eTb+5JNP6q8qvpzNVxW9Y54wrpH6K6uT8bqnbuML1j4bKvCZM2di6623ttewzaWff/55fP/999hyyy3t8ejLUJOq8eVgvvZ4XfrheOVuYfNbj0DMCIgIFegGVCprr722ngl+9NFHuOGGG7DnnntqRXvwwQfj5Zf1BI1/0QdwSBHlbqfxvYxS1uYVM3QafqnEBloly5mrEZcMSytyKgSep3I0Cspem2mr7RAcu6Ok7YtmOVaIKOklTCN6dsl9Z3ZpZ8paaXIW7ShabbLINAB7jNGnAAZ9+umnWvmtttpq1ixCxkdGtC713HPPYdasWdYkY6ueafzg9Uvqp5/SQwsRbymlGDNA/KnY+QLbhFi3tLRgjTXWwOKLL872XjI+5XSUP44TcdtJV1ulFP3+NzaxCbjnnnt0lW220csM+k3LA01NKScex0f/hUjb2hZE5W5NXwDSN5V+8pFKvugR8AiUHgFG0izI2SbtrRSaZajQ77zzTlAh0YbKoBRXqNwd5Uc3PSq3NTnbo3AGa4QKlYbkhJ0V2hMAdDSpiFARD/7ss89Au/KSSy5pZ3009XQm+oVAJecod/1lAUBHRnIcjoInMyNfCP3t2J0ZP6NTeU5TC1C5O/cwprNBmHOciifs/a+/ftqTk2YvbdeiIqYYezumTp1qMdQ6jy+/zz+3HzpY0vZp/Ns5++dXB4PJPuY4+RXw448/YrPNNrOX8t5P4zHzMj5IRPqJyOYioglfRORLEQncRVBbWSn1gzET4c0338Tyyy+PtdbSMDKAa1deR+VO05G5B74Q3rH1zW9gBWLAlyafoxFt8uG+V+4WEr/1CMSPgP7r5Yzy6qvTAZGMOuX3+Pvrrrsudt11V6103aHwD9xRqJyt/ZHRn5z1UxzFyMVSbXN1Zuy2KTrDUwOw4/r770+RLR5wgP444DWP2QujWxMlqakAqKCdGfivRYTRm2klbRW5GQddDPvYY5xhGqF3CiM/+/JlwfPOPdCjKG2HsBUiW77VkrQ1U8xMmrt8eZ7IF8y///1vfc4qd0aiuphQmb7zTlpXMprVCleYt/j6668TjG61wgVPyqabpmO+9iGOF110kX2h8YXLl86d/DrgQu/8+fOXAjAeAL8YMgkXeTFjxgzw2RtZifWJFc1AHL9ZG3jZrFPY61bky+G9997T/fN+jHT1krbX+a1HwCNQKgREZALd2+hfzqhM182Oxyn0zXbc2rQr5L777kvzh9B/3ZVDDz00U1DRVF5DH23WWXXVVdN+2rbul19+KUsttZR2xyT3ioiQYCsd/+/er+FyTzOFsR6DozIJg6369OnT4ZR12SRBWFRI2sVxbrYZEyal5eWugnFEhNfoACn6nbty/vnnazdPuizSrXTatGku86O+lG6QTuxA2jNHRDSL2Oqrr67dOhmcNG/ePFlppZX0OB1OeM3pQkphI28xGIz7zc3N2jed2Dv4Ru36VMq383q6qzI+wJUbb7xR90dueyN6ddc+G1JC8PjNN9+sx2n6Sc/a7XV+6xHwCJQBARH5Hf8gr776aq10qdCmTNFu1zqqc/z48TqYxwQcUeHuw+vJNc6XAf/Q+UfMkHlGhTIoiEqH/upUVkxMYYXBPFSajFZlIA4jUqnonn76aR2uPnz4cFfpH53t9m2YO33z99prL6202C73//Of/+ju7rjjDs3FzuOMNmXkqQ1YIof6mDFj9Fh4nsrQcqdfdtllMmrUKH2ud+/e2mf88ssvt37+6U+bTGMjKSI7Z1TqcsstJx9++KGO9mTAFpX7tddeq19CjJalvzoVvImM3Zc7Ng7g66+/ZjE92+U+D6y55ppy1FFHCQON2CbvnwyMjCQmrz0TgIwcOVJMfUadkZ5xXdadPHlyGqcnnniChyjWhJW+Hbq38wQzQDE4jLjYZ0s8iNeLL76Yqi2ifSRtZROzoGkbGKhm5B/2PLcd3ibuSb/vEfAIlA4Bw9r31muvvabowUH7+m233Yaff6Y5FVh99dW1WSaRSNDjYaxSikyM9Nve4cMPP8Qdd9yhTTa0p//6178GSaNOO+00vXBI98ORI0emPSt4PX3caXa5/PLLtRcOj9FTh9cZ90B2ezmAY7L5Z5PuF8BDX3zxBb78Urt7pwGhdw9dOrmw+fXXIa9HcDwrrbQS3n77bT2+dCVA+4/TRky/cdqsXaEJYvBgbSX5UinVQSHaa83M/jmaMF566SW9KMm6dInkAivlrrvuwosvvqh9yYcN01YlLmb8jl41ra2tiy+99NKacOugg8hhhg2UUuSSOQ3AObynK6+8UpuM9t13X6y33nqgqeTmm2/W41555ZW126qJQThVKXW+sa2TyG0lmnzeffdd/YxHjRrF9gcrpUgwpsWsOdAutDIP0DOGC6t8tttvvz1OPvlkcF3kf//7H9dEpnGNxT4jc+9cMBjAcTBm4aSTTmIzeyql/pXqwf/vEfAIlBUBBibaaVaWLadqaZsz/5BNjlHNcJWljntYTxU5U+XMj7wnWYRx+vvncvMiMjELSRijXWk6ypT/NEu36cNz0nvhHbb1hrln8vEyY1LGPKoisriIuOn/2BLT5JGtkjwrrjzKkH3er4icxxPky+GXgxHm4GM+VybMZqIPnaTUnhSRpgzkZOR+STu9m7a3NAupOurU0BKwGe2PafHmS5sH+bX1/vvvO92IvP322/oLiOMzcoqtZ/rQdqiXXnpJR+YaPno+B+tJ5V7u9z0CHoFyIMDZHWlyRYQE4+uJCEPRGZf/WxHRwTeZxmHyglLRMRPPEc4/mhls2jgyYR1PhcA/fOoxhz+FypR9HWySR+f11U52ARFZxfxbLJMHSHTcVGiGA4VMkKx7jNVWGbanm2tXFxG6NLrClwgjWjMK+VaMDXp7y5FjyL/IcKZfEG5Fk95vPsm2SH1AXhcj6T7MNeSiIc+MdpExjJb2eZGJUvuQRtpmKrxP2R5NUGadhDPvtIiI5p4geRzNWFEqCpq8mGWJ5h/zskpHnZoXkE78zbWG3/6WPxstf0134Hc8Ah6B2kJARJibTs9YSUBF5U57tyOc5XZQSKVGwSggrrry38KmTJKbz8mFQrZG2rzJl0IyLsOo+DbHYVkzuTZA2zVnsbSbG0mTshc7Zmvv/utf/yo77sh3oxa+RLTPfqHti8gktsTxM1OSkUlue5bRkXZ80vX+97//tdfpxXQuTHNR1shvI3X34nEm9SbnkCE1I0BZTVhufb/vEfAIVCEC1oOEVLuk56Wu7N+/v4wePdpVFppbJo7bM94iV1qPE6ud3C3ZLLnYSpIxju+CCy7QrIgiomkHRERzEU+cOFEWWGABnZCbtLhmNpx25i52/HS0EZHZfLFwYZQLpEbuKqZtEdGa+sgjj7SzdlIquwFkfIGRv3k+Z/Vc7KYHDrMpHXvssTrl4PPPpz9c6E2T/sIyL8tPyJy51VZbyb/+9S875pAnTTHj93U9Ah6BCkSAnnH8a6dHDelz3X8Ola5m9Srl8I0JhEo9baemHfiDDz7Q5iGaiIyrnlVG2ruHyp0zZyP0UtE0jaTAJdOiY6+me6Lri16S4dv+6KEyYsQIOwsmbS99zQsSm/TbfI3w1hi92kEsmyb7pjsjefc5W3ee05NRd1ARuY0NXnjhhfqfwe3D6HUdOvMHPAIegepGwOQlvcX80WfanM07NDNH2qMZhPSHbNmYRIQ8vPQn5yInlYjmCmfgklkQ1Iu/dlGTNmJyudM9c9iwYdpnnzlKmYSEtL9GNNUvlT994jfYYAN7nC+GNrqBko7YcSHk4mrByrazJ2rWP27lAH744QfNp28Gs31n9To7Z2bXXCzluI/s4lquB2gfTQuCiNDQfgHNWdG6fAY0UVk3U2OCK/nLOtqvL3sEPAIVgIBRWDoQx1EYdvftLCYTzlYZMZoWN73ejz/+aH3PyQkfdu0wWZAYGLXhhhsKg2+cWavt124fpgugTZhBpc93jVXk9MM/7rjj0r7/IkJ++pCXSXqAJdox3jF/dr46mLKJ0Z9lE/LhiMjWJq2e5vDJ1LnleDdgMkVip2n90vacTI35Yx4Bj0DlIEDuEjIekjHQxKjQufxFQ0SlB8ok2m1tbb8l0yCJscg9whB/Mgsa/3Htpz15ciqX8xJLLKF93pdeemlynSxGel96ipAOgYqY/tYMpWcb9Mm3stdee2G//fbTPOo8Rrra448/Xvvpm2u4QMqISTqyk3fgGqWU5hdmjBCTSr/55psJht3vtttumi6AfW2++ea6DID0mAcrpXSIvu03rq2xi5Mu4DOlVJTfPa5u82rXJNTe3JC8vRThds+rLX+xR8AjUAEIUBmKCM0HofR0ZgZHk8k1TNjEoTLRE2fAtHfb6E/OkE2mIF2FNm1GvHKhlbNmY+elp4gmaxeRP/FC0iQwgpZZgpZYYgnTnei2aFKhJwuFkZU77UTPzk7lB+Y7tXDajEi0ddMdkGNkRiUjzBjlea8sWAVuQ8z7Bbbhq3kEPAJFIkBbrWniR3dGZhbk6ErXi+RhjMZktKiVlVdeuX7IkCEk4uJM+SIAB5GydpVVVln/uuuu0xS106ZNs5S4uhqzL/3hD3/AWWedZZshm+IpSilLN6vdAZmJiHSz11xzjZ5R24v5VcDITNLfUpjEgpmfSIZGMi9GWTISllTCdrvaaqv179+/PxOP/tMwIk4l0dchhxyC5uZmzUzJmT+Ap5ilycXA9uu3+SHglXt+ePmrPQJFIyAiewD4M6CpZml2cO3KtGv/x3CEHwZgElkYTz/9dE0BS8oBmlJIXUDaWibIGDJkCMekub2pOEVkWwBTVlxxxTVIJ0yl++qrr+pUbRdffLGmBTjhhBNYh0r990opKlpXnmeBGYfIsMgsUePHk9wwJQ8++KDu07Is8hqabZhYgv8yCVPB9e/fn9zE1syik0rYF9WJJ57IarTBH1kuU0ymcfpjHgGPgEegIAREJJ2tmeRehtBKWyPo68wkzEaY7XkevUe23XZbmTRpkjWf2PPulqH17guCSpkUjPPJyEizB2kI6EJ3ySWcPGuhN0coSYW9IbMoq0mtSFzF+s64NBOjk3RaGDBFkq0uhLZ8TYBiFlV1lCXNOeuuq/m2WD36krFD8luPgEfAI1C5CBielCQpZE899VQhAyJdB8eOHStHH320dnEjwyGVvBUyHDoh5jzMmTm5U0gxS2IxatWMnCJkIWAFRmBSQU+YoBmHbdM6V1w2tMhEywvvuusuHXBkK1HZc4Y9dapmFubhefSOWX/99UMvKuMK+ILhpCH1os4ETYoFUqqwIhkXScvLoCsjvKd01upsY/PHPQIeAY9ARSFgXQCp2LngSSFxFJXlDTfcIHQnXGaZZeSNNzipTgk5wbkA2oV8HOVjN4uwegH25JNP1hS0hx9+uNsMPW+yiojsyosZHUr+FY6TVAEMl2fUq6El5mIueXHkySeflLXXXjtN5+t2ZPZJRk//bR1+ya+W0047Td87lbvhUeGlzHPqpQQIeJt7CUD0TXgEckRAm0522mknTcfLOqR6pe18991314uPTuo3naGHmY/OP//8dPOktWVia+bM5OIlM/AMGzaMeUdpn7+YFxqekYeZ6Yj5WXkNaWRvuukmBEGAZZbRNCRcwGUquWyifai5qEraXC6i0rY+aNAg7VLJRVPmjDZ9jt5iiy2G33jjjZp+lpmRmPqOC6qkNh4wYABGjx698HLLLccMUrod0v02Njbqf1w/cDJNWZt8tnH54zki4JV7jkD5yzwCJUDgZgDb2NRvbI+eJszLSa+SqPTp00crx403Zi7lzEJlb2RBbsm+COC/TM96991345tvvtFc31yI5ULohRdeqP8BOARA2l3GNuJsR9NP/rDDDgMXYX//+9/jgw8+0F42jz/+uL3sUqUUicpIe/vvTTfddBRfVswp+sknnzDNnL3OTcJ9lVJq8KhRo5ieL0rly4zQnSbpSDfodzwCHgGPQKUgYLlBrMmCpo3FF19cTjrpJHsoZG/nwY033liHxqcv6LhD5Xo1F0cN3a+24ZCr5Jhjwgy7Q4cO1WRcJr0fKYCZiKODGBoD7QNPMwyZB0kXwAhUZosy8qBb0TA/kjyexF9cPHWFZaacSmcTMhTGpEAghTFpDnq57fn94hHwEarFY+hbiBWBR/sDvVcBZClADQKSjM5cFEj0B3TeT3Jw6FmrGQbTGnHKyH+pFEc6SlKSgPoFkGlA4hOg7iNgy+mASsY6fNO4CMePLzmTZraga6+9VmfZ2WabbXSGJR5jViJm1DHJs2mPVtdffz2YqNkcY2s/AWBm7G8BMEKV+0y8zXQ/2h2SphcmV+ZMvb6+/jsA9E9c9dZbb9VRpXSPpLnGYHSSUuoSFwMTzMTkDwtPmjRJR6h+9913OPTQQzF8OCfcYAahrZRS37j17L6JcF2PSZfMGF9XSjFDkZcyIuCVexnB9l11hcDD/YAFNmPqTwCc5ZEJkPbkuISKnanPnk79U88Aw98BFBVrScV4gUz/4IMPFE0kt9xyiw4Ooq84bdT77LMPaLM+55xzsMIK+pbJ9XIWsw1RGdNEc9FFF4WCkdwBtrW1aXoAvjRaWlpw7rnn4tRTT+UlzzKkfv78+b/+y1/+gjPOOEObSLi///7704bOe11WKRXKoSciYwHwDdDX7QfAYwAOUEppit7IOV+sIAS8cq+gh9EzhzJ1FaBtbwC7APgVgO5eByIXyuOAPArUPwJs9b9SPRdmuwewF3NrcuF0k0020YujnGVzRt/Q0KAjPQFcppyz6wIAACAASURBVJQ6VkSIyz+ZYOPSSy8F/6255ppawXMRkouVG2ywAUwOUHz00Uc65yfHy0VX5illPlATJHUUF2dnzJihc4HyBM/369fvE87qnejU9O2KCN8y5H9fG8AcAP8HoNlHj6Yhqugdr9wr+vHU6uCeWAaYvy+g9gSwaYXf5fsAHgXwILDQ48CQWYWO17grXsNExob4i18OfJlY6gESVo1XSl1l+yAtL0m3eA0TNH/88ceaDMyet0mqbdnZ0mRzI4AzAGxk7sFdteWMnexhRyulaGbxUmMIeOVeYw+0cm+naQEgsQsgTDXP8Phc0r19BiiGyL8NJKn4PkvZyxPTgdZ5QP2PQOt8oNHa1p3bnzoAaK0D6hcBkn2Atr5AXX8guQKQWBHQs1LahZm3NCvNqtMgd+cAqgWQ+4G6+wud1TsMhMxgPwPAugCo6N9TSpFBMSQmkvR3ABgINNT5umEd4kMlTUXOfRsE9GV0hm1S7JGrgLb5NyqV/TB0875QMAJeuRcMna+YGwLNmwPqQECbGEIh8pH6bYB6ARASR00Fej0JbMnZZ8zyZm/g2w2BJJXelgC2dhRkV32/k1L0iQeAflOBIe2+f9lrUrGSwZHsX7lc36El8wVQp5TKTOTSoYY/0BMR8Mq9Jz712O+5aTlAHUCGQgDk7s4mNA2QT+QOIHEXMJyz2G4W5qycQs70bQFNwMWZcnRRMdMYfwIUzTcPAMkHgMbogiNNIyQLY3AQv1pmAqAN/k8A6NHixSNQUgS8ci8pnD25sacWBObvZswunP12xsdN972bgLabgFEV7iLHmf2MBkCNAXSwTi5ZevjSejGl6BP3A9v1AmYzYjRTyD9NKcMAMFmGF49AyRDwyr1kUPbUhiZvBtQdnIPZ5WdA7k7ZhkdMKZd/eYmfSh/g5OHActsD/UcCvdYPv8RouueapWvCp54/MAl82tnLjn7mmsw883ifGgjMWRpILAMo+vkvnfL3V3SHWRRQAwDNB8+IT9LqWpkNqPcB+V/K5JV4AdjqPXvSb2sbAa/ca/v5xnR3TQzI2R9QDGFfp5NOqNlaAHUjkLw788JnJ7XLe4qKkSYk/mNQEDNRDASwOIAlzLZD0uLMQ2RTvJT/aIGhw01n0m8OcP9N5gra5AcAaiCgGRKpzNMcA521kuO5DwC5Eejzd2ALbw7KEbRqvMwr92p8at0yZm2e2NFwkpAutjN/dM4UbwKS/wAaaYKpNKENnS6YDLfczChzmls68+ChImREJv9lcIdUClhoSaB+aaCtPzC7Pr/10nsiOTtih+wXQF0DJCcAjd4kFDvc5e/AK/fyY15lPU5ZH5BDANnPzGCzjZ8Kj2aXG4CG5jiiPLN1nMNx2roZnk/bNv/RMybKZfKZCat/16Ss45YeLQzzp0J3zR05dDl1BeDL7YGpY4HL2HcXQqsNY4boncn3DsnCclnH1eMiJQH/RcfIL46uvja4iH060HBdlZrKusC15572yr3nPvtO7pw23rn7Agna0rNTEuoW1JNA8kZgwTuBoVQwlSI0p5CtcFcyMUb4Z+hiScoBul3y36vGeyWOsVNrTwNAtsYsQgoW/iny/UJLFoUfRmvPBTb8Gth8GrDOm4B8AajPAfkUkC+BRT7tOqiqie6n6wKJjQEdELVFli+UyUDbgcAo+t57qQEEvHKvgYdYmlugC2DTSCBxhFGI7qpgtAsqGXq73AiM5Ay3UoT+4/ub8W/lKDG+dB5IRZlqpU5lW06h+yNZFDNgmvgY2GVb4IQZwKWLAY83AN+RW4eMjTYgiWMlRwwpg+k+ya+JAqVpJUCdZvjfo4u83wLJw4CRpBnwUuUIeOVe5Q+w+OHrxdGDAfUbcox00h4jJ/+bWhz96mFgbNQE0EnVWE/xN0zbOV9K5EGxi4/0MyevOZNNkye8Q+RnrKPq2DjHSK5y6/fPKToV/uHG/BOtQcXLr6btDV0B7TUUJpZmPS7A8oVFzpcCpGUjQC41Ziq3PtkzTwAaQkyR7gV+vzoQ8Mq9Op5TiUcpCaBpGzNLp+kian92+3sp5b7Y+5YK866gVwkVOv9ZhcmITc5ubwPwnAnpd++lu/dpmlnZ+LTz5ZOPCYQ0CQcC2BcAvZUonMFfCeBvAKJBU+aSzjb8WmvhS52K3KVNZqVzgRE+5V1n8FX4Oa/cK/wBlXZ4L/QFfjkAkD84CjFTFz8A6p9A27XASNqjK0lIsnUMgGNNGD/HxihXkmvdVfhMtiy3+A+joMmtw2jWQoQePaxPN1R+qbDMr5J/ArjIUBjn2W7LeoDcabyGnLrqbKDhTOeA360iBLxyr6KHVfhQH18RqDsKAGdploEwQ3NcHKWS7HdX1wt1GarHe4izVb6UyHNODxCaJ6jQLjDeLfH2XprWqShJbcx/+czas/XOrwAGPx1qyMBo6rkXwCnG8ydbvQzHSbTWRjMWvYkcUWcADaQN9lJlCHjlXmUPLL/hTm4A6o4BhB4j2Xy4vwPkZkCuAUYyFL7ShOYX0tYeZ+zpdLm81ij1TyttsN00Hr6wyRrJLxoGPbUCuA7ABGZ/yn1MpJCYd5/hv7HVJLUe03C9PeC31YGAV+7V8ZzyGGVTPZDYF5ATjdN0trrPmwW1fwGNBS7KZWu6JMe5oEjysfOMwqLHC23LtA+XgS2yJPdQ7kbojUNzTWDs8r8AuBDAJCflYBdjaloIUI8A2Ny5sBVI7gqMvN855ncrHAGv3Cv8AeU+PB1BShZGsgxm86kmxezdgFwCND6Te9tlv5KKhUqcqfbIWU5bNd33Clg0LPvYK6FDfu38EQBf8NxnoBJ/F0ybZx3pOxkn89b2ajFc9/a6WYCMqvDfjR2r35rICQ9EVSOgP6VpSz8JwPJZbmUGIFcDySsqPEiFft1/BcD0cpx4cA2ANuUXstxXNR0eY7jimVSDHkjlEBKLcRZPjyJ+CTH/Kfc/7LpzZstqJf4rOdd+AyRGAcNfc475XY+AR6C0CNCNrfkAoPkzoFmy/HsRaDkUYBakihemk6O5hTPLjx0FX/EDz3GA9HHnvTGbUrmFSUi4nsL+mbWK6xfRAKYMY5q8JtA8I/LbmgE0ke/ei0fAI1B6BKb8Cmh5JvJH5yr4p4HJOwJ8AVS8MKr0VqN4aIK5LAvvecXfSBcD5FfVboZdsotLYzlNe/x44ypKJU/6BbJfdiFNQ4HmnyO/tW+BZkYAe6lgBKrhj7+C4Sv30O6sAwaNA3Bq5sAj5vdMng008vO7suUsrIwmHIsncQTmoi/6YRa2wxSsDyZr5uLph0hgGpL4AIG3tZfwYa5tYgI4m+csnl42t3TefsvwVDpBbb+3l84F1BFAg6Uqtsf9tkIQ8Mq9Qh5E18No4cyPLovkHYnKq4AcDzQ2R09UVPlcLIF5ILvkoZiK9TQpAOeQvzJhOaRBzyykpGUw1StQeB2CJxHol0Dmq/3RrhAgKxln8VykpnmGAWA01czOXnHylkDiIbNA6142EWgIKowF1B1fj933yr0qHn0zP+fp283kEa58BahTgOE3VzRd69lYEa16wfdQzMWCIHX524ZGi8uM67q3lPM+PUBoWrgfvXAvTs/HnzvnPkp1Ibl7qVAriTWT90YqYtI10Deei6RjAXRCBNc0BFAkFVsmAswNwEK/zTFBeKSqL8aFgFfucSFbknZ1XtKLAOGnc0TU3UDyd0AjucYrU87CqmjDWQD20GYkquM7DDs6Le17RXgPC78L2urpgXIH6nEbzihJ9Gfho+lYk+RgXFClOyKTZFeS0EOJphWyUJKbh5QGnXwB6uTnjIKNLKqqh4FeuwFbdDL7r6Tbrv2xeOVesc9Y832QAGtwZIizATkBaCRhVGXKuVgM83RU6ZFpmtv/GbJakgaQ5mtXPI++uBsKk1GHT5DAPMzFACTQH6KpbleAgNmRVjeKZLXcPDx0wopHoXApBA8j0H7y3Y3Tb43P+cmGpbK7xxPtn3pgookE5hM6zFA7RK8zZR3oRB4fMla68gCwxG7AYLbhpZsR8Mq9mx9Ax+7p4TLlSEDImRJ1YXwDSO5doTQB9KheAIJjofSCL5NEpIROeCTeJUnwr/AiRuMgnKVd8+wVXW8DvZhH6zypc5lwgoFO7X1kboHJS6/SJq0APpVcZozco+So4aTB2uT51ZVFXugF/ExsGRHrCM02yT2ARlIgeOlGBLxy70bwO3b92GJAr+sAIbGUK1x2vALofWLFfvYGoPWc/OCcbbfL84Z1XKENA3AMvuV9lEACvRDIfHTEir7j2S33gh+hcBl64684rZhEFyUYd+U3waxV/wKwiIloJVFbJ1z4TRcAitGwrlwKjOACrZduRMAr924EP9x1c6NZ3Fo2fJzKqIKz46QWS6nUqdzD0gxBMxQUZkO0j/fD4QtKWJqIwUhqLpp9TDLSTI3PNDPTi7x7ZSZ40seYGIQ8MvTQYmIQLuh3ouBbLk55a6XrM8D4t0AD1xm8dBMCXrl3E/Dt3T7QB+j7F8PoF3ke6n5AHQIM51JkZclV6IXpOAFJnAkFJqAOy734DC+CyUGZUGJHkyYufE0cpTtRh7exEwRHm3D/TL2QKI0sh5MQ4KNMF5TwGNcKmGSDZFxPlLDduJuiRww559cxNMJcFM9iS9dJP+grzxerlTYguYsnG7NwlH8bUSblH0DP7vHxVYE6RmfSvOBKK6ACYPh5FeniGGi7998zLPbyHt7FjXgZH2l+GL6URhSWQMKFo8D9idgSSb2wG134sw2SSI34n4sA79mDJd6SzI18MvSSobdMNQm5aZiikIFPXDWhfxMxyyB6kZUvL2aMsvItIEOBRq59eCkzAl65lxnw9u5aDgSEFLZMPOEKSZ32A0bQh7uyZBL64RcwcQOzIEV/O7OhcB7OQgKtmqyKLpo0Nb3R7TcRYIiJ6iWvfSZOFS713gbB6ZiAT0o8XjJ0MnMSg7Aq75l2fbOLA3gcwPoAGMREDLOYaHRSGCby5kvByltA/VBgGE1iXsqIQPQPtIxd99SuuGhaz4Ak/pFE5VKg958qctF0AkYhiWugdA7Q6LgfRB2OwZlaiV0OgDziXJirLFrhQJsYOHum+YAeIVGhj/b5WAfnYKz27Yme76llZsHiDJ5cNLebPK5c5M8g5D1KMu0hA7esPAJ8PbqCkqrbcdX01iv3sj7eZrrv0TbJ9GiuzATU0RXJ03E++mOOTmVH3+fo74Wp4o5HoL0rmLiZ0Y6c1Y3uPBDGvfVu2CevTZuOmKUbX9TdlAN6GAtgL/wJP3bD6Cq1S0axUmmvCuBsAJ3kVm3ZCxDGaDi/F5kANJJ+2EuZEHDAL1OPPbIbTfhFHg+SfkVnjM8BbfsCoz6oOGgmYGeIdl2MevBw1savjxMR6JD6oWZmR5MHXRP5+V75Eugweia0oLtfVMm3YCC2w7HZTBCVf3sxjJAzd5qW+ptFYuawzSLN5xpTmD3PdH17AA3/tgf8Nl4EvHKPF18ALasDwtk6swq5Mh9QJwHDL6u4RdMAtLMyExJn41FhrOnhCDDZnOBMjnZWEgrQK4Sz9+qSQCekoDvnzpGB34ggGqQTuaLzIqOLqQz5/PkCqQVhAm2yjvJFvoOxx2e4L+1BQ7KJPZ2T84HkzsDI+Fxinc56+m6mxaWejkkJ7795T0Co+KKK/bMUl0fDJRWo2JkF6a0Mip38LZegH9Z3FDvtqncaxc7F4epT7HzadIccj10gOrLWff4HYyKK4S0nbwvdB7O4ELpdVc0+PWK4oN5LL0IDdPXMIEoAYdYnl4isF5C4A5jsetRkqOsPlQIBP3MvBYod2ni4H9Cbipt26qj8B2g9HNia/t+VIykTBd0bo9GxzN/zNupwGMaFvD3426Fip/8zZ/Eknqr+kPNAJwuhj7yV5xBgM1socMtEGbWk4AkDE2+fYLyh6MqbhTBs6hpA25SIB833gIz2+VgL/DXlWM0r9xyByv0y7S1A32nSY7kyC1AnAA3k46gcEShM0KYH/rFGuVro0zwJAzExg+35DwAuAkAzDf+4K+tlVSjCKXdP+ry7tLYbIcArhTZZo/XqjHmGcQyMRCU5WhbhTD1Bb5sBzgU/A7JbVSSWcQZdTbveLFOyp6VtjH8EkrSxRhX7q4AaUnGKPcAqmKAjJ6/LoNhf1ko7wOkZFDvD07lgxkhPhqbXhmLnb+Ek/AKlvYPcXwaDd7yEEWBsABOvMFCN5hdywWeRkfz9k/+HmZ+sLAQwArvpxCpJB2nHXTVbr9xL8qjou97yX8PkyE9wK/QQ+CswazOggekpKkMC1CMAvURezxCiP1cH83A2nnm2Sjs7F8roXcI2mOShtkT0/XGNwQo5zvMV/m0dA2BkvhWr6PovjFKnoiedQyc5WRueMli4VBq9ATUJaHkIaKIvvZcSIuDNMkWD2TQMUDTDkGTJla9SdKgjSLxUOTIRGyOpP6NJnxuVp1CH3+BMnScpes6Wra2VvCO0s2cJZrGXV+k2AJUR4xIoggXRH6cgnyhLehEx7J725kypEU3TNbGZZF705AAlHXMnay+T1zTp+laK3DmV/ulAw3UV52QQGWi1FP3MveAnJQmg+VRA0ZYYVeyTgcRGQAUp9nOwJAJciySeM1lL3Tv/CUoTbW3VhWJnWrbjjRmGAUC1qdhTyLg2doW5nc1KXSjT+7NSykrPaNMHa3SHMRz8XdErrAuq35HvAr03NmRkLhxLpGz3Lc8DmiHVPef3C0DAz9wLAA14chDQehMgnLm60gowEm/EuRUz+yB745c4BoJxUDr4xB0v95ky7UgEoHtmZ7Kg8YwgVwp9l8n5XbsSaE8QfqWkRGFXjAfzh3rJjAD59JnqkF5B9O//OPNl9qhOSnMCIOcZt0p7wm6bATUeaOCXj5cCEPAz97xBaxkJzH85g2L/FJBGoPHsilDsKS+Y3fAluDB6YQbFTrPRXmCSja4VO1FidC0VO7lFaluxp34T4WTW0oHgLe9fTo1XIEEcF9lJ/5wDjzv94BsuBBRfBKRDjsoIQFqA5seB5mJiDaLt9piyV+45P2rtDXM6IPwhum5ypND4P6D3hkBjZfB1B9gaE/AMBAz1juZg5eLX39EbgxFoP/VcECDlK32aybVCF8jaF6XJz9rvMxNnfftZv5dCgLNwOg6QBdPldu8En4ZpwIjtAOHXYKavRy5ITwFaHgO4vuUlVwS8cs8JqakDgCn3AkLCJPr3WiFJ1nHAcGZ9/84e7LZtgC0Q6Mz1XOyMcsRzWC3a3h7gqDzTzV1sEl1zZja92+6vvB27Xk9cXcjCY551UIzy7Wk8Kvx7sC9/LrIyVV+O0vgvoPcagOKaDr1wIiKjADU1peQnbxk56YsZEPDKPQMo4UMMSmp7ERBmE3LlQ0BtDoy4FOAnZjdKgEEI9MIdvxwyeWa8DKXNLyMQ5O26yAhUri1QWVHJ9wyRSHap6Ey+cxTIZ86w/OhCe+e1auMseWPICEmyuZPyu6UtZgOk5JBVUyyp+LRjfSr5xBMp98mWjTqe90csAl65WyQybltOAZLkholS9N4J1G8ANNCe3X3ClHITcBSAd0wW+ugCOXk99sJ4bIzxeuE037GSP+R8U4kh+fnOXvPtr5Kud/nIOS56v+QqXM9gfRJr9UQhZz4D3DgLdxN35IhF4xyg4XKgge6SDI5y+WlMG3RmkBeB5ptSDg45Nt2DLvPKPePDZl7T5hsAoWJzKXrbAHUG0LB3t2eWOQur4i08BQEJu9ywbt7RN1D4DYB1tV1dFeyySG4c+ms/YCh9M6JVkwejNnaJ2OC7vmn6ejMbVU8UZrMiy+ZCmpatYARUEhhxF7DQeoCQVTM6k+dk5gBg/utAM11zvTgIeOXugJHafWxJoC+JsA6OnJoBJLcDGs6pADPM/mjTbmdRuzoVymXogzUwHtch6CyYJHJ3HYt9ANB/mSanThIzdKxYE0cklEmIt5TPzL0mICjyJri4ynUoThCyMEfm2sOQ+UDjlcAs0mcz6jdqkyf75vVA820Ac7l6IQJeuYd+ByQ4qmcwBqPsXHkakF8BI5lLsvvkz1gYAW4y1LrRxSq+kEhwdSxOxfclGOThxmbMxMj0X+5pElUSLi9KV1gMB7BhZPG9qzq1dv4HwxxJ097E0tzc6LlA49+AuWsAwoTjUabNvQH1HDBlndL0V92tRG201X03RY2+mQRYVJyRP2q5BBh0MjA4+kMqqre8K6coeUllwETFrsyCwvEYj2vcg0Xuc9Y+DcByJprVjdYssukqqR5of36XMGxZBB1mjNluhh5FZNjkb6mTUPxs1WvmOJO/k4KByV9Ipsf9EkrTWkDiRkCilMw/ALJzxbgml/CO82nKz9xB//Vmmh8YmOMq9rmAOghoPL7bFftZ+g/jyQyK/VXUYUiJFTt/PzRJ0dODmPQ8xZ76C6JCapeBOTNf0lX2RvOvJyt2YkcuHmb0op6hOaXE0vgO0G8rQBhJ7HqsLQqohwB6uvVc6eEz9yYull6bUuKhH8FXgNod0Ex2oRNlLwTaX/1+M/ux3fOHfBkG4uQMdLz2mmK2ZItkODk5QHqiSYbZmfhSsxmDZiLIx2e7GOi7sW6gmT75tbYMlPFyEbQhoV9s3yGJb8GXXH55ZfmS5AIrX3RsOxz5W7LbbdoJUExn6JorPwNkE6Cxp8RmhNB0PUFCJ2q/0LQAkLgdkGjmoVeBujHAVvxBdq8E2FZHmYY9N1qh8Du9YBrP6EgORsVOn/meqdhTuLbP3KVGvV4CDITCjhAdG0GfcT73VPCWOw92yY+5RBpoXnZy+DMalabC2xHg6yw/R3oM/QPA7wzv+wVZrivycON9QNN2qRl7mkNpOUBdn4pR6eZYlCLvrpDqPXTm/mZvYMZ9ALaJgPYfQA4EGvNZPIs0UaIiqXnb0IKwYmcqs70R4L8l6iVTM4yq5PoDEzGQyrhnSqDTxpGznvICgg55cM2pKtyQniIVYMTQ/uIneHQTVbgSwAUIMkYwr2FeBHRlpOdMjOaqyQxyouuuE2FMDxsuxPYs6aHKvYmmmEh+U3UlMPyoiiD9Go8VoPAMgKWdn+N3SGAMxoG297iEUYUfGg4Z2twZiNLzhF5Js0Pmg4cQ5ByQ9FfDPcQk0pVlDgh0QNCpxpMnjuc6WydtFExCAHe+z74eA8CvQn4pxzk5AdDMyNi/ODc4G5D1gcYSL+g6PVTgbvFv7Qq8qc6H1DIWkIhip1vVCEbVdb+kbLv8qnAVOz9tGzBOUwDEOUamS6PrGrPq9EzFTnTnd8glm08awTEAVgBwYJwPKq+2mU6RZHEpGolsVRl9/KahdeYMm77k86DQB4LFgA7/GLU9MNLYghAd0bwdAhwYYRvlzJnKnYv1MSt3sk228DlYorEFgQTNQbtGxlvTxR42c2+iDY5p4dyIzuuBERFl303PPEXTSw8VN63bHAi2wQRtA49zYPwtfACAId/8dGbi654pAegnTUVn5QoEONIWutjypbwioL+8uri0DKcnYD8InQb0Ymm0Q9Ik3AHBXVgMz+e1UMrf6lkYgqROjM0XGScFrnyFBLbDOLxqDtJM8qWhBCZGpYjFcPuL7DetBijSENOt14gaBTQwHqRHSA9yhaTLo+LCjqvY3wZ6kzOlMmSiptV1FTv/hA4tg2Ln/dNXmLMxJvjuuYqdSCQ6cLfnk16PCowmte6XABMg+GcGxU4uosMxECsiwHH695WfBwxASotxeB6BprmgTf2hyA0viSSaMRGWwZFxIsy9S2XbSTLtSCsFF7UJhhQIjtBlknqgZ0gPUu4tZDd0kxXPA9R+AJnoKkDGYxgEDNl25WyM1wx77rG49smnTbnTbHvuJlmUcq8M3AKQcpcJVtoltfDJpObr6ZSL+Sr09pbCewE+wniMNnS/pP21siiSeAxB+u+ObJEULtaXQRYgRbfrxbMh0MRx9gjpIcqdRGBpdkPzYIUpvLqX1dH+xJjfVOlZjftpOxnrYIK9JOYtZzOcTXERzCt3FVHuKufE2JVB8xvgHJOw2v3ZvIReGIzxuLBIziG3zfZ9zuQD/BUKO0B0Uhd7bgHtzhvoyGq6135kbOHRBNn2+hJuh/4EyFnhBuu4oNwjpIco9770XOCikpVpwKCLbKFbtzS8zNcLmG52py/RC/tiLJg1qRxCAjIGmNCcQLNCz5ZoSj3JWbmTRZTxEVFuovLhGYAzc0Zcu/Io+mE4zugqr6lbpcD98WhCnfabb/cUSuXufQDnYCkAdzN1mXG3LbCTfKr1uQ4A1xaMyJZAC7l/al56gHJ/gbzap4SfZPKkbqcUsAOaoBek3E9FKvR9cbr7g7QXx7b1JhkX2sJn7lyEZUJnJjYpv6Ts364LIMdAn+8xOClvyuLCx89F1AS2j0SjLov5uAML6PGw7Why+cL767Qmza5C91RHhDkQal56gHKfyZV8unJZmQKMrIws9mdhdQiiEXtnm1R5drzl2DKohRKzi5rppfI3hSbqGG8yYZERsbwSgGtKDCRyFwybsQj2QNANbq1U8Ep7fbmEe1vhRNBFkRQEnD3bILGYsWq9wvDc2H52BUjtXdtS48pdEoBiYmdHJLpo6Zwr426AerTh5kgEKumGuQhUTuGPnEyTDF7iPy/JkIIkJZUbjF95+JCmAiCvipvf9wUdMHSCjrTtnjGPBymyw7PkehyLAfrLZsEsKSFjGOs2TOxOryErvYFeTGZT01Ljyr2Zs4PVnSf4HjDiEafcfbsKpxv3w9QY6MlQh/1jWezq/C6JEWd7TZ1f5s9WJAJ0NRSdiLs93F4058sOCEJRtt0z/ED72NPubUVhcww2hSj9h70mhq0wMM8ROcgp1ORujSt3tW/4qcllFUEvMBGbQHBGaGwJ/BFnag710OEyFBpNH165W7ATBc3UDwBwVQZaZttq6bcBNkQS90W+/j6GwrYIKorsjLEk7esQa6S9kfjFUSZp5JeM6x23eq1TvzYDwwAAIABJREFUAtewcn+Kn31usoW5QD0/XbtXAvRFEjdHCJvuw3itGLpjbA2m05bu6Lwi+xS4vtocYtQGn2nYXEAkfYO7vpPputIcS3H8P2ySgtg2v0IdtomE/dtz3bdN2fw5U04RhjGNCf+lWCj7l29g0dl7W2TyV76RlKOnGlbu87hI6HA7k7x/q5hDnnN6ZAwuYVYaKwyy6C76Ay5orWUy5ESTD9vx9cRtdEE0pYo6R4Luh1zUjJ8m+WysiDY8CoC5Q638oMP9u+frz44h+zbAC1DOelKKOUlhQDqCNXvdkp3pRcZTh9BM/bqWI1ZrWLkr172QP4+7SvYbKbShQLuHMYt7uyj8phMu7Pbr4tkjORjdRMNjiqevamo1rNxTftpdjf9j48PNxbv4JMBSaNWKnaydVmZBsLPD42KPV9ZWdHAVE8G00+Ktgj+Wb5DDvgDUVKe/lYCpmzjlmtqtYeUuOzhPqg1I8hO2++Rc/bnORR3XVe1qjMe93Tco3TNdMUnH6sUikIiQWgmWsKe6dZvKo9sccRKYD4U9ysQ/VNztB9osQ/u7wIbszUUDAh1AV1zbOdcWBlE5ktzRKdTUbo0q95b1DDOffVjPAo2kze0+mad9kF0a32nop4nCum9MvufMCCR0iLx7blW3kGGfuXfd/LsZLiny0Nkg1z4XvV2THk0MB2K8zoZUZAdlqh7oIK9b08p9unbfnFim3uktGo1xod99TUqNKvfQrJ0TBaYC6z6ZgF8be6wdQysSOKCsUYO2Z7/tGoFWfB7hsydnTGdCn2kyR8bDMMqZbatW7GRftELf+6MQ4HZ7oGq2dTgTfTFPr4iRKX8eDkCgqabLcAs6faZL57wBQCrw2pMaVe7R0GbVfSaZ8zAAgstCPx0uLI3Ds6FjvlA5CKSyCLXTHgtWB4POsgsVLbnwS5/p52zQth41xbC/IxHor8Hso6rUM2fqYLmr9HIw7+Q7je0fyjdc5U72FJCwHmPlG0IZeqpB5f7Ewk4GFkL4NdDwYhmwzNzFXE296ppjXkZqYSnz9f5opSDApC4pYR7bBDa2xQzbS0yCkyineYZL8zjEdIutWrG7ZiGaYo6oWsVub78ef0Z/47nC5WvBYUitS9krYtxKNGGHzdgUY5/lb7oGlfs8Bka0R+tpk4xy3J/KCHKKx/pQp0f6+dI7JsYEwU5vfrcYBMJ+/4IRxTSWd92zQIXOGbvLZtqmk7ekoj7zbrKiKpyBz5EwbqNU7nyB0jxTFpn/VNglUrxyLwvuRXeSiCyQJLqHDIvBSsDVEe8YcmnH7wddNIa+AaNY24EQzd/SXo5zL8CmaMNTUDozlu2JbKFcPGU2sdqQb8GUknDY35lftQyiuWZs+j/2NxiYUhkeUSW8+xqbud9J4iTXv30OkOwuLhkm2nA/p6dhkbIl3yjhT6SHNjUe7wJwA7tGIJVoOgoIeVKCEE9Q9Ip8yhOws/GKcQOUmLx6XwS4NZ+mKv7a/+kvE6A9qmADBPhVmcbNxCFWFNDafRz8dhQl3taYcl+SeUAXb8dItQCNP7eXy7QXYIhJOWY75LLREehOhj47Er/NDQFmFkJolsy/lcMzVOZiHKl+iyPBSiVHPxWC/4ToDkgox4TpQU1myGJiE3fmzlKZZu/qyfCzrKs500yNKXeJBCTI/eEHWIbSVToLPLPNu/Sr13QDR3sZbrbmuyCbYft6jeD3YIRoWPhlyHWVe8KH8yidj/6YgP9AcG7kd/M1FEYiwH15tFZNl87Qg2V8rRXRXyjOmpk9UeptnRupysa3KnUP3d1ejSl3Hd7vYNoN/u3TcTKADZxBfIEF9DHnkN+tCgSY+BlonyCkaAgujoyd7o83AHgjcjy3IhlC5yDFvR6u8R7qsAUCkOO/VoWOBXMwGzQ7pURhMShtmrJHYtqSisBNOygbAZpsMKb+yt9sDSn3x8jGt6ED4btAY+n9jp0OOuwGWKsDlS8DTf7kLBl1qOQPVDgCzEnqskTujUPAnLxU8oxVoGK+EQCjonOXAAsgwHlIgp4b0SCp+9AHQ3Gm9p3Pvc3qvPInzENYDwls2seY70hcu3tvYK47KYu57/ibD4Maf38x9lBHXnLnfhRZ88onge77mkjqsLsQFPG5Xr7R+56yIRDgPQBMfJ0SLrHeDvq1Hw+AicXp/046Wyp5pnTsWugiK9pr6k8R6me6Oo7DeIzBqRF+m65brdYrZkJ0VjKm3rMyGnz5xS4q8lWUYEaymhFHGVb9PUX9kMubfEJp2l53UeY7QM/wqh5YfwNaub+pjQfkFp2dERHaiflydzN/hS+ciI0QgIFOj0Nh7fBJzNDmiPE4C6nF3Mjpmi2StgH4RafkszfJQESb19cei2ObYqhsbzm/r6/2ehW5V0PKXdmMQgQ6CfRiAEh5JMBAsxjW3p/CiQgwvf2A36taBFLJJsbgdfzYReI6KvgjQ/c5Cf0Q6PSJjyCpZ/fbhc6nCneiNwZXFQFYhpso8FDKm226pjF2m9jdLcSzLzWt3F362XjwK0urj/YHejERh72fV4ERrv093lEEuDzyR92M8RjZw2Zg8WJcCa0vjOsxE4d0OpTF8DGO0WRe/Ywdnnzh2TI5fQROAsYjQkPbaQ+1dpIuiVtgZayFg3QaPGZQIx3Bt1BYKv5o7mZOwJgknvINMKJmgplqZOZez88pq9j5y3jGPKz4N/zUBn7rdEQPgGO8YncQqZXdmaCHRecyECuaBChkiKQPfCbF/gMUTsZArNXDFTux7KUB/VCHMrVz86S8ZspB6NXOIaRjZJ6wTPOdP+cqOFsjyl1FbGWJ6OdWPI+CgSdJ/C3im/w3BAW6xcUzSt9q6RDoOqmJywTTsV8yTTLgaXWMxyQcG/LC6Xh1zzhifdrnAWAavHYRlME0g4iumBfRJe3Dqba9GlHuUTe0tsJ8jvN9ehO0y5YbtjxdO7jl246/vloQ4DpO++wyOuoBxncmfJxRmDdB6QVCUgdPRMDPfy8GAavc6evOYC0qeSu7IuWFZssxbKN299rxmOmMozoGIGNrkvwejiiXjN85XsLdFL93OIOMwmnep72EGFdmU/sav/YwQZ3Cq9gQp6EPZoNfdAnMQB98hFN0Eo/KvJPKGJVV7vMQ4GcE2mvGpsikiWQooGMBYhpt3VtuEDIgbqarmPosT7M1oNxFAS1u8MEXZUmpp3A4JJTy7FVIiIukPE/Q91JuBLhwv4teBORCYIpm4g0IHkIT2nS+pHKPqLr7oz87KR5SUaoKd0FglTvvbI94lfvcd63ZPwWjcsn+qhrZGlDuzcsCqr/zFMoxa08giZPcJVwonIPxDg+JMyC/W5MIMLKU/7wUjgCdIMh+SY6ZFL9ML9yHeSC9seVm4ov0hMK76Kom6X+b+cKmUY2ystlW/aYWbO5rhZ9CGUwyDLAIc22/jHGGmzo8GF/yCHgEsiMw0HjLtMeDnKYVvUvqtQoCuLljs7dW8BnVnlIRWA5405qKCm6xEirWgHJPRD6jZFoZgHVdH9ndBd71sQyo+y5qDQHLsNmu3HmHCg+EblSFzDShU6UpJF0Oqjrgq5qYvdeAcpeIck8yUXF8EmAhAC618PdYRHNwx9enb9kjUJsI2OChqHIPu5wKNo/39kMzd1JUde7QGu9gStZ6DSh3FXkQKl7lDgwH0Md5Anf4JBwOGn7XI5A7Apln7kkwBZ7LxBmzWUZcswzN/5EJY+43VElX1oByF/cTqg0YlMruEh/K4Yw7qhO/5/jG4Fv2CNQCAsubm/gydDOBdkxwTSWra/fS0EWlLMiHkdZWipSrslgDyj3EwvcJMNgNgojjobhBS23ojSlxdOLb9Aj0AATWNff4VoZ7dRXuQpgA1yMuw+XFHFIfR2ovFylXZbHKlXsT86WSHtRI1HZmj5doy+AUYab0tEzrQbzb6Zv2Ox6BEiGwjmknk/syKbPbRbBIe6HUe/PCXw5QS5e6h+5or8qVe9Q2JvHa28/BClAg25+VTDMOe85vPQIegewIUPfQjZk+5pkI2cKJ7ZV2ZMjeWlFntmMScjdZiFfuReFZksp1EdtYB9tZSXpJN9KGyOIt3k6f8zseAY9APghwrYyMmdn+hsLm1UTIiSGffnK91vXY8co9V9Tiu05WCLedYBK0OCVMB6rweZyd+bY9AjWMgDXJZFPuDoW3RoEUBXGK+/WwEPBMjGagOG+jve1qN8ss234r3Gv7LFwucUnSpP62Yfdtb4/5rUfAI9A1Asw/S3nFbKObsG5KGnqC6FWlK0fs7rOqfvYeBrB0QJWpJRVR7hKvcm/nn0jdn/LUrWV60L6b2kPAJuJoyXJrqSQe9mSdIRaz5dJvIxO1OhtgVfqeytRitSt36ydLuASYG7dyj36quYswZXpkvhuPQNUjwFR6nLmT1z5b7gU3UBBoCwU1xQEAyctcoSdeVUuVK3dx/VFnAKPdqLY4HkxYuavQCnsc/fk2PQK1iADzylJ5P5Fmg+x4l2HlXh+3clffhoeQJKlZVUsVK/cm0hXb8GU+hLhn7ezD8anXpKR+5l7VP38/+G5CgBQeFJf90RxKb8LMjK1xK/e2iHJPLJYeSZXuVLFyT3DBw3I+E/5yKPfwzH2ez7JTpb97P+zuRWAr031nyj08cw+n34th9Ilw0BTEz9xjQDnHJpORxVTlujLl2EaelwkWdWrMQRDK9+ic8rseAY9AFgT4NzTCJOh4Kcs1POxO3Fhu7eTaEpxKRpW7n7mXANUCm5DIarbEr9wBd5El8hlX4G34ah6BnoUAMyvR5PIf+i53cuuptHv2ggUQ9p6xx0u27R1ZUE24Jt+S9VLOhqrYLFMXAV99VQbg3E+1yJu+DL37LjwC1Y/A7uYWqNw7k7BynxO3ct+SrpBOVKys2NngquFcFSt3Ye5FR5JfO4XS705CP6gQM105Xialvw/fokeg+xCgQ8J2hk/m8S6GQb4XV1yTqHu8RPuKEbDuup1X7iVCtoBmVES5J+JV7rMR4bHBRwUM2lfxCPRkBHYwLpAPAl0GJYWDihIoR8SomwtiYWCqTZpdlc/Mz9xzfWzJDlnRoxzQubbkr/MI9FQE9jE33pVJhpeFlXuyLMo9kn+5deNqflBVrNxVZEE18mMo/VNh4IUr77kFv+8R8Ah0igDNHDsbet97Or2SJxWikyfSA8cs8lSkg2GRclUVq1i5h7xlZgGNYf7n0j8GG3hhW2Z0nRePgEcgNwQOMe6NN+Tk1pjAa5Fm14+UYyjWR5S7crOuxdBfvE1WsXLHEg408drbr0IvCNyZ+0cIMiYYcIbkdz0CHgGDAH3WDzOuj1fnhEobmGZvlnNtGZT7MJplXBfnzYA3w5GyzoAqfbdKlfsL9Hl13RLjVe7TsW0kA5OftVf6L9uPr5IQGA2APFCPAHAXLbOPMZUk2w1yWh0BwvkUstcu8IwSAJOdyosAMyx7pXO4OnarVLnPoluUS+ZPdrn4RHBAqHGF/4bKvuAR8Ah0hsAR5uR1nV2U4Vxz6JhCY6gcS0Gif9u7xtJNGRqtUuXeFs2E/mNsWAU6Me8Yp/0fILjXKftdj4BHIDsCGwLY0czYo4oze63UmabQBYLtQ+VYCnUPRyJndwLEnUjG0mscjVapcg9xvHASH59yV9pWSP5pK/9CgDm24LceAY9ApwhMNF/Z5+bg2x5uaBE8DaDdUUKwMy6NO5fqcNIQPOsMZAWgZahTrprdGlHuwgzqpZfUQurxoYYTuDFU9gWPgEcgGwJDjPsj3Rqvz3ZR1uMnYDaAB9LnGSH+PbZOl+PbuT3StPXPjxyu7GKVKneJmGVimrlPx94A3CTcz2AcnqzsR+pH5xGoGARONyO5IO9Zu70FhX/ZXb0V7BEqx1Jo+zcANyH37oBUna6sugGnnmVdlGfih5I/4ztRB8EZkXYnRcq+6BHwCGRGYF0AXKuiJ1v+s3bbZl89c3ddIseAX9SxyqjPgdAkblmguep83qtUuSejnA+lV+5vY38Aa6R/Q4K3AXQdWZeu4Hc8Aj0aAc7WqV/+HPFXzw+Uk0ACsYecSgMxHSOdcly7UdMMv+KrSqpUuatwRiQkS7ugmpq1nxZ6kgoTkfK9DR32BY+AR6ADAqT1Jfvj6wAu6XA23wMdTTP75ttE/tcnyH/jmGbUbtVmmqlS5S59Iw+rfUU9cqKg4tvgj6d91p7K0H5nQW35Sh6BnoXAAgA4a6fQGaGzhBzmsi42Kdfjmc5Ve+D8EP22c6pUu8O/BJSbBnAZoGnzUrVejnaqVLknIvkV1dySgZXykBkfas/P2kNw+IJHoBME/ghoBlWaMN1oz06qdHEq0O6Q7uSqL+ZqZ4cuKhZ7OhlezEWC/vpVI1Wq3CWq3Evndz4dYwGs6jzB1yG42yn7XY+ARyAzAuRcP8VkNOK2dJJAOLo1qeNPStd+xpYS/weAlARW3GBGe6xit/UVO7LOBxZR7m1OeqzOK3Z5VnBs6BqFCRjv2t6cs3/GwpiNswFsBGjei4VMfkj+IC5GoM+1VwjwVwAHth/Qn6xcuPkDgrgTADu9+l2PQDwI7ASA2ZboVVZaSuxxeBoB3gKwjh66wiYIsD6CDuyRJbyzhk+BZvY52DQ6GJi6ArBVbvw4JRxJIU3Vysy9NGaZ8SB/86YOkK9jfCez9lMwEwGOQx0Og+ioWXLM05Nnxw6KnY0ujZMg+kWxIBQuBLA2AhzjFbuDuN+tZgQ4UeHs9tRYbkIhyih5eCz9hBu9P1xsjVJ/h09XUKlKlbuKzNxLRAegQrNqUpNdldOzOhPToNDuXaOwZsZ60/EbKJ35fQeMxzkIEC/hWcZB+IMegdgQ4KIneZeKX0TNNETBbaEk1oJ94qcjSE4JD0VVzaJqlSp3rSAdzNvCmdKdMznvBtonl5liUiL4BYJ/2GKX23W0TTD1KSp6hh4mG5qAvSAYhwQaECDMdtdl4/4Cj4BHAIEOiLovjYTCYvgOu6TLsezUPxdp1v2yj5yqrGK1KveIjX2B6Ew+f5QFjEBbKl1R4UGkVunThzrdGYs2qLQL2GBMdDgwAmwLwV+07+84vNxpO/6kR8AjkB2BjqYZSyecvU5RZzSR2AdOExsAOp+Ec6gyd6tUuQuj1hxp6+cUCtvljNoVhfYZgnu8s/3UTP9LfYmAbHhAAPJBXwroGXs0dVhnrflzHgGPQBSBtfEYgM+cw404G8zPGqOoN5zGewE/xpw0xOmtiN0qVe4qotyTxSt3weohHCVE+xk6lbUQgF8U15jzQxHgLOM5MAYBPspaz5/wCHgEckOAX8gImUsTaMPBuVUu9CqJeMckli+0pXLWq1LlrvkmHJykeOUOrOY02IaBOoejcyjH3V640ln0ORZ12B5Bid3CchyKv8wjUJMI1IFJttv9zwWHgJQh8Yn7pUB6eq/c48PaIfDXndQtVoK+3Af2BY5FYe6Vp4NmGZupaWG0ab/fEgzPN+ER8AhoBM4EbeCuU8KKeAs7xIeOfBFuW+jyXPFSpTN35S5w8CVugwyKAdwN6Co84nUitgLA1GJsgx4zfypmUL6uR8AjkAEBpWfv7okYF1YTP7kdASrKbRU+XSGlKlXube4CR6mUu+u66LDB5fGkAmyBpP7RMdejDbjYE4GJqsujKX+pR8Aj0AkCgrsg+Na5YjQCLOeUS7kbmewpN+1mKfspaVtVqtwXIbe6o4DVeiVAxVXu7fa8XBs+Sy/I0i9+bwR4H/W4yGSfIcbH5dqMv84j4BHIAQHmMQ7P3uugEFM6PGG6P1fIfFnxUqXKfQgzs7zjoLsu8Fixdnf37UyOmNzlLKyKNjCz++EI8IKueAaYN/IW08hBMc4qch+nv9IjUFsIcGG1XQRxsTa6JltaCvKf/LWPsmx7VarcNT6POyglgPpis7N85bS3JETby51DWXYDDEUbyPtMjhh3kYd5aMhrzR8Cg6zi4dvIMix/2CNQ8wikiMTed+5zGM5FsZM8pzm7q5awe2Y7I1KuyGIVK3cVVqRAY5EIu8q9F87DwC7bC3S6rwdA5shAB1eEq4zDmwCeMAcPQeBEwIav9CWPgEegMATcFHx1mK8jzQtrKWstWTxyqio4oapYufeicncJisYUmQYrFVlqn+K8TqLe6FM7AUxK8DCARzG+E4IxhctMk1yEOdc277ceAY9ACRBQka9lwQYlaDXaRNT10Sv3KEKlLW/xHRCavS8LtGxZcB9Kz7Ld6nRnDAtt6xPwB7yFVyDa5EJb3BwEyOwaNRHrQXTyD9vOwZiAMzEB29gDfusR8AgUgYB04Grq+HdbRPOm6rrhJuTDcLkyS5GFgsocZPZRJW8H1CjnPDOUu3kPnVNd7IpO5uteFHmgAFqxNBLoDYV/OhfOQy8wQpaLvGFRmo6AiXYfTZ+gBV5hGW3TV06UXfoCv+MR8AjkgQBpPZhDOeUEIbG4HbsvDDpeuM4ceQy1vJe67n/l7bkkvT05CJjP6DEbevwF0LA8oBw3yRw7CrSNnZ9bFpNnEKBquJtzvEt/mUeg9hBIZWOy7tA/INAJc0p0n00LAepH0D0iJS8DI35VosZjbcYOONZO4mt8y68BtDjtLwNMYTal/CXAdwDc4Cim8Vo0/4Z8DY+AR6DMCLjOEIsiQCn90Ic6ip239WqZ763g7qpcuev7ZmovR4SmmULFfVEwKIJUAl48Ah6BykYg7JpYX1J3yK0jtx7JzBQ5W0HFGlDurQwecr1mdi3Ya0ZhcujZiOZiDx3yBY+AR6DiEIgk70EJ1xITI8J3G3LiCJ+qsFINKPet+Unmvk2XBpoLS2IreBhMr9cuuyOIpvRrP+n3PAIegYpAIBwx2ppeNytycI/2B2Rjp5EPgYaq8JThmGtAufM2VMQ0owrjmAgwC0r7rtvnuShUJEOTPeO3HgGPQKUgENZj9aXyQutNOgP3K8CNiq+Ue886jjAoWS+r9BPJO4EQ//pY4IHC8qoqhF8UYT/1SgfCj88j0BMR6B+66VbQu6UEIntGGvl3pFzRxRpR7o0/mGhRC/aiQN9tbSGvreD+kGlGsBuC0Ns7r+b8xR4Bj0DsCAxweqAbdIR/3Tmb8+4LDEx0dchPwKzwmlzObXXPhTWi3Ame3BaBsBjTzCPptpReeS/Mhp9uxO94BDwCMSIwyGn7RwQuHbhzJq/dmWOAUOT5A8DowrKz5dVv6S6uIeU+j6ntGKlmZQzAAIQCROGuSK09ImVf9Ah4BCoBgUCvG67kDKVEiejVAU6bnDzeES5XfqmGlPt29HJxbWL9gMTuBT0C0TlQXYJ+ZlNyF1YKatZX8gh4BEqMQD2WBUJBS5EUnIX09zjbZDY1K/TIu88WqmVbQ8qdkOuFVRf7wpR7oL8AyPhohZSf3jRj0fBbj0ClIJDEKpGh/C9SLqBYzy91VzfeDTS2FtBQt1Zxb6BbB1KazhehrZyLq0ZkG0AvjNgD+WzvjlzsTTMRQHzRI9DtCEgH5V4CP3Q5NHxfcnO4XB2lGlPuQ+YD2qRi0e8L/OJ+XtnjXW/74H6TA9VeuytS9j1b9luPgEeguxEQrBoagkKRyr1lCwDrO22+CjQ+45SrZrfGlDtxVxHbmBSWV/FUfA/ADVpYGgBJhLx4BDwClYNA2Fxah7eKG5ocEql/U6RcNcUaVO51DwKaR90+hGJyq0ZNM7vYRv3WI+AR6GYEzgeDl9wJ1/s4A58WPirtXbeXU38ekKhKkwzvoQaV+7CZgHreeUArAU2uq5RzqovdXtrE43LD79pFDX/aI+ARKBcCc7EvgF5Od+1JcZyDue8m9gOwsHP93cDwMOOkc7LSd2tQuRNycal7+Q5rKOhBnA66QD3t1F0DQSyZXpwu/K5HwCOQEwKCI0LXJXBLqJx3QX4TrqKuDZerq1Sryj2Sak82K+Kx3BOqqxDlmwid9gWPgEegDAhM1LkW3PR3b2Icniy85xZmchri1P8QGN7slKtut0aVe92L4Seh1giX8yoxMKqdUlRQTDKQvDr2F3sEPAJZEEjilNAZhatC5bwL8rtIlRsKStcZaaQ7izWq3LWdzPV3X61gkAMwKOIpp/5aCEJveOeU3/UIeARiR2AiyLHuesFNh+CawvslbzsOdOrPA6SI9pyWunG3RpW7RtT1d10OeMFdeMkXclIKuzLWLfh9j4BHoIwIJPHHUG8KVyLAnNCxvAq9uDDr8FCpe4HG6Xk1UYEX17Jy/9LBuw6YGeZ8dk52udsLJA1yU/ntBSlVtpcue/cXeAQ8AhaBiRgMwHVX/Bm98Dd7usDt78P12q4Il6uzVMvK3U2XR96ZfgU/opTXjBvQtAIm+OTZBePpK3oECkUgiQkRF+6rcBq+LbQ5YDI96biYauUNYKT7t26PV922hpW7zAo/jXqS7xcuCrdGKkf4JyJnfdEj4BEoLQITsREAlwyQFN9/Ka4TFZm1S5ELs8WNppS1a1i5J1zKXtISLFgUcAJGq7p88aQBXqSoNn1lj4BHIHcEkjiHf8hOhcsQ4GunnOfuk4MA5QYmzgLqi/SVz3MIMV5ew8pdIvzrbfOKwjFFA+wurPaFQmHZnooaiK/sEeiBCEzAdgB2SN+56DypF6TLBe3MYxCUm2v5n8BW5JSqCalh5R56aDTTlSJF1nWhpy7wppkQIL7gEYgBASbKEVwYafkvCPBd5FgexTvrABWOcEXy73k0UPGX9iDl3lrczJ2PMtD+7q86T3VTpOyAziG/6xHwCJQYgSMB7SVjm50GhSJn7YuTCnx52yCA54CR7t+2c6o6d2tZuUds7FKEH2zo4d4YKiX97D2Ehy94BEqJwLk6QX0QalLhVAQh5tfQ6dwKiaPC10lNuD+691TLyp2p8awIsGgRn3C2GQC9QQpQ18SzPy5C5EXiXO93PQIegcIRmIczAQxwGpiC8dq5wTmU727TWh1zpA6KesPl22jFXV/Lyn0xB+2ZgM7S5BwqcDflU/tfp/ai+Amru0OVAAAgAElEQVS7OWW/6xHwCJQCgbOwJgCaZKwkkcDxtlD4VrFN1+vmOmBw8WbbwgcUS81aVu7uzL2IIIcMuCuEF1aBwzJc5Q95BDwCxSDQpu3qLm3ITRiHl4tpEniG7ssHO220AupKp1wzuzWq3JvoBrmo85RKq9zXxmMAPnPaH4GzsaJT9rseAY9AMQgEGAFgJ6eJWajHOKdc4O5cui+7CTkeABqKyN5U4DDKUK1GlXuCq+DuvbmKuHhYx2qemX84DSXQimjuRee03/UIeARyRuBO1AEdvGEuLC6Fnu1dIgup6nJ7pta2rgKsoXtLhjOiQ70fw81dH+J556deEHqhxNClb9Ij0AMQeAukBCCtr5Uv0A9/toXCt5NHdeSRaXik8PYqu2aNKvdERLkLOdlLKyme9yanUZplRjtlv+sR8Ajki0CApSA4O1LtJJyECBFg5IqcilH3x9q0tVsoalS5yyr2BlNb9UG4XKKSQjTH4tElatk34xHoqQhcCAWXnvtxBB1I+wrApmklAGOcij8BdTc55ZrbrVHljohyT5Z+5s6fguCuyMLqdgiwbs39SvwNeQTKgcAEnV2JiTOszEEdfmsLxW011QBt+UbkZmDYTFuqxW2tKnfXLNMKLPxxLA8vQCvQIb1XiX6MsYzYN+oRqEwEGAgouCQyuPNwJkrw1d20AIDDnbYFSNTsQqq9z1pV7u7M/dOSBTBZ1MLbq4FQKPRBngo4DJAveQS6ROAnnArAnZS9A+D8LuvldEGCmZvcuJcmoOHtnKpW8UU1qNwfY2SqY7NT8Zhk7EMPwFyL99ii8aF1Py2dU37XI+AR6IDAWVqpnxg6rnBC8fwxtkU51u6ltrXr/ujeZw0q997u25+GcTdRtnvvpdtPdMjheLx3iywdvL6lGkegDZcBIX6muzEeD5bmrifTpfJXTlufA/3udco1u1uDyl3cfIh8cNNif3rjMBXAi04/a0JhF6fsdz0CHoFMCEzQvEztSTgALnKWgD/GdhZ1f8TVMZtpbcfdvo1kK4plPExMvSzw/+2dB3hc1dGGvyv3Ru81VIOdBAgOHUum/PQWSmihE0roCSU0r00NJaGEDgkdQwKEmoDBkiimQyimQ+jGgCnG2Ma2dv7nPXvO6mq9klVW0ko+8zzS7eeeO7s795yZb77R4pIW85mjAyXBGcGx3p7ExzzbIrVPIfEB10rAkjJaE/1fQem8Yv21NRruzXYURzO1HG/P39t0gqS789txJWogaqChBs7V/JrhRu3p/RllGlB7pI+1cL0aP3u6WtpMyYiRzRNSKuO+lKTVJUGlCRRwqDfmGHUMeKnkO0mfSZok6UVJBEVe93/f+ptQRDclPTmv/WWI7tQE/U+JVvA3W08ZDVdGj7X/zeMdoga6oAZm6Hw/8Audf0XSJWGj7cuKQyQDKRNkjDSCGNk8IWnay+Y+MCPwjSSNkDTMG/RUALNBM9QjZNSNMQ5GeZYflTM6Z5Se5kbng4AbnbqG/f2S+/GSYNTPMk36k74Z93ld2mljae3e0s8lzTdRquLF0zEySofJlC7V9R9lUnUfO6YX8S5RA+WvAWqimv6T6ih2YZgywsCXQJ7vJU39QFLq91+xtjS8YwZ7JXiCtjbRHONOgHJjScMlbShp1YKbTvUj6FclTZD0mqS3JYciKVX1o/QtMfoYeWYKzBKw4swU4H5O0YPyaEt8L00kCw2fOCNoXgDtJ+drgH7QR5IW8jfB1bRm6b6w7df12HLUQIdpIOMGbhjxNPjhAmV0fOn6ULOrpFRB++QZqXK90rVf/i3NzS2DQa9JEdtnJeHDxlg+kas7KN6OGLGOEkb7BEn5SxfN6C2deJjU+6JcF/nufMAoHxY4/ugjQU+u4a/0vnj4LzJu5H6qVwZvGEqE/aqjlBPvEzXQBTQwusCwQ+w3ssT9LqQCAZEzT8ncRu7LSRrlR7wY8ycl4fcuU6mhrynO57cOkQ75yruQtpHy/nD6T9YqkChG9s+V7IEyLlkCbH1wH5kq9Eud3gBNU7LbxYaiBrqUBjJaR3KF5gMVAIOuTZRxg8gSPcq4NaSK/6Ya+1xadPnuWG0p9YxzrM4NComLAZ7ykyU9WN6G3T3bmg2fcFVcMXdJOlJyfDO4cBhVP+srn/N2Z51hPvCrRRte34qtjL5SogtTVybK6pzUdlyNGpg3NZARMTUGU8Gwo4fLSmvYabKicNR+9bxm2J0Wutm3DOMdZLr0ZSHGnbjAWZLWlbSMd9dg3MHG/8WTgPEy2K6h/z402cxljiMjoHe4aHONdvGKZjYQT4sa6JYaAB5MbCzIp+rrBlthuwTLaiqwpeGPlNErZG8twX3Kv4m5jdzL/wnyPXyMUXe61N2r0m51+cNzrhBcBdmCoScw+2dJGGSKXeOTh7DouJR7Zc4WGtuTce2kR+9SVpcqo7nFOBprMe6PGujaGsg44AMegHpJdJhOKrWbN8HTkIZf/6u7ltGrV2TxtW5k3OtA8qRiCMn44o9cdC8on9/70fyOksZ6tw0GGt/82UBvil7Z2M5+juEujaldS4mObez0uD9qoNtqIOMSFW/y0ObwmGM00sW8wnYJlnfg7jm6YUPZixpuzztb3ci4J+sXfGxPFWw3ZxOs7T2S/s+X+RrjR+4w1sFRw0gf3P3c5UR9r6QgjdqUUY4kae7XxzOiBrqPBkCM1btMTRSsLzDCpXjYxQFNpGbvyQvSJoBA5knpRsZdpTDu6S8ByQ747sD1/1U4VuRqOwLbApUT0DDpaxquj3R0BA+kdvZXnW5QrgBwandcjRrophoY7X6X+NrrpUK/U8bRitTvK8maFQZS0wmFJblDV2qkmxj3CfDTkC0b5JMS+tkYsYO2ARZKbUd0BtwSnzyjD+7duJgOl0SiV5ANNaGUyRqh2biMGigzDZCslNUNBeiY25Ub9JS4s7UkNW6WanSy1Ou21PY8t9pNjPuXkIVBWxCkNS6ZcG1jS6aSp/mR/HU+CxV/HkUFGmeAHOUyVvHn10uiURqtAoKz+sNxLWqgm2jgT5JWST3LRPV2CYWpXaVazZKomIq52bXSBs0gGizV/cuvnW5i3Evukmnqk/pU0kGOViCH/YcojGId1FNdsuiFI10pvrR7preyDnpZ9PS4M2qgy2sg40bRGNx6SXSwTnb+9vp9JVkbO7+U7Jtqqk7qOU+7ZNBFNzXu1h4j99R3x63CoUMAZ1NPhbCLZ6c8dA69Jo76gBfC16lGRiijLVPbcTVqoHtoACpf6W8NR9K6ViOVHuCU8Fl7YdihEQ9yn7QxCZjztHQX4w5WPciP0vSXwkYHLMdJzsXCFJQv2BWepAwfYL1Qji9xkMr6fdI5sWJTWh1xvVtoYIaDAS+bfxbT/9TP5Yzkd5VuxXDFFBSlT/gNzvPSDYz7ozBEBg51PtAXpa3TNMId8SHj2ztJcrwZvFjA3D8v6YAGNzdRcf3j1L41lWiL1HZcjRro2hrIiDyRfVIPkVUP7S+gwe0iNcych6SafksaTp7KPC/dwLj32KTgU4TFsrMEww4xEoaehAoCr7fkYZMZzRDB1LRYewWY0jeJ61EDHaCBkQ5Rxnc+FdjURTpdte1394rCknwXS0lHstS236O1seVuYNytsqEOks407nSF0oC4aKgIBZfNnp7nngInEA9j7GGqDLKVcj+KsB2XUQNdTwNQayS6NVXLgGd4VfOVmjsmrZrHV5Vs69Se7yQjEzbKHIG/LqmShIpQQWZKA6AmLgehBCDuGb7w+B8fkXSsMq7yFKObILxgeQFEiRroyhogC5XvexDqLuyu49SOcMQ6fO2pWULyd2lEOqck9GWeXHbxkfujpBqvmPrknpeG8aUqF8HPuJekAyVBYgY52S36zI3e6/uYaO/6jbgWNdDFNDDKIcag6KiXREcp4+ob1+8r6dpDkIOlY1pZaTaZ5FG8Brq4ce9Z4JIpJeF/Sb8jwMJ+6bNa99DVulpTGlSCGhqTmkqq79hYR2kgo8VkurnAC3CbRrp4Uzv2oi8DIuh9gzwkbUrWeBSvgS5u3LOUAUxJluIc5SqvewMPdHI9/VUriHSoIFk3wg9bcRk1UP4ayLg8GXzcacZUuJfI9WhnscJ7RPhjgca7uHFPqlLPM0tKyp0B7htJBIBu00zN51g3KCWek/305wYUCmF/XEYNlKsGqIUKg2qQmY5sL6MpYUf7LKtJ/ktXXXtfqmynBKn2eYKOaLULF494lMrp6erpz3eRYAoYfPzw72imTne8keS2LqJFdZsgOoIDntHPP6nw3RFfgniPqIEWa2CUdpK58pv1lyY6SSNdfkf9vnZZq8jk6t2HxpNLpATW1igpDXThkXuPwuQf0ChdRcDhjtRgneeICe5wlSSltxwBGQiA8yUxpqcSVJSogfLSADS+OUhvCqmif+h0dUBhjOptJUtnpH8m9bq6vBRUHr3pwsa9MLMz+1B5qLQFvXhLJ2p1feUM/JyX9ZJcoe3d5jwU90QNdJIGztAqyrqCNmkWVniWDlCOQ6kdO/b0fFJSiIg5d15nf2xM4V3UuDv+9jS+/Vspeaaxhyzj/b31hvrMpX8NM1rncnI8HDXQbhrIaE3VCdAC9YqDkJC3kzINahaEYyVeTgdKnKq0BJWHXVPim3Sb5rqocf9ig3xKv/sokkelEWSGdjVZXdm5VnRaTdLCXe3BYn+7mQYyguYDGoF6ZIzpB8eMmnExonZ+4OodpYR8kSAm2W+lETPCjrhsqIEuatwrCvzt1vVcMrnPoXkB7bUbjJQafoJxK2qgvTWQ0e7K1S6YL3WraUr0K2X0bGpfO63WriUlhbQCV0gj/tNON+wWzTbPuJTdo1qBca97uOy62LwOQVFARm3/Rk+HGXtbPa/tdKsS3akFVaOjHIVBo5fEA1EDJdEAtX5fdzTVxzdM8xeQ3m2V0fiS3KfJRmB9tfsK+NrfkAbSpyhNaCAd7W7itHI69MRS0uxPUl+2CVLVT8uphy3sy3lSEzVVKfvd8FUGpcFDSjROplqN1BvtH8hq4RPF07u+Bs7WwprpoLmbFzzMRFVoC53uSPEKDpV6s3oJKcHHny7V94VUt4606Yelvlt3a68Ljtxnw9+ceiklXZm7GcKwaz1e/1dFv1wvS1rb4eDD4UGSdpEJdLw0Sl8qo2pn7Cs0TqfpnXBiXEYNtEoDo7WWZuouST8puH6Cemg7nSaKxrezPDFImn1PgWEnUXGPaNibp/qUkWzeBZ1/VvXNUkISkBfbqov63ghQ8WIiYYmZB+Nzihws6BOZrtMCOlbfanvH3nGQEvUOz9zk8hVJTGPvdQVDMorJHU2qKx5soIFR2lcm4IbpsnWcQo3gAzoGFQPk8ccHJUuzTBJAPUAacX2D/saNRjXQxYy7VUi1EyUt5p9ohjRw4TJjgmxU2QUHQjGP2yX9u+BY2GRmhZHeSovqSf1On8i0mZJmo2c+lOly9dF17VOYOHQzLru8Bv6kQZouikoXMpTWuUzUjM7vGPcfhn0GgVIckmk5Wqq6JL0jrjetgS5m3GvAtkO8FeRhqaqhRzoc6T5L3DBg+KnJeoIyLrGJ8nybOkMvUQSk8YBsTg9wat+iHjq7Y6bU3Uf588STjNbayjr/etq3TYb/ZFVoD410M8wOUEWjhv0UqersDuhAt7pFFzPu1dcWYF0Pl6q6EhscdAL3e2qBlnyRcNs8LamvG8Xn3Dm56zPOWbOe5HDIvPww9o1BXOG1udhRH1DyL8q8rQHQMG/oOJnOlOZw+j3lishkOorfyBl2IM18l1OSnCRVUtksSgs10IWMe/VAKQElAzgQmSnNXkrabLLfLvfF9pJL24b69+e+eEdL+gzWGGKxL3yIFV3MKWdpcc3WjjJHTrbxnCe4Pa+rh3bRaQKKGWVe1EDGBUtvlFT4HSFGgzE9XRlXMrIDtDN2fqkXrpgCw64TpSrQZFFaoYEuZNxrDvf0WuEx75Kqdg4bXWCJrv/iDXRrqRKulASxGCOcrVxF1qYePOOqwsN7TWZfoesGSCUBMtgno8xLGhil3yirS5XkB0rh6WEk/Y0yriRk2NfOy/H9pJnkqeRqDNffLRr2el20aq2LGHdLpNoJ3u/sH9RGSCM6uxh2q5TehovgoeHFsIYviHBVs9o6U0trtnux7FpwPuyUF0g6ueNGaQU9iJsdp4FztKB+FG7MXxe5KcitvZVxM8Mih9tj1x09pMXvlGyHgtZPkKpgRo3SBg10EeNeu71kYF6DvCJVYeDKWfB7EwQibZoXU6mEwOoLftS+Vov89xmHhODHXQhzq3bFjDv0h10qdcR2mqWBjHaUdKmkZQrOn6FEJ8t0sToUNusGbFD1HlTQnzhiL1BIaze7iHGvocISZGFeIBCqpC5pOQujZJjaX/I+ckbJpRJSr/FFEvTCZ0rx7ebJaA1V1rliICRLyyeq0C463c0M0vvjelfWwNlaVDOdUS82Wn9RFdpHp5d08NFMbdUc6wvGp88/T6o6Mb0jrrdeA2Cty1xqSfY5NdXJD6WBB0lXl3tyDoFTECkZyXFxpB6hzasgZwIyBm6a5pcXrNaXqnLFi5eSxMg/yHwy7acRmqmaFrQXro7L8tKAKVGi32q27lPicpzT/ZupRKdoiPbX4ZqUPtAx6zVQGhDMTQ0u4WmvisVpSvgBpJRbwlZL2lTNgz546Fu146UR+InndWHk/aJXAgQFLUO+8OMf7WBw50oqpKEgC/AwRbhk1/yO5WIsl0muslfhM7yqCh3ceTO06kWkBFINBhdeklppwObSsFlhT1y2XQON4aHb3nJJWqimCC7FcIN8K/VqXhAxXNFxSzDouGHW6aBbvunganJFtVte3oyqOSNdQhRcPaAk0rKfc/mc0aBGbfp4XC9HDVylXhqlEzVLbxUx7BhOCloP6zzDjtKSCxsadk2UsrtHw176L1SZj9xrwHWD7w5ytlR1StgosyVp2wRPcZHgBy+lj72xR8Wtxugd3Dzl+OD/aLmcpSU1y72YCuFoX3k/PEUaopSzBkarUllHHzCkSDf5juyvjOAd6kSp4XfBdynYnTop2USqhPkxSok1EJRc4mZL0Vz1ylLC6DTEBaZLs1eQNusEH2GznwcsPrhxEo06SgIlw8ceKkp1nJZLjrv7NEn8pWd0dc4/O9IltrS83XhF+2pgpJZTIlww2xa50VRJv9dIXdMxvDBFetBgVyEwAnhu9LM3UFEJN8rYuNdeIRkJOEGukKownlHm1ADuINA51FslgNt6GaW9ZI6GGDdTWi7SEP1Bu7UAmZO+Oq6XVgO4YD7XocpqZCNEcg+oh44sHy4hF0RNF9X5QrKVpBFTpeozpQrvfjUoMhigfCvZZ9Kg4xt32dT80dNuoNuZOfdicq5UGWmvU9Oj0n7x2tzaI4tLPT/wXCq0VifZatII6HHLRRaQdLc3qJ2dTLWix9IDiSTQWpyaoLmaG611lXV83qmgl7v4bs2nvXScICKL0lkayGHWoQhYtUgXPlKiYzTSfTeLHO6sXTWwm26XuvvvpSoKXuPBrJAeW14yKAjCM50u9b5A2qCJ79rz/aWp2IRFpYS40yVSJTPYKAXT7zJSSA8wsKmRo40pM8OOruCKqZL0hzJQ3PuS+HIPcNmmbe1QDuv+S2mO+pg7aYoeVkYLtfUW8fpWaGC0NlbG+awZVAQjGBoiYHqeBmhI+Rn2x+lr2m00WRoIlYaXJCtV/k/Knhz2SFqxacPOmVMJ/PeVkkqp8vho2FPaK8+Re/UCUkIJrVCM16RkDany1YZdL4stiobwQwNr3tnCTIIKORj4wX69bX3KuBcslaJSxVFcqPgN9dJWOlWx1FnbNDz3q4GsjnI8QrggCgPe4fpqVejojil9F27ZkmUNCJk0hv18qeqEOVtwWavkhzD7/FbqvVTjBr52uGS3Scm2UiWJglEKNJAOnBUc6qzNZP+UYacTY8vIsBfq65YyMezo6VufXt5Lgk2vBALOfaR+IznIZH2DiVbXbI1XRkBVo7SHBjJaShkdoVEODfVAI4b9TSXaXhltUr6GvZoZOCPsIFmprhE4c0K1JV4EyALSLMpQFpFxQyW7QUq2jIa9iHr8rjILqD7fS5r6nqRl67uc3Uza5NH67U5bw/9MQhUjjnRgqNM6VOTG6dE7U2HiFqWRjI7xRj79goNZchdlylYfpXn2jmolR8MLBwz1calElNZ1uhdf+sD51eVP+DZuZ6kizTx6v1SV9r2nn4vKkPjRP5KoNpY8I1UW0AA/vqBU94jE97Hq8YKL42ZKA419eVKndOTqD9QQTRl2jS8Tw44S/s+zMTKSLVdh9E4pMkbvRaa9beh2xvn095AEmiEIVaLuV0YHhB1x2QINZLSaRulgZXSTMs7FhVsNWmhqhxb7bVJi8nj100rK6PLyN+zooqKgbJ9d17SGhk2Tkmty59i6Um2KUwrDXweBYCYa9qa1yNEyGrk7f9trkuMg9z23XaUR6bf+3J+ofc9gxEG909nte5s2tb6w97dj4FeS9FmbWiu8OOOCyMQZmCWkhZfK77uGwUl3u4PWc5QA0ESEv2GSFm/m3cn3oILW9V2LEsJVVyIvJYAjpki2uDRiLlXAnlhKms2ss5eU3CNV7ig92Efq/6CU3CpVzuUF0UytdvPTysi4j9tCqgAKFeRtqXJ1iUh6pwn86emRaqd1pIU3hgsbFA/p5iNbeO3cT8+Isn+4qNKzLK6rUU/trVP16dwb6cZnZEB6uHgEMQnI2TDkS7TwiTFuFE8fo4z+28Jry+T02n0lg6coyI1S1b5ho+llzV2SdsrBoLNDpQq+y59IVb9v+rp4NGigjIx7DSPiFI9MclAnv6FJ6b9PEolTBLS6kiwnCXjk194Al/4FlRuJgl3+RQPF5IoqH6KRurPB/u64kXHc6ENEgNm8QTetUaTCUXOenmzSx5WoWolqdJqeL4+s0uZ0vbFzaigAsln90WQLqbKZ8SrYYC3E2r6RknHS8N06ebBX/yhdYK1MjHv1alICBCr05wtp2nLS1qU3Ss3/UOBqudVzTpfWf938PrTlTHyTYPGBMfIcpZeMK93HFDnN/xPuw8j+eGXE59q1JaMllGioTEMl98fMBQ6XQtdUS57zK0nPyfSEM+bkFHRYzdKWdLO15zrXCglFIXYwSfpiaWm3ZtYecG5arl86VxOh94aNwyJb28fufV0h1WsnPW1ycMqwA6T+eycbdvRASj9pzF0VQwvfCMb9d+1o3KfJtKdGa7zMFQ8JvlX0t7ULQmd0syr05/KF6qW+8rnkLGZsGO6cATe3XLiNNHDkQUDe9ZxPDMOQM7PqxjLrV1ISDDs/77ubb9hRywv9JC2WU1Ayl0zVbqzGNjxaGCm3oYm2XuoCJfhoCQQis6UeK0kbA4fqaCFxakpH37Sd7sdnC8c7CU34fCnN136S88OD+8c4FgoMmY8q0RiZ7lFGjFo7TzLqqQoNVlY/8wgo+sxfYQm6lvaR5wTxQsLdq0r0iir0igbr3XmPk6eQJMxlkbaA/bG6Skoo/zhL6rmwtBGw2ygt0EAZGPfaX0vQC+TlQalqm/xWx61USo5PhZqOoEG6gxBUJbgK0gKcevsKZFYT3UyBIG5jLgum5c/7GRGzotfUQ5NVoSmape+UKWG2L/35SotolnOlYLypu8uIHPcKwfLWCkac7FxenhOU6E2ZJqifJuhERSOk6p9ICTOTYF8+kSqXb5m/vAYqgrMkPS1VgfmP0kINlIFbxgoL5JLu3hnCzAHc9sqdcfN2uidIhXN8tXtQBs30d7ayN4cwytJFyjgfP7z74N8Li3FD4byu/8vdiF6FnuU4LYHKQRg1t2W6o0zjg1uIz3FZTXQIlZRrIH16s9bpFagVCpxjyIkf8PemMiIAGqWoBipAyQTDzhm3tsywu0YpxYeLtvklJHMXxP9eA+kPoBOUUruCZLC6hR/gJGngso1TfLZ7F8GFkyHbneQhn4BFxaVxHfpgGS0g028dS6G0ZIfeu+U3Ix+AYhavOHdKoteV1RtdC1fe8ocu/RUuEMpvaIX6tiuGSsNbEFh3wVjcW72lZF+pknqrUVqogU4euRvZnsGw0/UbOtiw8wXkSxSkuxl2ngusNNm1v+4E407G7Hm6QxdqgtZXItxtW3m3SGd995gNMBKnjie+8VfUWy/rZE0OX4K4bIsGamFKTRl2gsjNNezQj/ywmjSbGsm9c73IrihRd3VE58Zp2qKSTrq2k0fuNbzNV69/9mSIVNmyQs/1F7d0DagjZfFwC7HsroLvmxqpuBEYPXd+EeI/q5+m6qcy/ULmDAF9JJjNH77w/m5p6q8ktR32z/2TwoBTDQsoHX+8tHOj8iF6Z94Lbs5dYaU7o4bfUopyIDlCqgS51QypoQqYD2qTvJj9Tkq+y5HiwU8zHE6dKM3UQCca93FrSBXpzLuXpaqOZBmEXxq442G5GUMzNdY1TyMZi+cFnkiyWNcW8PV9CgKifTQjFhHp7I+1dlnJeJFCfYH86Gl7SaaL0sEa6KypMd6YhhzhEjC6jpT7/fSxnGuylkofZIti3AM3Tqna7Zx2coiacuDQ75znL9u7Zk+UkmDY6eV10gbRsHfS55X2d3d0F6A2DUJBjo4gCAMKl5Z5wbDzvLzI4OjZIv3wcT1qoHQawC+epHnb63zpu9LdIrbUIg10knF/HGKlVep7mjybK7NVv6cd1sjUBFd9RDu0Xe5NEozC54ze+YsSNVBiDSRUiqIKmBf7ZyxUHXTROctOMu51hSPIjvAD49+HD5s08HlRIHFCPH7Yb8VF1ECbNUD8TEelmjGpggLeUTpRA51k3GGHS0sWLHZ7C8kQIHPGt/eNyrR9qtcgKZY+vycuogZarQEKaFQQL0vH726K5e9ardCSXZj+QErWaNMNuVJ6I1LnfC19CaFSewiUArWphrsob8z4ftKMVaUKSufBC76oX+TbVjsAACAASURBVFLsgXX+eFHzN79/Xtbxs3+Tg0FOnyZtn5V6bC89cL7Uw8MEs59JfV+JjHupb0lcbaYG4IX6Ad516ByCfCv1Oj5sxGXnaaATjPuUIVIFeGYvkAM1lwY0XNOsJfwmJLPDqQK3ShcRR6RGaTHcJxB+rSrNXE6qaC1sdcHcg5Odz2/wv72lD/9Q73rnHTBztlQzQbLnpeQFqeJJaTg++ihlp4HxC0mzl5BmL1qATCFh7F1pBMsOkMeWlLK3SJYeqAGMOFDakByDKJ2sgU4w7hUNizvkaFDbQw24eqie3hEunzb2H3xw9tdSgkHfyCfxtLHNYpfDZEvo4a3CuCrfgzWkBN/pgbkBfw20EHdIybUdEOwu1tl5fF/1ypKtIfVYM7d0pGeLSzM94Rkv5WJSQ6YtHzBuyMelHk9IGzN7K5G4LNLDpSwDp0JyuHOlSkbyUcpAA60dDbah67WXSpZGrPyfVBWCfW1ot+ilZVwmzyqkmu2lhEAU7qPGfq1FH8zvhJ2QUdIP9dtJwcjNINHCeC8ojesvje6Tq152dFPtpo/h2nkw92PepH1pg9N3nafWma31+6VUQWHs9SVbrwX1VeemKT4/ahM/ICX3S5Oead1MmT4O2EsyXC6rFbnpJVJVs79URa6Pu0qsgU4w7oU8z7ZoiXgjYBskSedfJdZROzRXvZnHAKd9lcXuww8TXvu3JaNwyJtS8p5U8alU94X05Zct/KHC+fG+1P9V6f5RUsXykvhjNkXhZnw3jQl9uUGqOEUaDuooSqs1UL2AZBt6Y76xd78FRstWt9rMC4HFwqv+pGTjpekvNV4Y55HFpV4bStmtpWQHSYsUuccsyY6TRvy1yLG4qxM10BnGnWnjQv6ZP5aqqPdZCrlC0qHl7WMH3193oaR0Alfhs8OD/YiUPCplx5XoxRfuwecNPwfcLcQ9ZocDUnXP3PQ/Wd//kCGAKua2g6PmHMnOk0akrq9vqbzX8FnXLSDN7i3ZAKlnD2k2CTdwmMyW+kwqfXD58eWkLO42RuXDPad8S2dqvFDhkP9Mso+l5CMp8bTDxucEX/pKkkFZTeC9qRd14UdEMh8Fc0I2KTNeip//pPDEhtvJQ1L2GGnEmw33x61y0EAHG/cJvaUvIXUK9x0vVTEVLYXgL77S1/PkR1BG8tAAqQ/FB45LcY6n+4d/++9SjzHSxu1dfi0ULaZ4BayIjUj1EpIrf0hAOryM0+c+LdXtLW1axkyaUErDp2OVUrKiZCRw+QBz+lHmWMe1BQUwRg+j+pVkvBQ/lyq+yK1nZza8qqK/lCwqGQimBXP3Sqj0RKCjkNO+4aVzbvH9xWdOgHuCNOtlabMWZFM7Nw8v6U2khOdnZhZ+c3PereV7npCSc6RK6uRGKVMNlPIDb8YjOmKhdPm8u6WqXzXjwi56CtzWj+0pGQkdFPotFIz6yVLlna0oZlDYVnO3qcxEhSbolm+e+0WPLyhlT5bsyCLVi76XHOtfGfFt15L5/FvJ4NIp5hue+yN3/BlvSPaYVPGY1KNG2ogXSwkFZEvdNlIC9TODqaVa0TgvvLslu1oa8XQrro+XdLAGik2727EL2cUbDiCSFoxG5ugWqc4HSrqUci1zHO30HePWlmovkQSssVCmSsmZ0g8XNe7vLLykZNv47pFmVpxySIvjperLpARdYzSDDJLsBqlmI8mOkkYwK+skqcXdcZxkxF1a6vLoyD5T3el1KanNGfTZj7VsVN6arroYCRXOfJUzyuARvDUqYhFzgWYXg08OBYKLaqKUfCrZC1L2MWnGI53wXfXdiYvWaKCDjXtSMCW24ONrTd//JglOdgKpf2lNA+1zzZOLSTPPlpL9ixgZXkI3ST3/WPrRWbOfJrh9WsgxM4Jyc9tJ1QdJyZ99ScJw04OlZJj06K4d76apHiYlf5Jsk9CZRpbw2ONCol7A/yRL8donzGCB9RHUhPMeQ8eymDuqkeaL7v4xZ8jtdakCXqPnpBkvSFsEdFPRi9p/p/ss+Tw7mom1/R8t3iGvgQ427q4mZv7mUuKrraR2NX91tPdllolLwGXeHinNOl1KQpZo+mme86Pbzp7SttK4h0cZca30+DipDsMAZC/IWlKPF6Xq30kjmuHuCZe1don7xc6UtGsj/mRGyOj8AanHg1LdKy0PABNkBiFiC0s9Fs4t2U4WkrKpgQqFJcgENv/X4xup7tMcsqkrBp1b+5nE68pJAx3sc3ejrDTVwOVSFWyNXVjyfnUMTTF0AVWQ8Kvf0IF+9ab0icGi+DQBwtb4Xn3bLmh3oZQU+/z+Jg08UhrWDpzrT88nzThVEpjqYoMDnusKafbl7e/uaErN8VjUQOdqoIN9kz0Kf+wtgWuBsYVSoIP73NQHVDNCeozRISPVQsMOmuJ8qe9gqervZWLYeRjgi/DK4F8FEtlK2fpHacQRku0h6fuCRg6Qpr4ojSsgiCs4q0Wbd/SQavaXZgC7I5Gm0LBj1I+Sei8vVY2Mhr1Fyo0nd0MNdLBbZtZ3ORd5XpPAxporuF8orkwqNYHKTpTHhuRw3rZN8Vhu8oBUcZy08dud2Mmmbo2/FZgggbQ29nHEGOnR56Se8IwQoAsyWKr4j1Rzr1R3XOt98dX4wXeREqCkqXq74TaCDO5iqef50kaFL5n8SXElamBe00AHu2VwYdTyYwy43w+lqsIRb2OfAYkZFASg5mknoTIcWdIoSQf4QG5BXyHdsj+2I51Cwf1avUntWHzVIHmeanUrDS50MQd0c2KR2RWBxRuliiul4c3k04dbxeHs0XWxzMjsGmsMuuPee4c+s9xyfW5LkjYhrxo8SSk3zAwILDOYJ5IkaeOLtJQ9i211dw10sHFHnTUYk1Qgrsfy0sZp7HsZ6vyRhaVex0h2bMNqM/muEqQ8VaocIyVlCMvM9zOsXJXDgmubHG9M2N2ypZmBJgFl8k2SJJ6catymUgXtr9RIa+9KVitVPJujVMhOkUCrELRMQKisCYFar17JkKWX7iMzaeLEHzVzZr1ak0Tjn3jiFzUbbDAI7D3cOST9rJ4kCbGEshIzq/HcQfTtmCRJri6rDsbOdFsNdLBbBj0mNZ4YySs1u6WkYl948LcEzfCvgnzoBMGo9ySr9EjJEXAV9uGrHF59kSukoQUZi4Wndvy2GTMllyH5XpIk6dlOIBcrZPWbayfNIDzTSZJ+n4IKZs2M2cCRSQJtwoNDpf4cx5WSKr3mmmdEDuMhOQqpQX6i4cPn19FHL6Of/WyAVlihr3r2zI09vvlmtnbaaYJqa78lo/aUbLYS2OPJH3zwgWbOnKlVV12V78qmvlZsrtky+G9mzFAr33zzTfXr16/f8ssvf5WZLZEkCUivKFED3U0D1VVSjdX/1T7RyBMy4mG4dkgjx9txN4RJ1WdKNVPq+5nuc80PUs1ZEsiN8hQz29DMPrScfGVmvESD4N5Ct8WQLuGcokszu5Em6+rq7KmnnrKxY8fa559/7m9jT/oXir/20aWlmmukmmmN6DH/PVhyyfE2fXpdaMdmzpxpn332mU2dOtXte+aZ726XrMLMMux4//33bZtttrEff/yRTf5Bp1BWYmY9zezLGTNmuL6+99574fnSrKhl1efYmaiBNmjAUd1+KN1t0tYmDcDI8AfaAVItptkISTakypOk1EEybrA3RjMaMUZ1ueNPtAFC2P6PYmbLmNn3GJXx48cHgzLNzCCvQohboPNT/HazFma2O429++67tuaaa9pvf/tbO+mkk6xv3742YcKEcB9YDgukehGp5nCptkaqmVVMt9tt96q7/t5777XBgwdbRUWF+14888wzod1dzGx9M5v93Xff2VprrWWvv/56OHZ4wQ3LZtPMTqOT9HWNNdawb775hs3ZZrZO2XQydiRqoHQauPw8aZFg1AuXcE8XSwIq3e3naGnchlLNXVINxjs/mkytU6noJqm2GFpjjtY6e4eZnYcFWWeddZxur7zyymAEz/J9A77IsfNa0ldsFCPllVde2bbffnvXJsadtm6++eZwj12abpOSgdXrSTV7SNXHSLUn8jd8+IvnZrPO6Nns2bNtueWWswEDBrgRvJn9YGb9zcy9Qfbdd18755xzwv0ebvp+nXvUzPqY2Wt09swzz7S999479PslM+vAgUvn6iHefZ7RQM9/e+NSaNjDNhwm7SxQv2JcKC9X1KBj5GdKNddJIDe6jpjZA1iQzTbbzJIksTvuuCMYFDi5EQwwumam1CwxszVp5IEHHnCf0S233OLanDJlit1zzz3BCLOv1boyMyzfLGYcffr0cf33HX/LzHZg/cUXX7TFFlssuGsYAcMGWtZCH81sBi6mRRdd1GpqavxjGeRtUaIGuo0GwLYTIA2GvNgS4qJ2CPYSYHSj9JukmulNGPUfpNorpEcbQ3yU9YdhZudiPTCS+K29fM0o0ne8Ncb9cNq5/vrr3ef10ksMPBvIdDMjc7TVYmYr0CK+fL4fo0aNCjdgWvAoG1tttZWdeOKJYf8/W32zDr7QzFynjznmGNt4441D/1/u4G7E281DGmgHAzpX7ZE8M7csUwKVi3lO7bk22PQJGPSadaVkV6l2F6miqeIgn0t2WY4XvoqKNU7MDIgenNjQyQYDGQ6D4U5n3vJsQATB5bNOeyQL/TJVuBToHkFNCiLgoya+AFQOOCFTda4n9uCJp3RPAdol3LuxZTWMmX369FlkySXpuhPaq8R2/uMf/9i8d2+X4Fm5ww47MHoEScM9yVqlzzz7/UmS/C9cLMklKH3/fS5PaIEF8kCb/0gC3/5qkiQtJsQyM3SKuwv97MP9xo8f7247fDhEj06egRzuvffe07///W9dcMEFYT+QyznEzJWpg96WbFwKfUMYBgPpYI/vJzGKgP3dSZKkCMTmaKroDu8v39lnycK7fl+SJHxWeTEzXIvTkyQJKCoS747bd999F7vooos0YcIEDR069OcEgpMkicXI85qLK11ZA/yYi43WU/sqstLf0tmOLXxeAni1v5JqLpFqPmpihO7967XPSzUHS/Cl1As+UTP7s5nNDEOtTloCRznawxDrO1hkzcwOaqKPr5jZd00cTx+aZWYngX4xs5vCgQsuuMB9Tp988knYxRJUTovQKma2ObHZdCNhfbvttnNumWnTiAHbFDM7ghX87CuuuGI4DZ00yNPwfvkrzAxoZnPkodRspog2G+5C/2b2lyINv2wGTl8iaB187GYG1Gfv0IqZ3cm1yyyzjJ1xxhmhGeCiUaIGSq6BBj+OkrfeeINUZ2dk24gMk4QRSRjC3S0l1dLCrxXHkoNcqVtNssFSAi0AxaZ/2ghTYPp+DEFvk7JXS8ULPxOYBGc/bdo0XX/99frqq9xgfsaMGdpyyy31+uuv69BDqewnnXXWWXr77bc1efJkDRkyROedl4tV/vDDDzrllFPc/q+//lq//vWvtc8+boAqthmFvv/++xgFnX322a6tK664wo3sFl10Ue2www7acccd1aOHi71RH3bXJEkaLW9nZkBLNzz++OP1+eefu/vuvffe2nPPPV3bs2fPdvcBe03fTjzxRK2zzjq6+uqr9c477+iLL77QSiutpOOOO04LLLAAbIePStqcZ3v66af1r3/9S3fffbf+9Kc/afHFF9egQYNcH3v06PF6kiRzqwnr+mDmaAoIhM736quv6oYbblBdXZ2WW245HXHEEeK5f/rTn+qJJxxKFtoJ3Hj7Dxs2zOn2xhsdEeiYJEkIDIc2URBVpkZ8+umnuuqqq5x+F1tsMR177LGun7fffrueeuopt/+oo44S7Uk6LUng1p+7gHyRNBq98rnxPdh///219tqUnxUfHqPzt7PZ7HwPPfSQNt10U/Xu3ZsvzZJ8ZmZGQPvkXXfd1X0u48aN47pHkyTZbO53j2dEDXQNDZCOjYFKjdbDel+TriuGWGHft1LN11LNV1LNh1LN1LmPyhsES/Gz3yXV7iZR+q5xMbNBZjb9008/taFDh9qjjzqXrxttffvtt7bTTjs5NEoYfo0bN8723HNP9zybbrpp2O3w4LfddptVVla6YyNHjswfmz59ut144432i1/8wh0DWnjQQQfZa6+95gKU+LXXXXdd++Uvf5n2nVPVqVExs7Hc4N///rcxAkbHoDTS8s9//tO22GILd+zyyy+3ffbZxx5++GF3CiPyZZdd1oYNG2azZjF4z8lHH33kMO2HHHKIu+7UU0+1v//97+46MO9mNrmZM4tfevSLnX322bb88ss7HzsN0K+ttwYeKwex9LfewsxeBvfeq1cv+/OfmUg5aYDRN7ND2QvkEP2//fbbls1mbb/99nO6P//8841ZB/t49iWWWMI3Y/9tVJmpAz4eMOuHH35wn/2kSZMsk8nYT37yk9AOmNO92HjwwQfdM7z55ptsMutzxa/NbGd2/OlPf7KFFlooXAcEOErUQLfSABVzoB1wP4Tccv5J0l9JEGrMuLdm/49S7X+k2n2lsc2GWJqZs9DnnXee7bWX+82GH+OnTPvBK2OU0/LOO++4Z0kZd9wOBBrtpptucsdSxp2p/Pcc48fO82+yySbO+Pg2nftk4sSJNmjQIIcrx8ARJ/UxgKJfBjP7v+BGevXVV127KeMOJO9jGsH4c08ghyksPJBD+/3vf++OvfIKXhwnuEacBLcMqJWU8JxNFf12ffVuLgcLxDD269cvjY93rpTwogOV410yQAmnf/DBB65Pd999d7gtPvW8mJmzpLvuums6qcpuvfVWd93aa6/truOFCoYemKiXJurI5ptnZuXgpUA/n3/+eXfpYYcd5p4B6KaZoawLWDniiCMcjNO/HPNBU5BEHB8zZozr0xdffOHaMbOyTYar10Bc62oamFtgsz2f5z7PTAjPDCRWa0nfLSGtQfARP2Rrg0yzpORJiSmwbS4NXFCq3DLHp745KJzmimOs/Pjjj5174NtvQ8a+40BPCChuskm++I+r/o6LokAwHBBfObdAwTHm5M6ns9BCxDKlyspKJa4okK6XtChuoyWWWEKHHHKI/vvf/+raa12VNOIC6KuoJEmCu4Mkqyn9+8/B6MsxZk22yCI5Li5cIOuvvz5t4Spz2cDLLMNHIKWeeSfcF0VvKJ2TJMnKSZLgMpqb7CdpKO6d0aNHOxcMLixJj0t6iJU+ffo4Hfg+YRhRat8ff8zFK5daKp8/FoqOYHgJvg9+8skntdFGGzl3kSQXKOXzQ3BvIX379tVbb72lRx/F2+TkprAyl+VG9IGgLm4Y3GgPPPCANthgg+Ay47sFBYIefPBBrbfeeurZ0+EVXki168pK4ipCvvwyP2if44uTuiauRg20SgOdadzpML5UkBBA2pgem0TNzqpzpKo1pDp4SA6UkislaHRdNfgXJNgX+dMjkm7FD5rjFa9YW/oRY76RNOJUacQjbSgYAXulQGx8+OGHWn311Z0fGj8pfmsEv7MXXlTvFzGmjMh4tmlFDD+/7HskZQcMyHmIevXqFdq70KMsTuA4vlsEX7eXYnVZwzGMI37eH7yfPr9f0idJkrw+efLkbzxaRiusAHjJCY074+NfMJo+Pc/DRTlEyhoWk4uL7Wxknws2nHTSScpms9prr73CaRCALcM+fPDoesEFXaEjOuDefLNm5UAtQVeSpoaLJTnFYTR/9zvnrSGe8iDHn3/+eXfa9ttvH06fvfLKK78/cOBAnvUiSZQMbI7M5nM/55xz3Lkvv/yyPvroo/QLfqKknxPLIIbCS8ZLngUzSRL6NTO8zKdMcV8xTovGPWgrLkumgc6AQrag85sCYeOvMcPSgrZafOrrXLHzzjvr6KOP1iWXXKK//OUv7g/Ds8suu7igqYcEYqi/7N+/v6tLyqjOSy+gdmb2aZ8+fUAJuRGfP4ahBU0xrWfPno4C2Y/0OOxGpUmSAEn5ePDgwRBjadIkZ3tZzeMbfVvFFjODccdoevmE5bRp07LBuKeMJSPPLzhexLhjhRpA/UKDvphyarP4KjEMSetj+GprayH70hpruPwjrC+zhtUw7FOnTg0zCRoCMeWGueEZwjOl+5MkCa6ysausssrmvAw9Zw41bN2sa/nllw/34vmxunzvv/MvweIdnnPvvQMGDNiYYDNCYBkJL16MNjBSoJpIyrinR+4cSggeI6nvyRxTLHdC/Bc10AYNdPbIvQ1db/dLHaAbQwcuGcNzxhlnaNttt3Uj2muuuUZbbLFFGMXDXvkTDA9GM/WjpcQesnAYeYYfNi8DiKXwFASDVVGR/zhS9Tk1O4zoU9fmrbVvv9iiRzg/uDQkTebE+eeff1Aw4KkLiUe4/oZjoEG8MFVxQ/zQZuoZmwxMhwY8OqoXLhkEt4UXXDLkEPTE6CO4Orww82F2o6ADjL+XQoMIMdqavCSSJMHVsjgIn4kTJzp3l78GNAvnwasOfNPBXEKDc1mSzQuTpdPhPffc42YXHnHDi9FNLWpqalxf/fOht3zA1sMuewW94iLy4toMG3EZNVAKDeStSSka62ZtOBbFO++807kQhg4dqlNPPVX33XefMxgHHnignn32WedflURBBuej79evX9q4gyAB/7wQcEokjEB9shIO5J6pEXtQIVBO2sHIL8/0HwnTec9fHs5tbDkIqCMSjAk2EhraQYMG9QmupZSRxpC6l0Yw4NDpeqEfDroargvn8HIKJ81l6WYnwAiRn/88D4vHr+5wmo899pg7Foz7N984injnlgnGPeWnzhfw8C9JYIZQR19uZs9CdfzII3jtGrxICJxc9M0331TMmjWLl4Pj2qGghpldTJzZB0VzQQd3de4fNA5JkjCDrOBliVuGmIt/MeOW24LP9vHHH3cQy4ED3eMCD837tsJ3JLhjiKd4nePSiRI1UFINROPeuDodZhsD4fHInEmt1H8usMACM8BRg8sG654WjHvKKHLo1/z75BPnEXF4bn/+hmE0HEbuqXYoooGQzt8zGL1ttgm7XXalP2XOhZn9jAF6eKGkRu6MVClqEWYc6RcRvgKHtw6GO/UcDKWdPooY9+a69pwvIrzIVlzRebDoPKmuv6GPjNx5gQ0ePNj1b+RISubmJLzYCIZ6SWcagzGHYx6mRZ6BbFf95z8kz0rrrpvPhzuKlxHYfj+TWt3MmHmQT8ExXujw0L9gZo1VCBuAe4yXYqpd+HSWZHbHCykVaM85/H2HQ5lA8gnQA8FsZnCpLNb6M+Na1EAbNRCNe+MKdPA0DNzFF+djhuTD43+egUEmgEoCkBc36mXERiKTFyKGZ7AOsgJJjaKp6kTafn40H14AoFbMjLT240ic4v6rrbaa8/37QOJdrrEi/3zWphuRFjHuWEsXaCSBCgmGPFeQxAWn84Y/JG2RUUYVIc7/7rsc4Cj1HEAA3f2KdCe9C/RTlhkQknpxENSc78wzz3RJYhhMXj4kAfHMQYhzLLvssiJg6WUtlj7N/yj6uvLKK2uPPXJ5TRjZhx9+2LnJfvYz3nVOfvrKK6845JIPfhPbAD20HDM0kDu33HILJ+Lnbyxz9PPwguJF7sX5mMJMYcSIEWF/oXF3QQbcRejBt9NwdBCujMuogaiB9tFAwDWTtNOjRw8D704CTBCSj4YPH+6SlDyW/VqOHX300da7d29Lp+dfdNFFLt0cW7Tqqqu6pB2SYYL861//crjnIUOG2MUX4x3ICbzlkExBsfu///0v7G6SnMvTJRgYd/rCPX/+85+7JCyIxBCSow4++GB3DLx3dXW10R9w9E8//bTtvPPO7hgJVDAYgtfmGIlcAYe+xx57uO3vv3dQfZp1o+WmPg0zew5M+HrrrWfHHnus6wvbo0ePdtjw1Vdf3X7zm984nW677bb21VfUGHFUAv9ihXwDdO7lTu5lZquyDVafzZDW/4c//MHOOuss91kE3neSiiAeS/V5JzPbg+tJHuP6xRdfPLT/brFnMbMX+B7AJ08fg9xwww2O1x6aYk+bwKE8FMn31WXCoVcS1ry0iFO/WJ/ivqiBYhroLPqBYn0pq30UpgBnzqgZOCSuEXzsjB5xIcw///w64YQT3Ojdj2oZmr9aV1fXlwAsgTUQIfhXt9tuO4eqYGQJXA+MOS6HQIx11113OVQOaBzOGTt2rJuyv/HGGw7lQUq+H2mCU9+2KbIrEj0B+RD8DT73oNj99ttPq6yyikaNGpUeObvDpMTjJrjyyivzMwkOMEMhfZ8ROzj7lI/eXcezbbghHibtliTJP9zORv5B2wvQhLb+8Ic/uFkDuoSSAajiSy+95PqGfkApbbXVVrSEK4xA6LNAQaFRwO8+cODAd5MkWcXPVF7JZrM/RafQJ4CVB9cOkgXM+U033SSw+8wI+Mw8xv/SJEmO8hTFb3/00UfJbbfd5ugJPBJmWpIkDYLFZgbkEv/aYlAcnHzyye57wGez9NJLu2ci4M4sQNJLSZIQKHZiZrifJk2ePLk3tA185h6euV6SJMCBo0QNRA10hAbMrJ+Z5flywzCrYAmzVb5kmuccJ4jaIvnHP/7hRo1kqjYiTBn+FtLYm3p+n1nrsmIbaas9dk/0wV+Mv3NnNdZHMxvdAmIveNzn9+Rl75NdCm1AqjCIyyLzXPOgj5ojpJPC2pWPFZhZPu2V7Fwved8az+KzayElc1TK4aSwvP/++91nCNWEF6pd5cUTv7m+Mzvw2cakqMaCHXktxZVSaiD/BS9lo92hLVAOGEpJZK1gsICs4OcFBkGSEFCMq5Mkgb7XSZIk9+AmkISvvcrj0fOZSf40ICw4jvG1At3bL/jGU35ocPOgSDiXylR3JUmST4bx7RRdUKDa0+iStQO2nEAmcQIwhLNGjhz56zfeeGPnDTfc8Mqjjz7aMVcVbajhTpzWZK/mUisbHnuJkIKkv5oZMQlK/PFs+NgzSZLkI6BcliTJ6WZGpJPMXdJTP5XEjOQDX0ybbFgc8/irRyZJ4pz8ZnZz3759TyPn4LLLc2ffUAAAC25JREFULgsJUBfhDkqS5L9mBsLoRK93YknELIgV7Ctpa99lsJY3JEmCTtOCIYbOoD8ZqF4mhBW/JFB7KLEAZiuXXnqpyxwO51x44YWOcI38B0kENBy7GRuec8dlI9P3I488MkA7r0iSJAd6Dw3FZdRA1EDX1oCZ7eYLOxs+eWzAaae5cpsM/GDiaiyg19YHH8O9YDtoTkO+TFyeBCUMS5u5fK4592jOOfDpMGimmhFkXRC1ecmnCTennWLnmJkbkTP6PvdcV+eEpl2wO5zv6Y+db75///6ORC104LLLLnO+/ccffzzsOjhcx9LM9ucAcYEll1wyVJFi1ufgMulz43rUQNRAF9YALgFIw3AzQChGgA0bQKCReqe+iDL2II/hK+HjMjRltAjr5RCMmJk9Ymbv+T8Izah8BIoEwwR9YfbLL790jIwEhGE0JJiYDhpfeuml7hkgIiP47IOWOYB+iToPxxpKeeihh5zOPGEXbpY26cnMJhE03mGHHYLLhTZdVnDoupkdzL2vu+66fNCbawiAw8/+yCOo0Ml/0jzznl30U2rPEowmeO6FIi5RogaiBrqTBrxx/xYjfu2119pVV12V/7v66qvTrIZ58pUSPT+JP8ARYXG8P1iZRpZAa0IyFck9Tk444QRnxG+//fawyy0ZvVZVVVmK6ZD99UD1EjyAN5Tv0zCznRTD5lNtaZ7C25MnT06/rM4tbI+AqJm5e2PgQSIdddRRrh9QQHt5ytMs5C/nfcAx2CQDksfMKHnokt7yJ8aVqIGoge6hgVBdKFiFIss3zMylMDbniQm2mhn4eLCU1Ov8IzbQzIjwXehLw23J6HT69OmOOxg3AdDDNddc01U4At4H7fCdd7qCQXTJsXB5o4qvfArwROB+G2ywQb7LY8eOtV122YV22QeXDhW5920Ov3tzni19DiX04OPhRrxQUtTDZAm3SkBGeVcYzfLWKoyTuHb9TIeZTTFhxJ6vPcgFZkZZQ0cRnIK4op8QA2hVf+NFUQNRA2WqAW+gqoORSlkKOMkx0EPNbBEzy4AN9+4SSMQx3Dl+4NSzmRm150CWNCXZsWPHPu0NsJ1yyimuqAXY9SlT8nTt6eu/MbMGBcIx9pxw5JFHutH7Y489ZviZDzzwwFDYA9/8XPHuqa63atXMeLNQZi8IbpQ2+a8pE+iNcZPoFTPrbWa/MrNLzOwqqv/5QPIczwIVseffD/1kiE8dgyhRA1ED3U0DvmBDU7A9SiLhk38pWISC5TvANINePGRzAudQTen444+33Xff3SVKUdiCZJ7DDz88jKpdU4wiqSTlfdbswzgGCw9T5RiP+gm3cUszW4qTKUpCwQtG/CnDzqE8P0KDC9thA8ilf/k95svftcNd2t6kh8fe51/MacqEtjceW4gaiBroWA3AV+LRMFTvedCXvqP83Y2hEtITTzxh22yzjS222GI2ePBgu/7660MGLLwmI7CUjKgJtoKwYLScCrTmcvhzU3+X6khlpVCCDiPfp08fO+aYY4wygayHgB/Zk2S8EiBNCYYdlkRG3swQiA7mCdCD9swsX3oqlMMj29UL6aQxKS4oKy6jBqIGuo8GfLLN0T5gFoxeo0uM94ILLuhgdKmUeHy+ZE4aJfZWW201u+ce4PN5+UtaY2Z2F0dCOj/rpMJj9z/88EOH/gDi5+ucOjcKafxff/11/g8kRxEhcWrjgnuBorFnnnnGURQweoeGwEuu5FH6grgeNRA1EDVQzhogKchnNd7qMcuOcM3MljWz/QiQecNOFqnjFgHSSPBylVVWccFKRuebb7552j3iAnNg2zHEFJv2wv4fMbicD++LF8hoSMhpIGb2j3BCWOKGgaemmNx777229NJLGzVFC//gsdlyyy3tjjuIhzr5TbiZmZ3KngkTJrii02RYwkED7w7FqL00xqQYmonLqIGogaiB8tCAJ5aqZ/nKWTHcK9d4X3UwbM4KM9oeNmyY83W//PLL+RFzOMkvKRLNi2EK7hHQJ+uvv37+FIipfve73zliLr8T5sXVi2nEzI7PX+hXMOyHHnpo4W63/dprr9k+++xT9FjBzo+gFDAzMk7/yTEKZu+22275F9Rzzz3nXkypAuK8FaJrptgHFfdFDUQNlI8GSI4xs5mwM4Iu2XfffZ2rJBhBsNGjRpEDlBNYF0lUYXScEmB7IUGI4N+fybbkKcGBc95hhx3mjCTQROSPf/yj3XcfMTgnpNbPgZDx11N84yNcLnCaMNrHeNM0PvwgJ598clh1S/z4uGxS8h2228ye8Hw6+IG2N7NLzcy92MgI5dkKfPUOEol7hheZl1sihrt8vsOxJ1EDUQNFNGBmT2OwoINlRA62O5We7lwpGMoguGKgCE4JmGZ8FgQp+XvGG3cqMGHclyD7E8PIJm4OEl1IZPKCBW4Uv21my3FebW2t9ezZ0z744AP3ohg4cKCBlEFoK5BZTZ061TnXOR+q34KEI39LAy0TkDOO4IrsU4Kyzz/PIzjhJUUijl144YWu72SsptA3uRJIRXQad0UNRA1EDXSqBjynSvbdd9+1W265xchC7NWrlz3wwAPevpntuuuuLksx7ABeyOg7BCtTmYvhlLDM1ZPLGXiSlWzDDTekjJudfjq5Qk4mFYMjppXiIZOfM2MgoMoI/dZbb3UBzz333NPBIkMm6aRJk/CnQ6L+LK0zMwA1A5f6U0895Qw3EEeSlIKRhl4AxA3c4/wFxI1P9JkGEoeZCzMPjjPjgPvFCzVZo0QNRA10kgaif7QRxfvsSmhfXdZh4BKn4s+AAQNcmTb4zy+//HLHMU4z8LPDoR4KTIemBw0a5K5ZeOGFHV96v379PkmSZFmOmxmGfmP4xyke/dlnn8GhTt3NDZMkgXGxSTEzSM/hcC8sGJ2/btq0aZ9sttlmLz/11FN7+SxKqjENgWsernMqA1E9KrBS7r333oFrPN9GagVGzM1hV6QUamp/ehXmxf3SO+J61EDUQNRA2WgAd3hIS2d0Ci49CK4NbDMj+yCk8qdw32F3seWZPKTnCHeAc7DsBxxwQDj3/JYoAZIrj+YhBR6HP4EAIJJwwuBwbzCK9hzpZLvmSjOFuzZcEivAP0Rc4DKfjXlk8P/7e5K2D5kXgVSQRGeZ2fot6Xs8N2ogaiBqoFM0YGYOowhsMEX85IigVlhhhWAOoeh1LhsMfErgcMHPDsf6J2ZG+jmUAgRCe3jOF6P8G7Y+BYeEgrbdZ1Wk7JvZzmZ2pqe95b6sU36uaBC3Uz6EeNOogaiBqIFSa8DMXG1Ugp34lBEggfPNN5/zNXtDDmWA853vuOOO+UzRlJEPqyQG0R6B1FfDTigCMO4BLeP3txefe6lVFNuLGogaKEMNtPvosAyfuUVdMjMKJa/08ccfu/qb1MrEN00lnnvuuSf4pqGIHU990OnTp1dQq3TGjBnaf//9Xc3V3r17a4EFFnB/+N8lzZbUc9KkSbr55pudz5udQ4cO1e677+7qrFLJJ0mSOZKWWtT5eHLUQNRA1EDUwJwaMLM1GEW/8cYbBndLkGuuucbxvYBS8fVAV+Fq76OHhMshS+D7BlET/sDJe1ZGLnwxtFdkyQifAt1RogaiBqIGogZKrQEzc9WSoRH41a9geTUXQCV9f8wYiBOdXJm+r6fzzbNphZNSS/zwf+QakC6+ODP0sdR3wx8PrW7RbNT0feJ61EDUQNRAUxqIbpkmtEMBHQpkjxkzRs8884z69Omjl19+Wccdd5w23xw0oCiivEGSJBSgbiBmRoFsmBUZ1feVNNMXbb6zsGh0gwvjRtRA1EDUQNRA+2rA85fDseKSc1IJOuwiGWix9u1BbD1qIGogaiBqoF004ImzIAoDxog/HWjjcWSHtssNY6NRA1EDUQNRA1EDUQNRA1EDUQPFNPD/oAIXGFOCe08AAAAASUVORK5CYII=" + } + }, + "cell_type": "markdown", + "id": "c9085e80-e7ba-48e9-8c50-12e544e3af46", + "metadata": {}, + "source": [ + "## Distance\n", + "cuSpatial provides a growing suite of distance computation functions. Parallel distance functions \n", + "come in two main forms: pairwise, which computes a distance for each corresponding pair of input \n", + "geometries; and all-pairs, which computes a distance for the each element of the Cartesian product \n", + "of input geometries (for each input geometry in A, compute the distance from A to each input\n", + "geometry in B).\"\n", + " \n", + "Two pairwise distance functions are included in cuSpatial: `haversine` and `pairwise_linestring`. \n", + "The `hausdorff` clustering distances algorithm is also available, computing the hausdorff \n", + "distance across the cartesian product of its single input.\n", + "\n", + "### [cuspatial.directed_hausdorff_distance](https://docs.rapids.ai/api/cuspatial/stable/api_docs/trajectory.html#cuspatial.directed_hausdorff_distance)\n", + "\n", + "The directed Hausdorff distance from one space to another is the greatest of all the distances \n", + "between any point in the first space to the closet point in the second. This is especially useful \n", + "as a similarity metric between trajectories.\n", + "\n", + "\n", + "\n", + "[Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e75b0352-0f80-404d-a113-f301601cd5a3", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 21, - "id": "e19873b9-2614-4242-ad67-caa47f807d04", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
lhs_linestring_idlhs_segment_idrhs_linestring_idrhs_segment_id
0[8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, ...[18, 16, 18, 15, 17, 137, 14, 16, 13, 15, 14, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...[9, 10, 10, 11, 11, 28, 12, 12, 13, 13, 14, 15...
\n", - "
" - ], - "text/plain": [ - " lhs_linestring_id \\\n", - "0 [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, ... \n", - "\n", - " lhs_segment_id \\\n", - "0 [18, 16, 18, 15, 17, 137, 14, 16, 13, 15, 14, ... \n", - "\n", - " rhs_linestring_id \\\n", - "0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... \n", - "\n", - " rhs_segment_id \n", - "0 [9, 10, 10, 11, 11, 28, 12, 12, 13, 13, 14, 15... " - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The third element is a dataframe that contains IDs to the input segments and linestrings, 4 for each result row.\n", - "# Each represents ids to lhs, rhs linestring and segment ids.\n", - "look_back_ids" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " 0 1 2 3 4 5 6 \\\n", + "0 0.000000 0.034755 0.031989 0.031959 0.031873 0.038674 0.029961 \n", + "1 0.030328 0.000000 0.038672 0.032086 0.031049 0.032170 0.032275 \n", + "2 0.027640 0.030539 0.000000 0.036737 0.033055 0.043447 0.028812 \n", + "3 0.031497 0.033380 0.035224 0.000000 0.032581 0.035484 0.030339 \n", + "4 0.031079 0.032256 0.035731 0.039084 0.000000 0.036416 0.031369 \n", + "\n", + " 7 8 9 ... 388 389 390 391 \\\n", + "0 0.029117 0.040962 0.033259 ... 0.031614 0.036447 0.035548 0.028233 \n", + "1 0.030215 0.034443 0.032998 ... 0.030594 0.035665 0.031473 0.031916 \n", + "2 0.031807 0.039269 0.033250 ... 0.031998 0.033636 0.034646 0.032615 \n", + "3 0.034792 0.045755 0.031810 ... 0.033623 0.031359 0.034923 0.032287 \n", + "4 0.030388 0.033751 0.034029 ... 0.030705 0.040339 0.034328 0.029027 \n", + "\n", + " 392 393 394 395 396 397 \n", + "0 0.034176 0.030057 0.033863 0.031111 0.034590 0.033850 \n", + "1 0.037483 0.033489 0.041403 0.029784 0.035374 0.038179 \n", + "2 0.036681 0.030642 0.038432 0.032481 0.034810 0.036695 \n", + "3 0.032808 0.029771 0.040891 0.030802 0.032279 0.038443 \n", + "4 0.035645 0.027703 0.037529 0.029356 0.031260 0.035501 \n", + "\n", + "[5 rows x 398 columns]\n" + ] + } + ], + "source": [ + "coordinates = sorted_trajectories[['x', 'y']].interleave_columns()\n", + "spaces = cuspatial.GeoSeries.from_multipoints_xy(\n", + " coordinates, trajectory_offsets\n", + ")\n", + "hausdorff_distances = cuspatial.core.spatial.distance.directed_hausdorff_distance(\n", + " spaces\n", + ")\n", + "print(hausdorff_distances.head())" + ] + }, + { + "attachments": { + "f73d72ad-8832-476e-9712-0676a4bbad10.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "993d7566-203e-4b87-adad-088e2fd92eed", + "metadata": {}, + "source": [ + "### [cuspatial.haversine_distance](https://docs.rapids.ai/api/cuspatial/stable/api_docs/gis.html#cuspatial.haversine_distance)\n", + "\n", + "Haversine distance is the great circle distance between longitude and latitude pairs. cuSpatial \n", + "uses the `lon/lat` ordering to better reflect the cartesian coordinates of great circle \n", + "coordinates: `x/y`.\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "70f66319-c4d2-4a93-ab98-0debcce4a719", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "a17bd64a", - "metadata": {}, - "source": [ - "## Spatial Joins\n", - "\n", - "cuSpatial provides a number of functions to facilitate high-performance spatial joins, \n", - "including unindexed and quadtree-indexed point-in-polygon and quadtree-indexed point to nearest\n", - "linestring.\n", - "\n", - "The API for spatial joins does not yet match GeoPandas, but with knowledge of cuSpatial data formats\n", - "you can call `cuspatial.point_in_polygon` for large numbers of points on 32 polygons or less, or\n", - "call `cuspatial.quadtree_point_in_polygon` for large numbers of points and polygons. \n", - "\n", - "### Unindexed Point-in-polygon Join\n", - "\n", - "### [cuspatial.point_in_polygon](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.point_in_polygon)" + "data": { + "text/plain": [ + "0 9959.695143\n", + "1 9803.166859\n", + "2 9876.857085\n", + "3 9925.097106\n", + "4 9927.268486\n", + "Name: None, dtype: float64" ] - }, + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "polygons_first = gpu_dataframe['geometry'][0:10]\n", + "polygons_second = gpu_dataframe['geometry'][10:20]\n", + "\n", + "points_first = polygons_first.polygons.xy[0:1000]\n", + "points_second = polygons_second.polygons.xy[0:1000]\n", + "\n", + "first = cuspatial.GeoSeries.from_points_xy(points_first)\n", + "second = cuspatial.GeoSeries.from_points_xy(points_second)\n", + "\n", + "# The number of coordinates in two sets of polygons vary, so\n", + "# we'll just compare the first set of 1000 values here.\n", + "distances_in_meters = cuspatial.haversine_distance(\n", + " first, second\n", + ")\n", + "cudf.Series(distances_in_meters).head()" + ] + }, + { + "cell_type": "markdown", + "id": "7f2239c5-58d0-4912-9bd7-246cc6741c0a", + "metadata": {}, + "source": [ + "### Pairwise distance\n", + "\n", + "`pairwise_linestring_distance` computes the distance between a `GeoSeries` of Linestrings of \n", + "length `n` and a corresponding `GeoSeries` of Linestrings of `n` length. It returns the \n", + "minimum distance from any point in the first linestring of the pair to the nearest segment \n", + "or point within the second Linestring of the pair.\n", + "\n", + "The input accepts a pair of geoseries as input sequences of linestring arrays.\n", + "\n", + "The below example uses the polygons from `naturalearth_lowres` and treats them as linestrings. \n", + "The first example computes the distances between all polygons and themselves, while the second \n", + "example computes the distance between the first 50 polygons and the second 50 polygons.\n", + "\n", + "### [cuspatial.pairwise_linestring_distance](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.pairwise_linestring_distance)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "35dfb7c9-1914-488a-b22e-8d0067ea7a8b", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 22, - "id": "bf7b2256", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "1 11896\n", - "2 1268\n", - "5 50835\n", - "6 7792\n", - "11 29318\n", - "dtype: int64" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "single_polygons = host_dataframe[host_dataframe['geometry'].type == \"Polygon\"]\n", - "gpu_dataframe = cuspatial.from_geopandas(single_polygons)\n", - "x_points = (cupy.random.random(10000000) - 0.5) * 360\n", - "y_points = (cupy.random.random(10000000) - 0.5) * 180\n", - "xy = cudf.DataFrame({\"x\": x_points, \"y\": y_points}).interleave_columns()\n", - "points = cuspatial.GeoSeries.from_points_xy(xy)\n", - "\n", - "short_dataframe = gpu_dataframe.iloc[0:31]\n", - "geometry = short_dataframe['geometry']\n", - "\n", - "points_in_polygon = cuspatial.point_in_polygon(\n", - " points, geometry\n", - ")\n", - "sum_of_points_in_polygons_0_to_31 = points_in_polygon.sum()\n", - "sum_of_points_in_polygons_0_to_31.head()" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "0 0.0\n", + "1 0.0\n", + "2 0.0\n", + "3 0.0\n", + "4 0.0\n", + "dtype: float64\n", + "0 152.200610\n", + "1 44.076445\n", + "2 2.417269\n", + "3 44.197151\n", + "4 75.821029\n", + "dtype: float64\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "\n", + "gpu_boundaries = cuspatial.from_geopandas(host_dataframe.geometry.boundary)\n", + "zeros = cuspatial.pairwise_linestring_distance(\n", + " gpu_boundaries[0:50],\n", + " gpu_boundaries[0:50]\n", + ")\n", + "print(zeros.head())\n", + "lines1 = gpu_boundaries[0:50]\n", + "lines2 = gpu_boundaries[50:100]\n", + "distances = cuspatial.core.spatial.distance.pairwise_linestring_distance(\n", + " lines1, lines2\n", + ")\n", + "print(distances.head())" + ] + }, + { + "cell_type": "markdown", + "id": "de6b73ac-1b48-422c-8463-37367ad73507", + "metadata": {}, + "source": [ + "`pairwise_point_linestring_distance` computes the distance between pairs of points and \n", + "linestrings. It can be used with polygons treated as linestrings as well. In the following \n", + "example the minimum distance from a country's center to it's border is computed.\n", + "\n", + "### [cuspatial.pairwise_point_linestring_distance](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.pairwise_point_linestring_distance)\n", + "\n", + "Using WGS 84 Pseudo-Mercator, distances are in meters." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "40e9a41e-21af-47cc-a142-b19a67941f7f", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "fd9c4eef", - "metadata": {}, - "source": [ - "cuSpatial includes another join algorithm, `quadtree_point_in_polygon` that uses an indexing \n", - "quadtree for faster calculations. `quadtree_point_in_polygon` also supports a number of \n", - "polygons limited only by memory constraints." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + " pop_est continent name iso_a3 gdp_md_est \\\n", + "1 58005463.0 Africa Tanzania TZA 63177 \n", + "2 603253.0 Africa W. Sahara ESH 907 \n", + "5 18513930.0 Asia Kazakhstan KAZ 181665 \n", + "6 33580650.0 Asia Uzbekistan UZB 57921 \n", + "11 86790567.0 Africa Dem. Rep. Congo COD 50400 \n", + "\n", + " geometry border_distance \n", + "1 POLYGON ((3774143.866 -105758.362, 3792946.708... 8047.288391 \n", + "2 POLYGON ((-964649.018 3205725.605, -964597.245... 593137.492497 \n", + "5 POLYGON ((9724867.413 6311418.173, 9640131.701... 37091.213890 \n", + "6 POLYGON ((6230350.563 5057973.384, 6225978.591... 278633.467299 \n", + "11 POLYGON ((3266113.592 -501451.658, 3286149.877... 35812.988244 \n", + "(GPU)\n", + "\n" + ] + } + ], + "source": [ + "# Convert input dataframe to Pseudo-Mercator projection.\n", + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\")).to_crs(3857)\n", + "polygons = host_dataframe[host_dataframe['geometry'].type == \"Polygon\"]\n", + "gpu_polygons = cuspatial.from_geopandas(polygons)\n", + "# Extract mean_x and mean_y from each country\n", + "mean_x = [gpu_polygons['geometry'].iloc[[ix]].polygons.x.mean() for ix in range(len(gpu_polygons))]\n", + "mean_y = [gpu_polygons['geometry'].iloc[[ix]].polygons.y.mean() for ix in range(len(gpu_polygons))]\n", + "# Convert mean_x/mean_y values into Points for use in API.\n", + "points = cuspatial.GeoSeries([Point(point) for point in zip(mean_x, mean_y)])\n", + "# Convert Polygons into Linestrings for use in API.\n", + "linestring_df = cuspatial.from_geopandas(geopandas.geoseries.GeoSeries(\n", + " [MultiLineString(mapping(polygons['geometry'].iloc[ix])[\"coordinates\"]) for ix in range(len(polygons))]\n", + "))\n", + "gpu_polygons['border_distance'] = cuspatial.pairwise_point_linestring_distance(\n", + " points, linestring_df\n", + ")\n", + "print(gpu_polygons.head())" + ] + }, + { + "attachments": { + "351aea0c-f37e-4ab9-bad2-c67bce69b5c3.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "f5f27dc3-46ee-4a62-82de-20f76744382f", + "metadata": {}, + "source": [ + "## Filtering\n", + "\n", + "The filtering module contains `points_in_spatial_window`, which returns from a set of points only those points that fall within a spatial window defined by four bounding coordinates: `min_x`, `max_x`, `min_y`, and `max_y`. The following example finds only the points of polygons that fall within 1 standard deviation of the mean of all of the polygons.\n", + "\n", + "\n", + "\n", + "### [cuspatial.points_in_spatial_window](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.points_in_spatial_window)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d1ade9da-c9e2-45c4-9685-dffeda3fd358", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "387565b3-75ae-4789-a950-daffc2d4da01", - "metadata": {}, - "source": [ - "### Quadtree Indexing\n", - "\n", - "The indexing module is used to create a spatial quadtree. Use \n", - "```\n", - "cuspatial.quadtree_on_points(\n", - " points,\n", - " x_min,\n", - " x_max,\n", - " y_min,\n", - " y_max,\n", - " scale,\n", - " max_depth,\n", - " max_size\n", - ")\n", - "```\n", - "to create the quadtree object that is used by the `quadtree_point_in_polygon` \n", - "function in the `join` module.\n", - "\n", - "The function uses a set of points and a user-defined bounding box to build an \n", - "indexing quad tree. Be sure to adjust the parameters appropriately, with larger \n", - "parameter values for larger datasets.\n", - "\n", - "`scale`: A scaling function that increases the size of the point space from an \n", - "origin defined by `{x_min, y_min}`. This can increase the likelihood of generating \n", - "well-separated quads.\n", - "\n", - "`max_depth`: In order for a quadtree to index points effectively, it must have a\n", - "depth that is log-scaled with the size of the number of points. Each level of the \n", - "quad tree contains 4 quads. The number of available quads $q$ for indexing is then \n", - "equal to $q = 4^{d}$ where $d$ is the `max_depth` parameter. With an input size \n", - "of `10m` points and `max_depth = 7`, $\\frac{10^6}{4^7}$ points will be most \n", - "efficiently packed into the leaves of the quad tree.\n", - "\n", - "`max_size`: The maximum number of points allowed in an internal node before it is\n", - "split into four leaf notes. As the quadtree is generated, a leaf node containing\n", - "usable index points will be created as points are added. If the number of points\n", - "in this leaf exceeds `max_size`, the leaf will be subdivided, with four new\n", - "leaves added and the original node removed from the set of leaves. This number is\n", - "probably optimized in most datasets by making it a significant fraction of the\n", - "optimal leaf size computation from above. Consider $10,000,000 / 4^7 / 4 = 153$.\n", - "\n", - "### [cuspatial.quadtree_on_points](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_on_points)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "0 POINT (33.90371 -0.95000)\n", + "1 POINT (34.07262 -1.05982)\n", + "2 POINT (37.69869 -3.09699)\n", + "3 POINT (37.76690 -3.67712)\n", + "4 POINT (39.20222 -4.67677)\n", + "dtype: geometry\n" + ] + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "gpu_dataframe = cuspatial.from_geopandas(host_dataframe)\n", + "geometry = gpu_dataframe['geometry']\n", + "points = cuspatial.GeoSeries.from_points_xy(geometry.polygons.xy)\n", + "mean_x, std_x = (geometry.polygons.x.mean(), geometry.polygons.x.std())\n", + "mean_y, std_y = (geometry.polygons.y.mean(), geometry.polygons.y.std())\n", + "avg_points = cuspatial.points_in_spatial_window(\n", + " points,\n", + " mean_x - std_x,\n", + " mean_x + std_x,\n", + " mean_y - std_y,\n", + " mean_y + std_y\n", + ")\n", + "print(avg_points.head())" + ] + }, + { + "cell_type": "markdown", + "id": "5027a3dd-78bb-4d17-af94-506d0ed689c8", + "metadata": {}, + "source": [ + "With some careful grouping, one can reconstruct the original complete polygons that fall within the range." + ] + }, + { + "cell_type": "markdown", + "id": "3b33ce2b-965f-42a1-a89e-66d7ca80d907", + "metadata": {}, + "source": [ + "## Set Operations" + ] + }, + { + "cell_type": "markdown", + "id": "d73548f3-c9bb-43ff-9788-858f3b7d08e4", + "metadata": {}, + "source": [ + "### Linestring Intersections\n", + "\n", + "cuSpatial provides a linestring-linestring intersection algorithm to compute the overlapping geometries between two linestrings.\n", + "The API also returns the ids for each returned geometry to help user to trace back the source geometry." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "cc72a44d-a9bf-4432-9898-de899ac45869", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from cuspatial.core.binops.intersection import pairwise_linestring_intersection\n", + "\n", + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "usa_boundary = cuspatial.from_geopandas(host_dataframe[host_dataframe.name == \"United States of America\"].geometry.boundary)\n", + "canada_boundary = cuspatial.from_geopandas(host_dataframe[host_dataframe.name == \"Canada\"].geometry.boundary)\n", + "\n", + "list_offsets, geometries, look_back_ids = pairwise_linestring_intersection(usa_boundary, canada_boundary)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1125fd17-afe1-4b9c-b48d-8842dd3700b3", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 23, - "id": "e3a0a9a3-0bdd-4f05-bcb5-7db4b99a44a3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 1507\n", - "1 1726\n", - "2 4242\n", - "3 7371\n", - "4 11341\n", - "dtype: uint32\n", - " key level is_internal_node length offset\n", - "0 0 0 True 4 2\n", - "1 1 0 True 2 6\n", - "2 0 1 True 4 8\n", - "3 1 1 True 4 12\n", - "4 2 1 True 2 16\n" - ] - } - ], - "source": [ - "x_points = (cupy.random.random(10000000) - 0.5) * 360\n", - "y_points = (cupy.random.random(10000000) - 0.5) * 180\n", - "xy = cudf.DataFrame({\"x\": x_points, \"y\": y_points}).interleave_columns()\n", - "points = cuspatial.GeoSeries.from_points_xy(xy)\n", - "\n", - "scale = 5\n", - "max_depth = 7\n", - "max_size = 125\n", - "point_indices, quadtree = cuspatial.quadtree_on_points(points,\n", - " x_points.min(),\n", - " x_points.max(),\n", - " y_points.min(),\n", - " y_points.max(),\n", - " scale,\n", - " max_depth,\n", - " max_size)\n", - "print(point_indices.head())\n", - "print(quadtree.head())" + "data": { + "text/plain": [ + "\n", + "[\n", + " 0,\n", + " 144\n", + "]\n", + "dtype: int32" ] - }, + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The first integer series shows that the result contains 1 row (since we only have 1 pair of linestrings as input).\n", + "# This row contains 144 geometires.\n", + "list_offsets" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b281e3bb-42d4-4d60-9cb2-b7dcc20b4776", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "0ab7e64d-1199-498c-b020-b3e7393337a5", - "metadata": {}, - "source": [ - "### Indexed Spatial Joins\n", - "\n", - "The quadtree spatial index (`point_indices` and `quadtree`) is used by `quadtree_point_in_polygon`\n", - "and `quadtree_point_to_nearest_linestring` to accelerate larger spatial joins. \n", - "`quadtree_point_in_polygon` depends on a number of intermediate products calculated here using the\n", - "following functions.\n", - "\n", - "### [cuspatial.join_quadtree_and_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.join_quadtree_and_bounding_boxes)\n", - "### [cuspatial.quadtree_point_in_polygon](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_point_in_polygon)" + "data": { + "text/plain": [ + "0 POINT (-130.53611 54.80275)\n", + "1 POINT (-130.53611 54.80278)\n", + "2 POINT (-130.53611 54.80275)\n", + "3 POINT (-129.98000 55.28500)\n", + "4 POINT (-130.53611 54.80278)\n", + " ... \n", + "139 LINESTRING (-113.00000 49.00000, -113.00000 49...\n", + "140 LINESTRING (-83.89077 46.11693, -83.61613 46.1...\n", + "141 LINESTRING (-116.04818 49.00000, -116.04818 49...\n", + "142 LINESTRING (-120.00000 49.00000, -117.03121 49...\n", + "143 LINESTRING (-122.84000 49.00000, -120.00000 49...\n", + "Length: 144, dtype: geometry" ] - }, + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The second element is a geoseries that contains the intersecting geometries, with 144 rows, including points and linestrings.\n", + "geometries" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "e19873b9-2614-4242-ad67-caa47f807d04", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 24, - "id": "023bd25a-35be-435d-ab0b-ecbd7a47e147", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Empty DataFrame\n", - "Columns: [polygon_index, point_index]\n", - "Index: []\n" - ] - } + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
lhs_linestring_idlhs_segment_idrhs_linestring_idrhs_segment_id
0[8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, ...[18, 16, 18, 15, 17, 137, 14, 16, 13, 15, 14, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...[9, 10, 10, 11, 11, 28, 12, 12, 13, 13, 14, 15...
\n", + "
" ], - "source": [ - "polygons = gpu_dataframe['geometry']\n", - "\n", - "poly_bboxes = cuspatial.polygon_bounding_boxes(\n", - " polygons\n", - ")\n", - "intersections = cuspatial.join_quadtree_and_bounding_boxes(\n", - " quadtree,\n", - " poly_bboxes,\n", - " polygons.polygons.x.min(),\n", - " polygons.polygons.x.max(),\n", - " polygons.polygons.y.min(),\n", - " polygons.polygons.y.max(),\n", - " scale,\n", - " max_depth\n", - ")\n", - "polygons_and_points = cuspatial.quadtree_point_in_polygon(\n", - " intersections,\n", - " quadtree,\n", - " point_indices,\n", - " points,\n", - " polygons\n", - ")\n", - "print(polygons_and_points.head())" + "text/plain": [ + " lhs_linestring_id \\\n", + "0 [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, ... \n", + "\n", + " lhs_segment_id \\\n", + "0 [18, 16, 18, 15, 17, 137, 14, 16, 13, 15, 14, ... \n", + "\n", + " rhs_linestring_id \\\n", + "0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... \n", + "\n", + " rhs_segment_id \n", + "0 [9, 10, 10, 11, 11, 28, 12, 12, 13, 13, 14, 15... " ] - }, - { - "cell_type": "markdown", - "id": "aa1552a1-fe4b-4d30-b76f-054a060593ae", - "metadata": {}, - "source": [ - "You can see above that polygon 270 maps to the first 5 points. In order to bring this back to \n", - "a specific row of the original dataframe, the individual polygons must be mapped back to their \n", - "original MultiPolygon row. This is left an an exercise.\n", - "\n", - "### [cuspatial.quadtree_point_to_nearest_linestring](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_point_to_nearest_linestring)\n", - "\n", - "`cuspatial.quadtree_point_to_nearest_linestring` can be used to find the Polygon or Linestring \n", - "nearest to a set of points from another set of mixed geometries. " - ] - }, + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The third element is a dataframe that contains IDs to the input segments and linestrings, 4 for each result row.\n", + "# Each represents ids to lhs, rhs linestring and segment ids.\n", + "look_back_ids" + ] + }, + { + "cell_type": "markdown", + "id": "a17bd64a", + "metadata": {}, + "source": [ + "## Spatial Joins\n", + "\n", + "cuSpatial provides a number of functions to facilitate high-performance spatial joins, \n", + "including unindexed and quadtree-indexed point-in-polygon and quadtree-indexed point to nearest\n", + "linestring.\n", + "\n", + "The API for spatial joins does not yet match GeoPandas, but with knowledge of cuSpatial data formats\n", + "you can call `cuspatial.point_in_polygon` for large numbers of points on 32 polygons or less, or\n", + "call `cuspatial.quadtree_point_in_polygon` for large numbers of points and polygons. \n", + "\n", + "### Unindexed Point-in-polygon Join\n", + "\n", + "### [cuspatial.point_in_polygon](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.point_in_polygon)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "bf7b2256", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 25, - "id": "784aff8e-c9ed-4a81-aa87-bf301b3b90af", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "host_countries = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", - "host_cities = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_cities\"))\n", - "gpu_countries = cuspatial.from_geopandas(host_countries[host_countries['geometry'].type == \"Polygon\"])\n", - "gpu_cities = cuspatial.from_geopandas(host_cities[host_cities['geometry'].type == 'Point'])" + "data": { + "text/plain": [ + "1 11896\n", + "2 1268\n", + "5 50835\n", + "6 7792\n", + "11 29318\n", + "dtype: int64" ] - }, + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "host_dataframe = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "single_polygons = host_dataframe[host_dataframe['geometry'].type == \"Polygon\"]\n", + "gpu_dataframe = cuspatial.from_geopandas(single_polygons)\n", + "x_points = (cupy.random.random(10000000) - 0.5) * 360\n", + "y_points = (cupy.random.random(10000000) - 0.5) * 180\n", + "xy = cudf.DataFrame({\"x\": x_points, \"y\": y_points}).interleave_columns()\n", + "points = cuspatial.GeoSeries.from_points_xy(xy)\n", + "\n", + "short_dataframe = gpu_dataframe.iloc[0:31]\n", + "geometry = short_dataframe['geometry']\n", + "\n", + "points_in_polygon = cuspatial.point_in_polygon(\n", + " points, geometry\n", + ")\n", + "sum_of_points_in_polygons_0_to_31 = points_in_polygon.sum()\n", + "sum_of_points_in_polygons_0_to_31.head()" + ] + }, + { + "cell_type": "markdown", + "id": "fd9c4eef", + "metadata": {}, + "source": [ + "cuSpatial includes another join algorithm, `quadtree_point_in_polygon` that uses an indexing \n", + "quadtree for faster calculations. `quadtree_point_in_polygon` also supports a number of \n", + "polygons limited only by memory constraints." + ] + }, + { + "cell_type": "markdown", + "id": "387565b3-75ae-4789-a950-daffc2d4da01", + "metadata": {}, + "source": [ + "### Quadtree Indexing\n", + "\n", + "The indexing module is used to create a spatial quadtree. Use \n", + "```\n", + "cuspatial.quadtree_on_points(\n", + " points,\n", + " x_min,\n", + " x_max,\n", + " y_min,\n", + " y_max,\n", + " scale,\n", + " max_depth,\n", + " max_size\n", + ")\n", + "```\n", + "to create the quadtree object that is used by the `quadtree_point_in_polygon` \n", + "function in the `join` module.\n", + "\n", + "The function uses a set of points and a user-defined bounding box to build an \n", + "indexing quad tree. Be sure to adjust the parameters appropriately, with larger \n", + "parameter values for larger datasets.\n", + "\n", + "`scale`: A scaling function that increases the size of the point space from an \n", + "origin defined by `{x_min, y_min}`. This can increase the likelihood of generating \n", + "well-separated quads.\n", + "\n", + "`max_depth`: In order for a quadtree to index points effectively, it must have a\n", + "depth that is log-scaled with the size of the number of points. Each level of the \n", + "quad tree contains 4 quads. The number of available quads $q$ for indexing is then \n", + "equal to $q = 4^{d}$ where $d$ is the `max_depth` parameter. With an input size \n", + "of `10m` points and `max_depth = 7`, $\\frac{10^6}{4^7}$ points will be most \n", + "efficiently packed into the leaves of the quad tree.\n", + "\n", + "`max_size`: The maximum number of points allowed in an internal node before it is\n", + "split into four leaf notes. As the quadtree is generated, a leaf node containing\n", + "usable index points will be created as points are added. If the number of points\n", + "in this leaf exceeds `max_size`, the leaf will be subdivided, with four new\n", + "leaves added and the original node removed from the set of leaves. This number is\n", + "probably optimized in most datasets by making it a significant fraction of the\n", + "optimal leaf size computation from above. Consider $10,000,000 / 4^7 / 4 = 153$.\n", + "\n", + "### [cuspatial.quadtree_on_points](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_on_points)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "e3a0a9a3-0bdd-4f05-bcb5-7db4b99a44a3", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 26, - "id": "fea24c78-cf5c-45c6-b860-338238e61323", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " point_index linestring_index distance\n", - "0 0 21 10.857363\n", - "1 1 21 10.937690\n", - "2 2 19 0.522859\n", - "3 3 19 0.050204\n", - "4 4 129 0.104261\n" - ] - } - ], - "source": [ - "polygons = gpu_countries['geometry'].polygons\n", - "\n", - "boundaries = cuspatial.GeoSeries.from_linestrings_xy(\n", - " cudf.DataFrame({\"x\": polygons.x, \"y\": polygons.y}).interleave_columns(),\n", - " polygons.ring_offset,\n", - " cupy.arange(len(polygons.ring_offset))\n", - ")\n", - "\n", - "point_indices, quadtree = cuspatial.quadtree_on_points(gpu_cities['geometry'],\n", - " polygons.x.min(),\n", - " polygons.x.max(),\n", - " polygons.y.min(),\n", - " polygons.y.max(),\n", - " scale,\n", - " max_depth,\n", - " max_size)\n", - "poly_bboxes = cuspatial.linestring_bounding_boxes(\n", - " boundaries,\n", - " 2.0\n", - ")\n", - "intersections = cuspatial.join_quadtree_and_bounding_boxes(\n", - " quadtree,\n", - " poly_bboxes,\n", - " polygons.x.min(),\n", - " polygons.x.max(),\n", - " polygons.y.min(),\n", - " polygons.y.max(),\n", - " scale,\n", - " max_depth\n", - ")\n", - "result = cuspatial.quadtree_point_to_nearest_linestring(\n", - " intersections,\n", - " quadtree,\n", - " point_indices,\n", - " gpu_cities['geometry'],\n", - " boundaries\n", - ")\n", - "print(result.head())" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1507\n", + "1 1726\n", + "2 4242\n", + "3 7371\n", + "4 11341\n", + "dtype: uint32\n", + " key level is_internal_node length offset\n", + "0 0 0 True 4 2\n", + "1 1 0 True 2 6\n", + "2 0 1 True 4 8\n", + "3 1 1 True 4 12\n", + "4 2 1 True 2 16\n" + ] + } + ], + "source": [ + "x_points = (cupy.random.random(10000000) - 0.5) * 360\n", + "y_points = (cupy.random.random(10000000) - 0.5) * 180\n", + "xy = cudf.DataFrame({\"x\": x_points, \"y\": y_points}).interleave_columns()\n", + "points = cuspatial.GeoSeries.from_points_xy(xy)\n", + "\n", + "scale = 5\n", + "max_depth = 7\n", + "max_size = 125\n", + "point_indices, quadtree = cuspatial.quadtree_on_points(points,\n", + " x_points.min(),\n", + " x_points.max(),\n", + " y_points.min(),\n", + " y_points.max(),\n", + " scale,\n", + " max_depth,\n", + " max_size)\n", + "print(point_indices.head())\n", + "print(quadtree.head())" + ] + }, + { + "cell_type": "markdown", + "id": "0ab7e64d-1199-498c-b020-b3e7393337a5", + "metadata": {}, + "source": [ + "### Indexed Spatial Joins\n", + "\n", + "The quadtree spatial index (`point_indices` and `quadtree`) is used by `quadtree_point_in_polygon`\n", + "and `quadtree_point_to_nearest_linestring` to accelerate larger spatial joins. \n", + "`quadtree_point_in_polygon` depends on a number of intermediate products calculated here using the\n", + "following functions.\n", + "\n", + "### [cuspatial.join_quadtree_and_bounding_boxes](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.join_quadtree_and_bounding_boxes)\n", + "### [cuspatial.quadtree_point_in_polygon](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_point_in_polygon)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "023bd25a-35be-435d-ab0b-ecbd7a47e147", + "metadata": { + "tags": [] + }, + "outputs": [ { - "cell_type": "markdown", - "id": "3e4e07f6", - "metadata": {}, - "source": [ - "_Images used with permission from Wikipedia Creative Commons_" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Empty DataFrame\n", + "Columns: [polygon_index, point_index]\n", + "Index: []\n" + ] } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - }, - "vscode": { - "interpreter": { - "hash": "ef2a625a21f49284d4111fd61c77079c8ec37c2ac9f170a08eb051e93ed3e888" - } + ], + "source": [ + "polygons = gpu_dataframe['geometry']\n", + "\n", + "poly_bboxes = cuspatial.polygon_bounding_boxes(\n", + " polygons\n", + ")\n", + "intersections = cuspatial.join_quadtree_and_bounding_boxes(\n", + " quadtree,\n", + " poly_bboxes,\n", + " polygons.polygons.x.min(),\n", + " polygons.polygons.x.max(),\n", + " polygons.polygons.y.min(),\n", + " polygons.polygons.y.max(),\n", + " scale,\n", + " max_depth\n", + ")\n", + "polygons_and_points = cuspatial.quadtree_point_in_polygon(\n", + " intersections,\n", + " quadtree,\n", + " point_indices,\n", + " points,\n", + " polygons\n", + ")\n", + "print(polygons_and_points.head())" + ] + }, + { + "cell_type": "markdown", + "id": "aa1552a1-fe4b-4d30-b76f-054a060593ae", + "metadata": {}, + "source": [ + "You can see above that polygon 270 maps to the first 5 points. In order to bring this back to \n", + "a specific row of the original dataframe, the individual polygons must be mapped back to their \n", + "original MultiPolygon row. This is left an an exercise.\n", + "\n", + "### [cuspatial.quadtree_point_to_nearest_linestring](https://docs.rapids.ai/api/cuspatial/nightly/api_docs/spatial.html#cuspatial.quadtree_point_to_nearest_linestring)\n", + "\n", + "`cuspatial.quadtree_point_to_nearest_linestring` can be used to find the Polygon or Linestring \n", + "nearest to a set of points from another set of mixed geometries. " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "784aff8e-c9ed-4a81-aa87-bf301b3b90af", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "host_countries = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "host_cities = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_cities\"))\n", + "gpu_countries = cuspatial.from_geopandas(host_countries[host_countries['geometry'].type == \"Polygon\"])\n", + "gpu_cities = cuspatial.from_geopandas(host_cities[host_cities['geometry'].type == 'Point'])" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "fea24c78-cf5c-45c6-b860-338238e61323", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " point_index linestring_index distance\n", + "0 0 21 10.857363\n", + "1 1 21 10.937690\n", + "2 2 19 0.522859\n", + "3 3 19 0.050204\n", + "4 4 129 0.104261\n" + ] } + ], + "source": [ + "polygons = gpu_countries['geometry'].polygons\n", + "\n", + "boundaries = cuspatial.GeoSeries.from_linestrings_xy(\n", + " cudf.DataFrame({\"x\": polygons.x, \"y\": polygons.y}).interleave_columns(),\n", + " polygons.ring_offset,\n", + " cupy.arange(len(polygons.ring_offset))\n", + ")\n", + "\n", + "point_indices, quadtree = cuspatial.quadtree_on_points(gpu_cities['geometry'],\n", + " polygons.x.min(),\n", + " polygons.x.max(),\n", + " polygons.y.min(),\n", + " polygons.y.max(),\n", + " scale,\n", + " max_depth,\n", + " max_size)\n", + "poly_bboxes = cuspatial.linestring_bounding_boxes(\n", + " boundaries,\n", + " 2.0\n", + ")\n", + "intersections = cuspatial.join_quadtree_and_bounding_boxes(\n", + " quadtree,\n", + " poly_bboxes,\n", + " polygons.x.min(),\n", + " polygons.x.max(),\n", + " polygons.y.min(),\n", + " polygons.y.max(),\n", + " scale,\n", + " max_depth\n", + ")\n", + "result = cuspatial.quadtree_point_to_nearest_linestring(\n", + " intersections,\n", + " quadtree,\n", + " point_indices,\n", + " gpu_cities['geometry'],\n", + " boundaries\n", + ")\n", + "print(result.head())" + ] + }, + { + "cell_type": "markdown", + "id": "3e4e07f6", + "metadata": {}, + "source": [ + "_Images used with permission from Wikipedia Creative Commons_" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file + "vscode": { + "interpreter": { + "hash": "ef2a625a21f49284d4111fd61c77079c8ec37c2ac9f170a08eb051e93ed3e888" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 77cfd5acb67918f044083ab5989d4b00d7efc22e Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 22 Mar 2023 11:27:00 -0700 Subject: [PATCH 35/36] add api doc --- .../user_guide/cuspatial_api_examples.ipynb | 185 ++++++++++++++++-- 1 file changed, 170 insertions(+), 15 deletions(-) diff --git a/docs/source/user_guide/cuspatial_api_examples.ipynb b/docs/source/user_guide/cuspatial_api_examples.ipynb index 761b11831..bd78b237f 100644 --- a/docs/source/user_guide/cuspatial_api_examples.ipynb +++ b/docs/source/user_guide/cuspatial_api_examples.ipynb @@ -889,6 +889,161 @@ "print(gpu_polygons.head())" ] }, + { + "cell_type": "markdown", + "id": "008d320d-ca47-459f-9fff-8769494c8a61", + "metadata": {}, + "source": [ + "### cuspatial.pairwise_point_polygon_distance\n", + "\n", + "Using WGS 84 Pseudo-Mercator, distances are in meters." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "258c9a8c-7fe3-4047-80b7-00878d9fb2f1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
pop_estcontinentnameiso_a3gdp_md_estgeometrydistance_fromdistance
0889953.0OceaniaFijiFJI5496MULTIPOLYGON (((20037508.343 -1812498.413, 200...Vatican City1.969350e+07
158005463.0AfricaTanzaniaTZA63177POLYGON ((3774143.866 -105758.362, 3792946.708...San Marino5.929777e+06
2603253.0AfricaW. SaharaESH907POLYGON ((-964649.018 3205725.605, -964597.245...Vaduz3.421172e+06
337589262.0North AmericaCanadaCAN1736425MULTIPOLYGON (((-13674486.249 6274861.394, -13...Lobamba1.296059e+07
4328239523.0North AmericaUnited States of AmericaUSA21433226MULTIPOLYGON (((-13674486.249 6274861.394, -13...Luxembourg8.174897e+06
\n", + "
" + ], + "text/plain": [ + " pop_est continent name iso_a3 gdp_md_est \\\n", + "0 889953.0 Oceania Fiji FJI 5496 \n", + "1 58005463.0 Africa Tanzania TZA 63177 \n", + "2 603253.0 Africa W. Sahara ESH 907 \n", + "3 37589262.0 North America Canada CAN 1736425 \n", + "4 328239523.0 North America United States of America USA 21433226 \n", + "\n", + " geometry distance_from \\\n", + "0 MULTIPOLYGON (((20037508.343 -1812498.413, 200... Vatican City \n", + "1 POLYGON ((3774143.866 -105758.362, 3792946.708... San Marino \n", + "2 POLYGON ((-964649.018 3205725.605, -964597.245... Vaduz \n", + "3 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Lobamba \n", + "4 MULTIPOLYGON (((-13674486.249 6274861.394, -13... Luxembourg \n", + "\n", + " distance \n", + "0 1.969350e+07 \n", + "1 5.929777e+06 \n", + "2 3.421172e+06 \n", + "3 1.296059e+07 \n", + "4 8.174897e+06 \n", + "(GPU)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cities = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_cities\")).to_crs(3857)\n", + "countries = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\")).to_crs(3857)\n", + "\n", + "gpu_cities = cuspatial.from_geopandas(cities)\n", + "gpu_countries = cuspatial.from_geopandas(countries)\n", + "\n", + "dist = cuspatial.pairwise_point_polygon_distance(\n", + " gpu_cities.geometry[:len(gpu_countries)], gpu_countries.geometry\n", + ")\n", + "\n", + "gpu_countries[\"distance_from\"] = cities.name\n", + "gpu_countries[\"distance\"] = dist\n", + "\n", + "gpu_countries.head()" + ] + }, { "attachments": { "351aea0c-f37e-4ab9-bad2-c67bce69b5c3.png": { @@ -910,7 +1065,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "d1ade9da-c9e2-45c4-9685-dffeda3fd358", "metadata": { "tags": [] @@ -975,7 +1130,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "cc72a44d-a9bf-4432-9898-de899ac45869", "metadata": { "tags": [] @@ -993,7 +1148,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "1125fd17-afe1-4b9c-b48d-8842dd3700b3", "metadata": { "tags": [] @@ -1002,7 +1157,7 @@ { "data": { "text/plain": [ - "\n", + "\n", "[\n", " 0,\n", " 144\n", @@ -1010,7 +1165,7 @@ "dtype: int32" ] }, - "execution_count": 19, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -1023,7 +1178,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "b281e3bb-42d4-4d60-9cb2-b7dcc20b4776", "metadata": { "tags": [] @@ -1046,7 +1201,7 @@ "Length: 144, dtype: geometry" ] }, - "execution_count": 20, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1058,7 +1213,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "e19873b9-2614-4242-ad67-caa47f807d04", "metadata": { "tags": [] @@ -1117,7 +1272,7 @@ "0 [9, 10, 10, 11, 11, 28, 12, 12, 13, 13, 14, 15... " ] }, - "execution_count": 21, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -1150,7 +1305,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "bf7b2256", "metadata": { "tags": [] @@ -1167,7 +1322,7 @@ "dtype: int64" ] }, - "execution_count": 22, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -1252,7 +1407,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "e3a0a9a3-0bdd-4f05-bcb5-7db4b99a44a3", "metadata": { "tags": [] @@ -1316,7 +1471,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "023bd25a-35be-435d-ab0b-ecbd7a47e147", "metadata": { "tags": [] @@ -1375,7 +1530,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "784aff8e-c9ed-4a81-aa87-bf301b3b90af", "metadata": { "tags": [] @@ -1390,7 +1545,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "fea24c78-cf5c-45c6-b860-338238e61323", "metadata": { "tags": [] From c22cb464fe0f736a636603df39606e7291d99522 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 22 Mar 2023 11:28:44 -0700 Subject: [PATCH 36/36] update docs --- python/cuspatial/cuspatial/core/spatial/distance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuspatial/cuspatial/core/spatial/distance.py b/python/cuspatial/cuspatial/core/spatial/distance.py index 56eac324b..5d61e0564 100644 --- a/python/cuspatial/cuspatial/core/spatial/distance.py +++ b/python/cuspatial/cuspatial/core/spatial/distance.py @@ -388,7 +388,7 @@ def pairwise_point_linestring_distance( def pairwise_point_polygon_distance(points: GeoSeries, polygons: GeoSeries): """Compute distance between pairs of (multi)points and (multi)polygons - The distance between a (multi)point and a (multi)polygons + The distance between a (multi)point and a (multi)polygon is defined as the shortest distance between every point in the multipoint and every edge of the (multi)polygon. If the multipoint and multipolygon intersects, the distance is 0.