Skip to content

Commit 5462365

Browse files
committed
feat: add for_each_point_pairwise_distance algorithm for pairwise distance calculations between geometries
1 parent 1d9db93 commit 5462365

8 files changed

Lines changed: 327 additions & 2 deletions

File tree

cxx/include/pyinterp/geometry/geographic/algorithms/for_each_point_distance.hpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ template <typename SourceGeometry, typename TargetGeometry,
3535
return result;
3636
}
3737

38+
/// @brief Calculate distance from each point to target using compile-time
39+
/// strategy for multi-point target
40+
/// @tparam Geometry Geometry type (MultiPoint, LineString, Ring)
41+
/// @tparam Method Strategy method to use
42+
/// @param[in] source Source geometry containing points
43+
/// @param[in] target Target geometry containing points (must have the same
44+
/// number of points as source)
45+
/// @param[in] spheroid Optional Spheroid for geodetic calculations
46+
/// @return Vector of distances for each point in meters
47+
template <typename Geometry, StrategyMethod Method>
48+
[[nodiscard]] inline auto for_each_point_pairwise_distance(
49+
const Geometry& source, const Geometry& target,
50+
const std::optional<Spheroid>& spheroid) -> Eigen::VectorXd {
51+
if (source.size() != target.size()) {
52+
throw std::invalid_argument(
53+
"Source and target geometries must have the same number of points.");
54+
}
55+
Eigen::VectorXd result(source.size());
56+
auto strategy = make_distance_strategy<Method>(make_spheroid(spheroid));
57+
for (std::size_t i = 0; i < source.size(); ++i) {
58+
result(i) = boost::geometry::distance(source[i], target[i], strategy);
59+
}
60+
return result;
61+
}
62+
3863
/// @brief Calculate distance from each point to target using runtime strategy
3964
/// @tparam SourceGeometry Source geometry type (MultiPoint, LineString, Ring)
4065
/// @tparam TargetGeometry Target geometry type
@@ -66,4 +91,36 @@ template <typename SourceGeometry, typename TargetGeometry>
6691
std::unreachable();
6792
}
6893

94+
/// @brief Calculate distance from each point to target using runtime strategy
95+
/// for multi-point target
96+
/// @tparam Geometry Geometry type (MultiPoint, LineString, Ring)
97+
/// @param[in] source Source geometry containing points
98+
/// @param[in] target Target geometry containing points (must have the same
99+
/// number of points as source)
100+
/// @param[in] spheroid Optional Spheroid for geodetic calculations
101+
/// @param[in] strategy Strategy method to use
102+
/// @return Vector of distances for each point in meters
103+
template <typename Geometry>
104+
[[nodiscard]] inline auto for_each_point_pairwise_distance(
105+
const Geometry& source, const Geometry& target,
106+
const std::optional<Spheroid>& spheroid, const StrategyMethod strategy)
107+
-> Eigen::VectorXd {
108+
using enum StrategyMethod;
109+
switch (strategy) {
110+
case kAndoyer:
111+
return for_each_point_pairwise_distance<Geometry, kAndoyer>(
112+
source, target, spheroid);
113+
case kKarney:
114+
return for_each_point_pairwise_distance<Geometry, kKarney>(source, target,
115+
spheroid);
116+
case kThomas:
117+
return for_each_point_pairwise_distance<Geometry, kThomas>(source, target,
118+
spheroid);
119+
case kVincenty:
120+
return for_each_point_pairwise_distance<Geometry, kVincenty>(
121+
source, target, spheroid);
122+
}
123+
std::unreachable();
124+
}
125+
69126
} // namespace pyinterp::geometry::geographic

cxx/src/pybind/geometry/cartesian/algorithm/for_each_point_distance_cartesian.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
#include <nanobind/eigen/dense.h>
77
#include <nanobind/nanobind.h>
88

9+
#include <utility>
10+
11+
#include "pyinterp/geometry/cartesian/linestring.hpp"
12+
#include "pyinterp/geometry/cartesian/multi_point.hpp"
13+
#include "pyinterp/geometry/cartesian/ring.hpp"
914
#include "pyinterp/pybind/geometry/algorithm_binding_helpers.hpp"
1015

