From 3d7dd928d6ba426d872ff41c77cf0d1271789372 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 8 Mar 2023 21:00:37 +0100 Subject: [PATCH 01/10] basic support for convex_hull --- CMakeLists.txt | 1 + src/accessors-geog.cpp | 32 ++++++++++++++++++++++++++++++++ src/spherely.cpp | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 src/accessors-geog.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 27c4a01..79f9b54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ endif() add_library(spherely MODULE src/geography.cpp + src/accessors-geog.cpp src/predicates.cpp src/spherely.cpp ) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp new file mode 100644 index 0000000..6942006 --- /dev/null +++ b/src/accessors-geog.cpp @@ -0,0 +1,32 @@ +#include + +#include "geography.hpp" +#include "pybind11.hpp" + +namespace py = pybind11; +namespace s2geog = s2geography; +using namespace spherely; + +PyObjectGeography convex_hull(PyObjectGeography a) { + const auto& a_ptr = a.as_geog_ptr()->geog(); + auto res = s2geog::s2_convex_hull(a_ptr); + auto res_geog = spherely::Geography(std::move(res)); + auto res_geog_unique = std::make_unique(std::move(res_geog)); + auto res_object = PyObjectGeography::as_py_object(std::move(res_geog_unique)); + return static_cast(res_object); +} + +void init_accessors(py::module& m) { + m.def("convex_hull", py::vectorize(&convex_hull), py::arg("a"), + R"pbdoc( + Returns True if A and B share any portion of space. + + Intersects implies that overlaps, touches and within are True. + + Parameters + ---------- + a, b : :py:class:`Geography` or array_like + Geography object(s) + + )pbdoc"); +} diff --git a/src/spherely.cpp b/src/spherely.cpp index 12c196e..df1d957 100644 --- a/src/spherely.cpp +++ b/src/spherely.cpp @@ -7,6 +7,7 @@ namespace py = pybind11; void init_geography(py::module&); void init_predicates(py::module&); +void init_accessors(py::module&); PYBIND11_MODULE(spherely, m) { m.doc() = R"pbdoc( @@ -19,6 +20,7 @@ PYBIND11_MODULE(spherely, m) { init_geography(m); init_predicates(m); + init_accessors(m); #ifdef VERSION_INFO m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); From 4f65fdf3ab934fe7f3acbdd86e9afb7d588ea0c8 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 8 Mar 2023 22:13:46 +0100 Subject: [PATCH 02/10] add tests --- src/accessors-geog.cpp | 15 ++++++--------- src/pybind11.hpp | 4 ++++ tests/test_accessors.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 tests/test_accessors.py diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 6942006..0a25211 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -9,24 +9,21 @@ using namespace spherely; PyObjectGeography convex_hull(PyObjectGeography a) { const auto& a_ptr = a.as_geog_ptr()->geog(); - auto res = s2geog::s2_convex_hull(a_ptr); - auto res_geog = spherely::Geography(std::move(res)); - auto res_geog_unique = std::make_unique(std::move(res_geog)); - auto res_object = PyObjectGeography::as_py_object(std::move(res_geog_unique)); + auto s2_obj = s2geog::s2_convex_hull(a_ptr); + auto geog_ptr = std::make_unique(Geography(std::move(s2_obj))); + auto res_object = PyObjectGeography::as_py_object(std::move(geog_ptr)); return static_cast(res_object); } void init_accessors(py::module& m) { m.def("convex_hull", py::vectorize(&convex_hull), py::arg("a"), R"pbdoc( - Returns True if A and B share any portion of space. - - Intersects implies that overlaps, touches and within are True. + Computes the convex hull of each geography. Parameters ---------- - a, b : :py:class:`Geography` or array_like - Geography object(s) + a : :py:class:`Geography` or array_like + Geography object )pbdoc"); } diff --git a/src/pybind11.hpp b/src/pybind11.hpp index 0b39d81..38ae43f 100644 --- a/src/pybind11.hpp +++ b/src/pybind11.hpp @@ -90,6 +90,10 @@ class PyObjectGeography : public py::object { return py::cast(std::move(geog_ptr)); } + // static PyObjectGeography as_py_object(std::unique_ptr geog_ptr) { + // return static_cast(py::cast(std::move(geog_ptr))); + // } + // Just check whether the object is a Geography // bool is_geog_ptr() const { return check_type(false); } diff --git a/tests/test_accessors.py b/tests/test_accessors.py new file mode 100644 index 0000000..23d7699 --- /dev/null +++ b/tests/test_accessors.py @@ -0,0 +1,32 @@ +import numpy as np + +import spherely + +import pytest + + +@pytest.mark.parametrize( + "geog, expected", + [ + # (spherely.Point(0, 0), spherely.Point(0, 0)), + ( + spherely.LineString([(0, 0), (0, 2), (2, 2)]), + spherely.Polygon([(0, 0), (0, 2), (2, 2)]) + ), + # ( + # spherely.Polygon([(0, 0), (0, 2), (2, 2), (1, 0.5)]), + # spherely.Polygon([(0, 0), (0, 2), (2, 2)]) + # ), + ], +) +def test_convex_hull(geog, expected) -> None: + # test array + scalar + actual = spherely.convex_hull(geog) + assert isinstance(actual, spherely.Geography) + assert spherely.equals(actual, expected) + + actual = spherely.convex_hull([geog]) + assert isinstance(actual, np.ndarray) + actual = actual[0] + assert isinstance(actual, spherely.Geography) + assert spherely.equals(actual, expected) From 0f5fb03e30a84d74082e9be1c43803090696c565 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 9 Mar 2023 07:27:10 +0100 Subject: [PATCH 03/10] cleanup --- src/accessors-geog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 0a25211..6685f37 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -10,7 +10,7 @@ using namespace spherely; PyObjectGeography convex_hull(PyObjectGeography a) { const auto& a_ptr = a.as_geog_ptr()->geog(); auto s2_obj = s2geog::s2_convex_hull(a_ptr); - auto geog_ptr = std::make_unique(Geography(std::move(s2_obj))); + auto geog_ptr = std::make_unique(std::move(s2_obj)); auto res_object = PyObjectGeography::as_py_object(std::move(geog_ptr)); return static_cast(res_object); } From 1cd93768511df6947b3df07d9ee8a6954ec8c350 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 10 Mar 2023 13:55:45 +0100 Subject: [PATCH 04/10] from_geog method + return Polygon --- src/accessors-geog.cpp | 5 ++--- src/pybind11.hpp | 10 ++++------ tests/test_accessors.py | 7 ++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 6685f37..5730f05 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -10,9 +10,8 @@ using namespace spherely; PyObjectGeography convex_hull(PyObjectGeography a) { const auto& a_ptr = a.as_geog_ptr()->geog(); auto s2_obj = s2geog::s2_convex_hull(a_ptr); - auto geog_ptr = std::make_unique(std::move(s2_obj)); - auto res_object = PyObjectGeography::as_py_object(std::move(geog_ptr)); - return static_cast(res_object); + auto geog_ptr = std::make_unique(std::move(s2_obj)); + return PyObjectGeography::from_geog(std::move(geog_ptr)); } void init_accessors(py::module& m) { diff --git a/src/pybind11.hpp b/src/pybind11.hpp index 38ae43f..06d2160 100644 --- a/src/pybind11.hpp +++ b/src/pybind11.hpp @@ -86,14 +86,12 @@ class PyObjectGeography : public py::object { // template ::value, bool> = true> - static py::object as_py_object(std::unique_ptr geog_ptr) { - return py::cast(std::move(geog_ptr)); + static PyObjectGeography from_geog(std::unique_ptr geog_ptr) { + auto pyobj = py::cast(std::move(geog_ptr)); + auto pyobj_geog = static_cast(pyobj); + return std::move(pyobj_geog); } - // static PyObjectGeography as_py_object(std::unique_ptr geog_ptr) { - // return static_cast(py::cast(std::move(geog_ptr))); - // } - // Just check whether the object is a Geography // bool is_geog_ptr() const { return check_type(false); } diff --git a/tests/test_accessors.py b/tests/test_accessors.py index 23d7699..b0b4dbc 100644 --- a/tests/test_accessors.py +++ b/tests/test_accessors.py @@ -20,13 +20,14 @@ ], ) def test_convex_hull(geog, expected) -> None: - # test array + scalar + # scalar actual = spherely.convex_hull(geog) - assert isinstance(actual, spherely.Geography) + assert isinstance(actual, spherely.Polygon) assert spherely.equals(actual, expected) + # array actual = spherely.convex_hull([geog]) assert isinstance(actual, np.ndarray) actual = actual[0] - assert isinstance(actual, spherely.Geography) + assert isinstance(actual, spherely.Polygon) assert spherely.equals(actual, expected) From 82a7e71e22fddbd9de0f896f976b68114b85ebd6 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 18 Mar 2023 20:26:16 +0100 Subject: [PATCH 05/10] Update src/accessors-geog.cpp Co-authored-by: Benoit Bovy --- src/accessors-geog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 5730f05..67b3b2c 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -10,7 +10,7 @@ using namespace spherely; PyObjectGeography convex_hull(PyObjectGeography a) { const auto& a_ptr = a.as_geog_ptr()->geog(); auto s2_obj = s2geog::s2_convex_hull(a_ptr); - auto geog_ptr = std::make_unique(std::move(s2_obj)); + auto geog_ptr = std::make_unique(std::move(s2_obj)); return PyObjectGeography::from_geog(std::move(geog_ptr)); } From 5a5d71420140d676180aa043358755eccb201f84 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 18 Mar 2023 20:36:54 +0100 Subject: [PATCH 06/10] update tests --- tests/test_accessors.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_accessors.py b/tests/test_accessors.py index b0b4dbc..da5c293 100644 --- a/tests/test_accessors.py +++ b/tests/test_accessors.py @@ -8,15 +8,14 @@ @pytest.mark.parametrize( "geog, expected", [ - # (spherely.Point(0, 0), spherely.Point(0, 0)), ( spherely.LineString([(0, 0), (0, 2), (2, 2)]), spherely.Polygon([(0, 0), (0, 2), (2, 2)]) ), - # ( - # spherely.Polygon([(0, 0), (0, 2), (2, 2), (1, 0.5)]), - # spherely.Polygon([(0, 0), (0, 2), (2, 2)]) - # ), + ( + spherely.Polygon([(0, 0), (2, 0), (2, 2), (1.5, 0.5)]), + spherely.Polygon([(0, 0), (2, 0), (2, 2)]) + ), ], ) def test_convex_hull(geog, expected) -> None: From 8f5a025b76686899112ff6ccef70416e54e26fed Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 18 Mar 2023 20:38:59 +0100 Subject: [PATCH 07/10] linting --- src/accessors-geog.cpp | 4 +++- src/pybind11.hpp | 5 ++--- tests/test_accessors.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 67b3b2c..5fd0589 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -15,7 +15,9 @@ PyObjectGeography convex_hull(PyObjectGeography a) { } void init_accessors(py::module& m) { - m.def("convex_hull", py::vectorize(&convex_hull), py::arg("a"), + m.def("convex_hull", + py::vectorize(&convex_hull), + py::arg("a"), R"pbdoc( Computes the convex hull of each geography. diff --git a/src/pybind11.hpp b/src/pybind11.hpp index c246f91..fc172dd 100644 --- a/src/pybind11.hpp +++ b/src/pybind11.hpp @@ -84,11 +84,10 @@ class PyObjectGeography : public py::object { // Note: pybind11's `type_caster>` implements // move semantics (Python takes ownership). // - template ::value, - bool> = true> + template ::value, bool> = true> static PyObjectGeography from_geog(std::unique_ptr geog_ptr) { auto pyobj = py::cast(std::move(geog_ptr)); - auto pyobj_geog = static_cast(pyobj); + auto pyobj_geog = static_cast(pyobj); return std::move(pyobj_geog); } diff --git a/tests/test_accessors.py b/tests/test_accessors.py index da5c293..6d62a4b 100644 --- a/tests/test_accessors.py +++ b/tests/test_accessors.py @@ -10,11 +10,11 @@ [ ( spherely.LineString([(0, 0), (0, 2), (2, 2)]), - spherely.Polygon([(0, 0), (0, 2), (2, 2)]) + spherely.Polygon([(0, 0), (0, 2), (2, 2)]), ), ( spherely.Polygon([(0, 0), (2, 0), (2, 2), (1.5, 0.5)]), - spherely.Polygon([(0, 0), (2, 0), (2, 2)]) + spherely.Polygon([(0, 0), (2, 0), (2, 2)]), ), ], ) From 11b0fbb447f4688ce54e3c58fa2a6e3f15e410ff Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 18 Mar 2023 21:17:32 +0100 Subject: [PATCH 08/10] add centroid and boundary --- src/accessors-geog.cpp | 42 +++++++++++++++++++++++++++++++++ src/geography.cpp | 13 ----------- src/geography.hpp | 13 +++++++++++ tests/test_accessors.py | 51 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 5fd0589..172b786 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -7,6 +7,22 @@ namespace py = pybind11; namespace s2geog = s2geography; using namespace spherely; +PyObjectGeography centroid(PyObjectGeography a) { + const auto& a_ptr = a.as_geog_ptr()->geog(); + auto s2_point = s2geog::s2_centroid(a_ptr); + std::unique_ptr point = + make_geography(s2_point); + return PyObjectGeography::from_geog(std::move(point)); +} + +PyObjectGeography boundary(PyObjectGeography a) { + const auto& a_ptr = a.as_geog_ptr()->geog(); + auto s2_obj = s2geog::s2_boundary(a_ptr); + // TODO return specific subclass + auto geog_ptr = std::make_unique(std::move(s2_obj)); + return PyObjectGeography::from_geog(std::move(geog_ptr)); +} + PyObjectGeography convex_hull(PyObjectGeography a) { const auto& a_ptr = a.as_geog_ptr()->geog(); auto s2_obj = s2geog::s2_convex_hull(a_ptr); @@ -15,6 +31,32 @@ PyObjectGeography convex_hull(PyObjectGeography a) { } void init_accessors(py::module& m) { + m.def("centroid", + py::vectorize(¢roid), + py::arg("a"), + R"pbdoc( + Computes the centroid of each geography. + + Parameters + ---------- + a : :py:class:`Geography` or array_like + Geography object + + )pbdoc"); + + m.def("boundary", + py::vectorize(&boundary), + py::arg("a"), + R"pbdoc( + Computes the boundary of each geography. + + Parameters + ---------- + a : :py:class:`Geography` or array_like + Geography object + + )pbdoc"); + m.def("convex_hull", py::vectorize(&convex_hull), py::arg("a"), diff --git a/src/geography.cpp b/src/geography.cpp index a165cde..1556092 100644 --- a/src/geography.cpp +++ b/src/geography.cpp @@ -46,19 +46,6 @@ S2Point to_s2point(const Point *vertex) { return vertex->s2point(); } -/* -** Helper to create Geography object wrappers. -** -** @tparam T1 The S2Geography wrapper type -** @tparam T2 This library wrapper type. -** @tparam S The S2Geometry type -*/ -template -std::unique_ptr make_geography(S &&s2_obj) { - S2GeographyPtr s2geog_ptr = std::make_unique(std::forward(s2_obj)); - return std::make_unique(std::move(s2geog_ptr)); -} - class PointFactory { public: static std::unique_ptr FromLatLonDegrees(double lat_degrees, double lon_degrees) { diff --git a/src/geography.hpp b/src/geography.hpp index f7ba645..5f8f851 100644 --- a/src/geography.hpp +++ b/src/geography.hpp @@ -113,6 +113,19 @@ class Polygon : public Geography { } }; +/* +** Helper to create Geography object wrappers. +** +** @tparam T1 The S2Geography wrapper type +** @tparam T2 This library wrapper type. +** @tparam S The S2Geometry type +*/ +template +std::unique_ptr make_geography(S&& s2_obj) { + S2GeographyPtr s2geog_ptr = std::make_unique(std::forward(s2_obj)); + return std::make_unique(std::move(s2geog_ptr)); +} + } // namespace spherely #endif // SPHERELY_GEOGRAPHY_H_ diff --git a/tests/test_accessors.py b/tests/test_accessors.py index 6d62a4b..205823a 100644 --- a/tests/test_accessors.py +++ b/tests/test_accessors.py @@ -5,6 +5,57 @@ import pytest +@pytest.mark.parametrize( + "geog, expected", + [ + (spherely.Point(0, 0), spherely.Point(0, 0)), + ( + spherely.LineString([(0, 0), (0, 2)]), + spherely.Point(0, 1), + ), + ( + spherely.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]), + spherely.Point(1, 1), + ), + ], +) +def test_centroid(geog, expected) -> None: + # scalar + actual = spherely.centroid(geog) + assert isinstance(actual, spherely.Point) + # TODO add some way of testing almost equality + # assert spherely.equals(actual, expected) + + # array + actual = spherely.centroid([geog]) + assert isinstance(actual, np.ndarray) + actual = actual[0] + assert isinstance(actual, spherely.Point) + # assert spherely.equals(actual, expected) + + +@pytest.mark.parametrize( + "geog, expected", + [ + (spherely.Point(0, 0), "GEOMETRYCOLLECTION EMPTY"), + (spherely.LineString([(0, 0), (0, 2), (2, 2)]), "MULTIPOINT ((0 0), (2 2))"), + ( + spherely.Polygon([(0, 0), (2, 0), (2, 2), (1.5, 0.5)]), + "LINESTRING (0.5 1.5, 2 2, 0 2, 0 0, 0.5 1.5)", + ), + ], +) +def test_boundary(geog, expected) -> None: + # scalar + actual = spherely.boundary(geog) + assert str(actual) == expected + + # array + actual = spherely.boundary([geog]) + assert isinstance(actual, np.ndarray) + assert str(actual[0]) == expected + + @pytest.mark.parametrize( "geog, expected", [ From 91ce98f678ad76362a46aa988616523d2bac5be6 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 18 Mar 2023 21:22:44 +0100 Subject: [PATCH 09/10] add type annotations --- src/spherely.pyi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spherely.pyi b/src/spherely.pyi index 64ea736..425280c 100644 --- a/src/spherely.pyi +++ b/src/spherely.pyi @@ -107,6 +107,12 @@ contains: _VFunc_Nin2_Nout1[Literal["contains"], bool, bool] within: _VFunc_Nin2_Nout1[Literal["within"], bool, bool] disjoint: _VFunc_Nin2_Nout1[Literal["disjoint"], bool, bool] +# geography accessors + +centroid: _VFunc_Nin1_Nout1[Literal["centroid"], Geography, Point] +boundary: _VFunc_Nin1_Nout1[Literal["boundary"], Geography, Geography] +convex_hull: _VFunc_Nin1_Nout1[Literal["convex_hull"], Geography, Polygon] + # temp (remove) def create(arg0: Iterable[float], arg1: Iterable[float]) -> npt.NDArray[Any]: ... From c3682711052713d86fc47ab077801ac1dd8810f2 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 19 Aug 2024 12:40:57 +0200 Subject: [PATCH 10/10] add to api docs --- docs/api.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index a8a9d2d..ea7ea46 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -42,6 +42,18 @@ Geography creation prepare destroy_prepared +.. _api_accessors: + +Geography accessors +------------------- + +.. autosummary:: + :toctree: _api_generated/ + + centroid + boundary + convex_hull + .. _api_predicates: Predicates