1116
namespace nb = nanobind;
@@ -24,6 +29,19 @@ Calculate the distance from each point in a source geometry to a target geometry
2429
Array of distances in coordinate units.
2530
)doc";
2631

32+
constexpr auto kForEachPointPairwiseDistanceDoc = R"doc(
33+
Calculate pairwise distances between points of two geometries.
34+
35+
Args:
36+
geometry1: Source geometry containing points (MultiPoint, LineString, or
37+
Ring).
38+
geometry2: Target geometry containing points (must have the same number of
39+
points as geometry1).
40+
41+
Returns:
42+
Array of distances in coordinate units.
43+
)doc";
44+
2745
// Calculate distance from each point in source to target geometry
2846
template <typename SourceGeometry, typename TargetGeometry>
2947
[[nodiscard]] inline auto for_each_point_distance(const SourceGeometry& source,
@@ -36,13 +54,34 @@ template <typename SourceGeometry, typename TargetGeometry>
3654
return result;
3755
}
3856

57+
// Calculate pairwise distances for geometries of the same type.
58+
template <typename Geometry>
59+
[[nodiscard]] inline auto for_each_point_pairwise_distance(
60+
const Geometry& source, const Geometry& target) -> Eigen::VectorXd {
61+
if (source.size() != target.size()) {
62+
throw std::invalid_argument(
63+
"Source and target geometries must have the same number of points.");
64+
}
65+
Eigen::VectorXd result(source.size());
66+
for (std::size_t i = 0; i < source.size(); ++i) {
67+
result(i) = boost::geometry::distance(source[i], target[i]);
68+
}
69+
return result;
70+
}
71+
3972
auto init_for_each_point_distance(nb::module_& m) -> void {
4073
auto distance_impl = [](const auto& source,
4174
const auto& target) -> Eigen::VectorXd {
4275
nb::gil_scoped_release release;
4376
return for_each_point_distance(source, target);
4477
};
4578

79+
auto pairwise_distance_impl = [](const auto& source,
80+
const auto& target) -> Eigen::VectorXd {
81+
nb::gil_scoped_release release;
82+
return for_each_point_pairwise_distance(source, target);
83+
};
84+
4685
// Bind for MultiPoint
4786
geometry::pybind::define_for_each_point_single_source<
4887
decltype(distance_impl), MultiPoint, CONTAINER_TYPES(cartesian)>(
@@ -57,6 +96,15 @@ auto init_for_each_point_distance(nb::module_& m) -> void {
5796
geometry::pybind::define_for_each_point_single_source<
5897
decltype(distance_impl), Ring, CONTAINER_TYPES(cartesian)>(
5998
m, "for_each_point_distance", kForEachPointDistanceDoc, distance_impl);
99+
100+
// Bind pairwise distances for same-type geometries
101+
geometry::pybind::define_binary_predicate<
102+
decltype(pairwise_distance_impl),
103+
std::pair<cartesian::MultiPoint, cartesian::MultiPoint>,
104+
std::pair<cartesian::Ring, cartesian::Ring>,
105+
std::pair<cartesian::LineString, cartesian::LineString>>(
106+
m, "for_each_point_pairwise_distance", kForEachPointPairwiseDistanceDoc,
107+
std::move(pairwise_distance_impl));
60108
}
61109

62110
} // namespace pyinterp::geometry::cartesian::pybind

cxx/src/pybind/geometry/geographic/algorithm/for_each_point_distance_geographic.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ trade-offs.
3333
Array of distances in meters.
3434
)doc";
3535

36+
constexpr auto kForEachPointPairwiseDistanceDoc = R"doc(
37+
Calculate pairwise distances between points of two geometries.
38+
39+
Args:
40+
geometry1: Source geometry containing points (MultiPoint, LineString, or
41+
Ring).
42+
geometry2: Target geometry containing points (must have the same number of
43+
points as geometry1).
44+
spheroid: Optional spheroid for geodetic calculations. If not provided, uses
45+
WGS84 ellipsoid.
46+
strategy: Calculation strategy.
47+
48+
Returns:
49+
Array of distances in meters.
50+
)doc";
51+
3652
auto init_for_each_point_distance(nb::module_& m) -> void {
3753
auto distance_impl = [](const auto& source, const auto& target,
3854
const std::optional<Spheroid>& spheroid,
@@ -41,6 +57,14 @@ auto init_for_each_point_distance(nb::module_& m) -> void {
4157
return for_each_point_distance(source, target, spheroid, strategy);
4258
};
4359

60+
auto pairwise_distance_impl =
61+
[](const auto& source, const auto& target,
62+
const std::optional<Spheroid>& spheroid,
63+
const StrategyMethod strategy) -> Eigen::VectorXd {
64+
nb::gil_scoped_release release;
65+
return for_each_point_pairwise_distance(source, target, spheroid, strategy);
66+
};
67+
4468
// Bind for MultiPoint
4569
geometry::pybind::define_for_each_point_single_source_with_strategy<
4670
decltype(distance_impl), MultiPoint, Spheroid, StrategyMethod,
@@ -58,6 +82,15 @@ auto init_for_each_point_distance(nb::module_& m) -> void {
5882
decltype(distance_impl), Ring, Spheroid, StrategyMethod,
5983
CONTAINER_TYPES(geographic)>(m, "for_each_point_distance",
6084
kForEachPointDistanceDoc, distance_impl);
85+
86+
// Bind pairwise distances for same-type geometries
87+
geometry::pybind::define_binary_predicate_with_strategy<
88+
decltype(pairwise_distance_impl), Spheroid, StrategyMethod,
89+
std::pair<geographic::MultiPoint, geographic::MultiPoint>,
90+
std::pair<geographic::Ring, geographic::Ring>,
91+
std::pair<geographic::LineString, geographic::LineString>>(
92+
m, "for_each_point_pairwise_distance", kForEachPointPairwiseDistanceDoc,
93+
std::move(pairwise_distance_impl));
6194
}
6295

6396
} // namespace pyinterp::geometry::geographic::pybind

docs/source/api/geometry.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ Vectorized spatial predicates for geographic geometries.
122122
algorithms.for_each_point_within
123123
algorithms.for_each_point_covered_by
124124
algorithms.for_each_point_distance
125+
algorithms.for_each_point_pairwise_distance
125126

126127
Distance Strategies
127128
^^^^^^^^^^^^^^^^^^^
@@ -220,6 +221,7 @@ Vectorized spatial predicates for Cartesian geometries.
220221
algorithms.for_each_point_within
221222
algorithms.for_each_point_covered_by
222223
algorithms.for_each_point_distance
224+
algorithms.for_each_point_pairwise_distance
223225

224226
Buffer Strategies
225227
^^^^^^^^^^^^^^^^^

pyinterp/core/geometry/cartesian/algorithms.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,18 @@ def for_each_point_distance(
334334
source: MultiPoint | LineString | Ring,
335335
container: Box | Ring | Polygon | MultiPolygon,
336336
) -> NDArray1DFloat64: ...
337+
@overload
338+
def for_each_point_pairwise_distance(
339+
geometry1: Ring, geometry2: Ring
340+
) -> NDArray1DFloat64: ...
341+
@overload
342+
def for_each_point_pairwise_distance(
343+
geometry1: MultiPoint, geometry2: MultiPoint
344+
) -> NDArray1DFloat64: ...
345+
@overload
346+
def for_each_point_pairwise_distance(
347+
geometry1: LineString, geometry2: LineString
348+
) -> NDArray1DFloat64: ...
337349
def for_each_point_within(
338350
source: MultiPoint | LineString | Ring,
339351
container: Box | Ring | Polygon | MultiPolygon,

pyinterp/core/geometry/geographic/algorithms.pyi

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,27 @@ def for_each_point_distance(
651651
spheroid: Spheroid | None = None,
652652
strategy: Strategy = ...,
653653
) -> NDArray1DFloat64: ...
654+
@overload
655+
def for_each_point_pairwise_distance(
656+
geometry1: Ring,
657+
geometry2: Ring,
658+
spheroid: Spheroid | None = None,
659+
strategy: Strategy = Strategy.ANDOYER,
660+
) -> NDArray1DFloat64: ...
661+
@overload
662+
def for_each_point_pairwise_distance(
663+
geometry1: MultiPoint,
664+
geometry2: MultiPoint,
665+
spheroid: Spheroid | None = None,
666+
strategy: Strategy = Strategy.ANDOYER,
667+
) -> NDArray1DFloat64: ...
668+
@overload
669+
def for_each_point_pairwise_distance(
670+
geometry1: LineString,
671+
geometry2: LineString,
672+
spheroid: Spheroid | None = None,
673+
strategy: Strategy = Strategy.ANDOYER,
674+
) -> NDArray1DFloat64: ...
654675
def for_each_point_within(
655676
source: MultiPoint | LineString | Ring,
656677
container: Box | Ring | Polygon | MultiPolygon,

pyinterp/tests/core/geometry/cartesian/algorithm/for_each_points/test_distance.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
Polygon,
1717
Ring,
1818
)
19-
from .......core.geometry.cartesian.algorithms import for_each_point_distance
19+
from .......core.geometry.cartesian.algorithms import (
20+
for_each_point_distance,
21+
for_each_point_pairwise_distance,
22+
)
2023

2124

2225
class TestForEachPointDistance:
@@ -141,3 +144,65 @@ def test_for_each_point_distance_all_inside(self) -> None:
141144
assert len(result) == 4
142145
# All distances should be 0
143146
assert np.allclose(result, 0.0)
147+
148+
149+
class TestForEachPointPairwiseDistance:
150+
"""Tests for for_each_point_pairwise_distance algorithm."""
151+
152+
def test_for_each_point_pairwise_distance_multipoint(self) -> None:
153+
"""Test pairwise distances for multipoints."""
154+
geometry1 = MultiPoint(
155+
np.array([0.0, 1.0, 4.0]), np.array([0.0, 1.0, 4.0])
156+
)
157+
geometry2 = MultiPoint(
158+
np.array([0.0, 2.0, 4.0]), np.array([0.0, 2.0, 7.0])
159+
)
160+
161+
result = for_each_point_pairwise_distance(geometry1, geometry2)
162+
163+
assert isinstance(result, np.ndarray)
164+
assert result.dtype == np.float64
165+
assert len(result) == 3
166+
assert np.allclose(result, np.array([0.0, np.sqrt(2.0), 3.0]))
167+
168+
def test_for_each_point_pairwise_distance_linestring(self) -> None:
169+
"""Test pairwise distances for linestrings."""
170+
geometry1 = LineString(np.array([0.0, 2.0]), np.array([0.0, 2.0]))
171+
geometry2 = LineString(np.array([0.0, 2.0]), np.array([0.0, 5.0]))
172+
173+
result = for_each_point_pairwise_distance(geometry1, geometry2)
174+
175+
assert isinstance(result, np.ndarray)
176+
assert result.dtype == np.float64
177+
assert len(result) == 2
178+
assert np.allclose(result, np.array([0.0, 3.0]))
179+
180+
def test_for_each_point_pairwise_distance_ring(self) -> None:
181+
"""Test pairwise distances for rings."""
182+
geometry1 = Ring(
183+
np.array([0.0, 0.0, 1.0, 1.0, 0.0]),
184+
np.array([0.0, 1.0, 1.0, 0.0, 0.0]),
185+
)
186+
geometry2 = Ring(
187+
np.array([0.0, 0.0, 2.0, 2.0, 0.0]),
188+
np.array([0.0, 1.0, 1.0, 0.0, 0.0]),
189+
)
190+
191+
result = for_each_point_pairwise_distance(geometry1, geometry2)
192+
193+
assert isinstance(result, np.ndarray)
194+
assert result.dtype == np.float64
195+
assert len(result) == 5
196+
assert np.allclose(result, np.array([0.0, 0.0, 1.0, 1.0, 0.0]))
197+
198+
def test_for_each_point_pairwise_distance_size_mismatch(self) -> None:
199+
"""Test pairwise distances with different number of points."""
200+
geometry1 = MultiPoint(np.array([0.0, 1.0]), np.array([0.0, 1.0]))
201+
geometry2 = MultiPoint(np.array([0.0]), np.array([0.0]))
202+
203+
with pytest.raises(
204+
ValueError,
205+
match="Source and target geometries must have the same "
206+
"number of points",
207+
):
208+
for_each_point_pairwise_distance(geometry1, geometry2)

0 commit comments

Comments
 (0)