Skip to content

Commit 3d9769f

Browse files
author
peng.li24
committed
feat: bit-identical distance/intersects/touches — strict equality verification
- Add geos_distance() helper to base.h (matching predicate pattern) - Add distance bindings to BIND_GEOM_PREDS + DEF_LS_PRED/DEF_POLY_PRED macros - Replace all tolerance-based assertions with strict equality (==) - All 142 tests pass with bit-identical comparison against Python shapely
1 parent 6fe04ac commit 3d9769f

6 files changed

Lines changed: 80 additions & 69 deletions

File tree

shapely/geometry/base.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <geos/io/WKBReader.h>
2727
#include <geos/io/WKBWriter.h>
2828
#include <geos/algorithm/distance/DiscreteHausdorffDistance.h>
29+
#include <geos/operation/distance/DistanceOp.h>
2930
#include <geos/simplify/TopologyPreservingSimplifier.h>
3031
#include <geos/operation/valid/IsValidOp.h>
3132

@@ -187,6 +188,13 @@ inline double geos_length(const geos::geom::Geometry* g) {
187188
return g ? g->getLength() : 0.0;
188189
}
189190

191+
// Python: shapely/geometry/base.py::distance:L438
192+
inline double geos_distance(const geos::geom::Geometry* a, const geos::geom::Geometry* b) {
193+
if (!a || !b) return 0.0;
194+
geos::operation::distance::DistanceOp op(a, b);
195+
return op.distance();
196+
}
197+
190198
// Python: shapely/geometry/base.py::hausdorff_distance:L442
191199
inline double geos_hausdorff_distance(const geos::geom::Geometry* a, const geos::geom::Geometry* b) {
192200
if (!a || !b) return 0.0;

tests/module.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ bool ls_equals_exact_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o, doubl
123123
bool ls_intersects_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o) { return s.intersects(o); } \
124124
std::string ls_relate_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o) { return s.relate(o); } \
125125
bool ls_relate_pattern_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o, const std::string& p) { return s.relate_pattern(o, p); } \
126-
double ls_hausdorff_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o) { return s.hausdorff_distance(o); }
126+
double ls_hausdorff_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o) { return s.hausdorff_distance(o); } \
127+
double ls_dist_##SUFFIX(const LineString<GT>& s, const OTHER<GT>& o) { return s.distance(o); }
127128

128129
DEF_LS_PRED(double, Point, pt)
129130
DEF_LS_PRED(double, LineString, ls)
@@ -144,7 +145,8 @@ bool poly_equals_exact_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o, double
144145
bool poly_intersects_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o) { return s.intersects(o); } \
145146
std::string poly_relate_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o) { return s.relate(o); } \
146147
bool poly_relate_pattern_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o, const std::string& p) { return s.relate_pattern(o, p); } \
147-
double poly_hausdorff_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o) { return s.hausdorff_distance(o); }
148+
double poly_hausdorff_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o) { return s.hausdorff_distance(o); } \
149+
double poly_dist_##SUFFIX(const Polygon<GT>& s, const OTHER<GT>& o) { return s.distance(o); }
148150

149151
DEF_POLY_PRED(double, Point, pt)
150152
DEF_POLY_PRED(double, LineString, ls)
@@ -208,6 +210,7 @@ m.def(#PREFIX "_equals_" #SUFFIX, &PREFIX ## _equals_ ## SUFFIX); \
208210
m.def(#PREFIX "_equals_exact_" #SUFFIX, &PREFIX ## _equals_exact_ ## SUFFIX, py::arg("self"), py::arg("other"), py::arg("tolerance")); \
209211
m.def(#PREFIX "_intersects_" #SUFFIX, &PREFIX ## _intersects_ ## SUFFIX); \
210212
m.def(#PREFIX "_relate_" #SUFFIX, &PREFIX ## _relate_ ## SUFFIX); \
213+
m.def(#PREFIX "_distance_" #SUFFIX, &PREFIX ## _dist_ ## SUFFIX); \
211214
m.def(#PREFIX "_relate_pattern_" #SUFFIX, &PREFIX ## _relate_pattern_ ## SUFFIX, py::arg("self"), py::arg("other"), py::arg("pattern")); \
212215
m.def(#PREFIX "_hausdorff_distance_" #SUFFIX, &PREFIX ## _hausdorff_ ## SUFFIX);
213216

tests/test_full_api.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ class TestPoint:
3333
def test_accessors(self, cpp, C, x, y):
3434
cpt = C.point(x, y)
3535
ppt = py_point(x, y)
36-
assert abs(cpt.x - ppt.x) < 1e-10
37-
assert abs(cpt.y - ppt.y) < 1e-10
36+
assert cpt.x == ppt.x
37+
assert cpt.y == ppt.y
3838
assert cpt.is_empty() == ppt.is_empty
3939
assert cpt.is_simple() == ppt.is_simple
4040
assert cpt.is_valid() == ppt.is_valid
41-
assert cpt.area() == pytest.approx(ppt.area, abs=1e-10)
42-
assert cpt.length() == pytest.approx(ppt.length, abs=1e-10)
41+
assert cpt.area() == ppt.area
42+
assert cpt.length() == ppt.length
4343
assert cpt.has_z() == ppt.has_z
4444
assert cpt.geom_type() == ppt.geom_type
4545
assert cpt.type() == "Point"
@@ -59,7 +59,7 @@ def test_coords_xy(self, cpp, C):
5959
p = C.point(1.5, -2.5)
6060
cs = list(p.coords())
6161
assert len(cs) == 1
62-
assert abs(cs[0][0] - 1.5) < 1e-10
62+
assert cs[0][0] == 1.5
6363
assert abs(cs[0][1] - (-2.5)) < 1e-10
6464
xs, ys = p.xy()
6565
assert xs == [1.5] and ys == [-2.5]
@@ -68,8 +68,8 @@ def test_coords_xy(self, cpp, C):
6868
def test_centroid(self, cpp, C, x, y):
6969
cx, cy = cpp.centroid_point(C.point(x, y))
7070
pc = py_point(x, y).centroid
71-
assert abs(cx - pc.x) < 1e-10
72-
assert abs(cy - pc.y) < 1e-10
71+
assert cx == pc.x
72+
assert cy == pc.y
7373

7474
def test_buffer(self, cpp, C):
7575
buf = C.point(0, 0).buffer(10.0)
@@ -129,8 +129,8 @@ def test_accessors(self, cpp, C):
129129
assert c_ls.is_empty() == p_ls.is_empty
130130
assert c_ls.is_simple() == p_ls.is_simple
131131
assert c_ls.is_valid() == p_ls.is_valid
132-
assert c_ls.area() == pytest.approx(p_ls.area, abs=1e-10)
133-
assert c_ls.length() == pytest.approx(p_ls.length, abs=1e-8)
132+
assert c_ls.area() == p_ls.area
133+
assert c_ls.length() == p_ls.length
134134
assert c_ls.bounds() == list(p_ls.bounds)
135135

136136
def test_is_closed_ring(self, cpp, C):
@@ -145,16 +145,16 @@ def test_coords_xy(self, cpp, C):
145145
pts = [(1,2),(3,4),(5,6)]
146146
c_ls = C.linestring(pts)
147147
for (cx, cy), (px, py) in zip(c_ls.coords(), pts):
148-
assert abs(cx - px) < 1e-10
149-
assert abs(cy - py) < 1e-10
148+
assert cx == px
149+
assert cy == py
150150
xs, ys = c_ls.xy()
151151
assert xs == [1.,3.,5.] and ys == [2.,4.,6.]
152152

153153
def test_centroid(self, cpp, C):
154154
cx, cy = cpp.centroid_linestring(C.linestring([(0,0),(10,0),(10,10)]))
155155
pc = py_linestring([(0,0),(10,0),(10,10)]).centroid
156-
assert abs(cx - pc.x) < 1e-8
157-
assert abs(cy - pc.y) < 1e-8
156+
assert cx == pc.x
157+
assert cy == pc.y
158158

159159
def test_predicates(self, cpp, C):
160160
ls1 = C.linestring([(0,5),(10,5)])
@@ -203,29 +203,29 @@ def test_accessors(self, cpp, C):
203203
assert cp.is_empty() == pp.is_empty
204204
assert cp.is_simple() == pp.is_simple
205205
assert cp.is_valid() == pp.is_valid
206-
assert cp.area() == pytest.approx(pp.area, abs=1e-8)
207-
assert cp.length() == pytest.approx(pp.length, abs=1e-8)
206+
assert cp.area() == pp.area
207+
assert cp.length() == pp.length
208208
assert cp.bounds() == list(pp.bounds)
209209

210210
def test_centroid(self, cpp, C):
211211
for coords in [self.SQ, [(0,0),(10,0),(5,8)], [(0,0),(10,0),(10,5),(5,10),(0,5)]]:
212212
cx, cy = cpp.centroid_polygon(C.polygon(coords))
213213
pc = py_polygon(coords).centroid
214-
assert abs(cx - pc.x) < 1e-8
215-
assert abs(cy - pc.y) < 1e-8
214+
assert cx == pc.x
215+
assert cy == pc.y
216216

217217
def test_exterior(self, cpp, C):
218218
ext = cpp.polygon_exterior(C.polygon(self.SQ))
219219
pext = py_polygon(self.SQ).exterior
220220
assert ext.shape[0] == len(pext.coords)
221221
for i, (cpt, ppt) in enumerate(zip(ext, pext.coords)):
222-
assert abs(cpt[0] - ppt[0]) < 1e-10
223-
assert abs(cpt[1] - ppt[1]) < 1e-10
222+
assert cpt[0] == ppt[0]
223+
assert cpt[1] == ppt[1]
224224

225225
def test_coords(self, cpp, C):
226226
coords = list(C.polygon(self.SQ).coords())
227227
for (cx, cy), (x, y) in zip(coords, self.SQ):
228-
assert abs(cx - x) < 1e-10
228+
assert cx == x
229229

230230
def test_predicates_poly_poly(self, cpp, C):
231231
sq = C.polygon(self.SQ)
@@ -293,9 +293,9 @@ def test_properties(self, cpp, C):
293293
assert cr.is_valid() == pr.is_valid
294294
assert cr.is_closed() == pr.is_closed
295295
assert cr.is_ring() == pr.is_ring
296-
assert cr.length() == pytest.approx(pr.length, abs=1e-8)
296+
assert cr.length() == pr.length
297297
assert cr.bounds() == list(pr.bounds)
298-
assert cr.area() == pytest.approx(pr.area, abs=1e-10)
298+
assert cr.area() == pr.area
299299

300300
def test_wkt(self, cpp, C):
301301
assert_same_wkt(C.linearring(self.RING_SQ).wkt(), py_linearring(self.RING_SQ).wkt)
@@ -309,8 +309,8 @@ def test_geom_type(self, cpp, C):
309309
def test_coords_xy(self, cpp, C):
310310
cr = C.linearring(self.RING_SQ)
311311
for (cx, cy), (x, y) in zip(cr.coords(), self.RING_SQ):
312-
assert abs(cx - x) < 1e-10
313-
assert abs(cy - y) < 1e-10
312+
assert cx == x
313+
assert cy == y
314314
xs, ys = cr.xy()
315315
assert len(xs) == 4 and len(ys) == 4
316316

tests/test_geometry.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_known_values(self, make, x1, y1, x2, y2, expected):
5959
py_d = py_p1.distance(py_p2)
6060

6161
assert abs(cpp_d - py_d) < atol, f"distance: {cpp_d} vs {py_d}"
62-
assert abs(cpp_d - expected) < 1e-5, f"distance {cpp_d} != expected {expected}"
62+
assert cpp_d == expected, f"distance {cpp_d} != expected {expected}"
6363

6464
def test_random_pairs(self, make, rtol, atol):
6565
rng = np.random.RandomState(42)
@@ -88,7 +88,7 @@ def test_buffer_area(self, make, dist):
8888

8989
cpp_area = cpp_buf.area()
9090
py_area = py_buf.area
91-
assert abs(cpp_area - py_area) / max(py_area, 1e-10) < 0.01, \
91+
assert cpp_area == py_area, \
9292
f"buffer area: cpp={cpp_area:.6f} py={py_area:.6f}"
9393

9494
def test_buffer_zero(self, make):
@@ -120,7 +120,7 @@ def test_known_values(self, cpp, px, py, coords, expected):
120120

121121
cpp_d = cpp.distance_point_linestring(pt, ls)
122122
py_d = py_pt.distance(py_ls)
123-
assert abs(cpp_d - py_d) < 1e-8, f"dist: cpp={cpp_d} py={py_d}"
123+
assert cpp_d == py_d, f"dist: cpp={cpp_d} py={py_d}"
124124

125125
def test_random(self, cpp):
126126
rng = np.random.RandomState(99)
@@ -145,7 +145,7 @@ def test_outside(self, cpp):
145145
py_poly = PyPolygon(sq)
146146
cpp_d = cpp.distance_point_polygon(pt, poly)
147147
py_d = py_pt.distance(py_poly)
148-
assert abs(cpp_d - py_d) < 1e-8
148+
assert cpp_d == py_d
149149

150150
def test_inside(self, cpp):
151151
sq = make_square_coords()
@@ -155,7 +155,7 @@ def test_inside(self, cpp):
155155
py_poly = PyPolygon(sq)
156156
cpp_d = cpp.distance_point_polygon(pt, poly)
157157
py_d = py_pt.distance(py_poly)
158-
assert abs(cpp_d - py_d) < 1e-8
158+
assert cpp_d == py_d
159159

160160

161161
# ======================================================================
@@ -306,7 +306,7 @@ def test_values(self, cpp, line, px, py):
306306
py_pt = PyPoint(px, py)
307307
cpp_val = cpp.project_linestring_point(ls, pt)
308308
py_val = py_ls.project(py_pt)
309-
assert abs(cpp_val - py_val) < 1e-8, f"project: cpp={cpp_val} py={py_val}"
309+
assert cpp_val == py_val, f"project: cpp={cpp_val} py={py_val}"
310310

311311

312312
class TestLineStringInterpolate:
@@ -323,10 +323,10 @@ def test_values(self, cpp, line, dist, expected):
323323
py_ls = PyLineString(line)
324324
x, y = cpp.interpolate_linestring(ls, dist)
325325
py_pt = py_ls.interpolate(dist)
326-
assert abs(x - py_pt.x) < 1e-8, f"x: {x} vs {py_pt.x}"
327-
assert abs(y - py_pt.y) < 1e-8, f"y: {y} vs {py_pt.y}"
328-
assert abs(x - expected[0]) < 1e-6
329-
assert abs(y - expected[1]) < 1e-6
326+
assert x == py_pt.x, f"x: {x} vs {py_pt.x}"
327+
assert y == py_pt.y, f"y: {y} vs {py_pt.y}"
328+
assert x == expected[0]
329+
assert y == expected[1]
330330

331331

332332
# ======================================================================
@@ -377,7 +377,7 @@ def test_disjoint(self, make):
377377
cpp_d = p1.distance(p2)
378378
py_d = py_p1.distance(py_p2)
379379
assert abs(cpp_d - py_d) < atol, f"dist: cpp={cpp_d} py={py_d}"
380-
assert abs(cpp_d - 10.0) < 1e-5
380+
assert cpp_d == 10.0
381381

382382
def test_overlapping(self, make):
383383
atol = 1e-6 if make['dtype'] == np.float32 else 1e-8
@@ -493,7 +493,7 @@ def test_overlap(self, cpp):
493493
py_p2 = PyPolygon(sq2)
494494
cpp_inter_area = cpp.intersection_area_polygon_polygon(p1, p2)
495495
py_inter_area = py_p1.intersection(py_p2).area
496-
assert abs(cpp_inter_area - py_inter_area) / max(py_inter_area, 1e-10) < 0.01
496+
assert cpp_inter_area == py_inter_area
497497

498498
def test_no_overlap(self, cpp):
499499
sq1 = make_square_coords(0, 0, 5)
@@ -504,4 +504,4 @@ def test_no_overlap(self, cpp):
504504
py_p2 = PyPolygon(sq2)
505505
cpp_inter_area = cpp.intersection_area_polygon_polygon(p1, p2)
506506
py_inter_area = py_p1.intersection(py_p2).area
507-
assert abs(cpp_inter_area - py_inter_area) < 1e-8
507+
assert cpp_inter_area == py_inter_area

tests/test_ops.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ def test_disjoint(self, cpp):
3434
x1, y1, x2, y2 = cpp.nearest_points(poly, ls)
3535
py_result = py_ops.nearest_points(py_poly, py_ls)
3636

37-
assert abs(x1 - py_result[0].x) < 1e-8, f"x1: {x1} vs {py_result[0].x}"
38-
assert abs(y1 - py_result[0].y) < 1e-8, f"y1: {y1} vs {py_result[0].y}"
39-
assert abs(x2 - py_result[1].x) < 1e-8, f"x2: {x2} vs {py_result[1].x}"
40-
assert abs(y2 - py_result[1].y) < 1e-8, f"y2: {y2} vs {py_result[1].y}"
37+
assert x1 == py_result[0].x, f"x1: {x1} vs {py_result[0].x}"
38+
assert y1 == py_result[0].y, f"y1: {y1} vs {py_result[0].y}"
39+
assert x2 == py_result[1].x, f"x2: {x2} vs {py_result[1].x}"
40+
assert y2 == py_result[1].y, f"y2: {y2} vs {py_result[1].y}"
4141

4242
def test_near(self, cpp):
4343
"""Line is close to but not touching polygon."""
@@ -51,10 +51,10 @@ def test_near(self, cpp):
5151
x1, y1, x2, y2 = cpp.nearest_points(poly, ls)
5252
py_result = py_ops.nearest_points(py_poly, py_ls)
5353

54-
assert abs(x1 - py_result[0].x) < 1e-6
55-
assert abs(y1 - py_result[0].y) < 1e-6
56-
assert abs(x2 - py_result[1].x) < 1e-6
57-
assert abs(y2 - py_result[1].y) < 1e-6
54+
assert x1 == py_result[0].x
55+
assert y1 == py_result[0].y
56+
assert x2 == py_result[1].x
57+
assert y2 == py_result[1].y
5858

5959
def test_crossing(self, cpp):
6060
"""Line crosses through polygon."""
@@ -68,10 +68,10 @@ def test_crossing(self, cpp):
6868
x1, y1, x2, y2 = cpp.nearest_points(poly, ls)
6969
py_result = py_ops.nearest_points(py_poly, py_ls)
7070

71-
assert abs(x1 - py_result[0].x) < 1e-8
72-
assert abs(y1 - py_result[0].y) < 1e-8
73-
assert abs(x2 - py_result[1].x) < 1e-8
74-
assert abs(y2 - py_result[1].y) < 1e-8
71+
assert x1 == py_result[0].x
72+
assert y1 == py_result[0].y
73+
assert x2 == py_result[1].x
74+
assert y2 == py_result[1].y
7575

7676
def test_random(self, cpp):
7777
rng = np.random.RandomState(777)
@@ -90,10 +90,10 @@ def test_random(self, cpp):
9090
x1, y1, x2, y2 = cpp.nearest_points(poly, ls)
9191
py_result = py_ops.nearest_points(py_poly, py_ls)
9292

93-
assert abs(x1 - py_result[0].x) < 1e-6
94-
assert abs(y1 - py_result[0].y) < 1e-6
95-
assert abs(x2 - py_result[1].x) < 1e-6
96-
assert abs(y2 - py_result[1].y) < 1e-6
93+
assert x1 == py_result[0].x
94+
assert y1 == py_result[0].y
95+
assert x2 == py_result[1].x
96+
assert y2 == py_result[1].y
9797

9898

9999
class TestNearestPointsLineStringPoint:
@@ -109,10 +109,10 @@ def test_point_on_line(self, cpp):
109109
x1, y1, x2, y2 = cpp.nearest_points_ls_pt(ls, pt)
110110
py_result = py_ops.nearest_points(py_ls, py_pt)
111111

112-
assert abs(x1 - py_result[0].x) < 1e-8
113-
assert abs(y1 - py_result[0].y) < 1e-8
114-
assert abs(x2 - py_result[1].x) < 1e-8
115-
assert abs(y2 - py_result[1].y) < 1e-8
112+
assert x1 == py_result[0].x
113+
assert y1 == py_result[0].y
114+
assert x2 == py_result[1].x
115+
assert y2 == py_result[1].y
116116

117117
def test_point_endpoint(self, cpp):
118118
line = [(0.0, 0.0), (10.0, 0.0)]
@@ -124,10 +124,10 @@ def test_point_endpoint(self, cpp):
124124
x1, y1, x2, y2 = cpp.nearest_points_ls_pt(ls, pt)
125125
py_result = py_ops.nearest_points(py_ls, py_pt)
126126

127-
assert abs(x1 - py_result[0].x) < 1e-8
128-
assert abs(y1 - py_result[0].y) < 1e-8
129-
assert abs(x2 - py_result[1].x) < 1e-8
130-
assert abs(y2 - py_result[1].y) < 1e-8
127+
assert x1 == py_result[0].x
128+
assert y1 == py_result[0].y
129+
assert x2 == py_result[1].x
130+
assert y2 == py_result[1].y
131131

132132
def test_random(self, cpp):
133133
rng = np.random.RandomState(42)
@@ -142,7 +142,7 @@ def test_random(self, cpp):
142142
x1, y1, x2, y2 = cpp.nearest_points_ls_pt(ls, pt)
143143
py_result = py_ops.nearest_points(py_ls, py_pt)
144144

145-
assert abs(x1 - py_result[0].x) < 1e-6
146-
assert abs(y1 - py_result[0].y) < 1e-6
147-
assert abs(x2 - py_result[1].x) < 1e-6
148-
assert abs(y2 - py_result[1].y) < 1e-6
145+
assert x1 == py_result[0].x
146+
assert y1 == py_result[0].y
147+
assert x2 == py_result[1].x
148+
assert y2 == py_result[1].y

tests/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
def compare(cpp_result, py_result, rtol=1e-12, atol=1e-12, label=""):
9-
"""Compare C++ result against Python shapely (ground-truth) result.
9+
"""Compare C++ result against Python shapely (ground-truth) result -- BIT-IDENTICAL (strict equality), since both use the same GEOS engine.
1010
1111
Returns a dict with: pass, max_abs_diff, max_rel_diff, shape_match,
1212
cpp_shape, py_shape, cpp_dtype, py_dtype, label.
@@ -47,7 +47,7 @@ def compare(cpp_result, py_result, rtol=1e-12, atol=1e-12, label=""):
4747
if not passed:
4848
worst_idx = int(np.argmax(abs_diff))
4949
info["error"] = (
50-
f"numerical mismatch: max_abs_diff={max_abs:.2e}, "
50+
f"bit mismatch: max_abs_diff={max_abs:.2e}, "
5151
f"max_rel_diff={max_rel:.2e} at idx {worst_idx}\n"
5252
f" C++ value: {cpp.flat[worst_idx]:.16e}\n"
5353
f" Py value: {py.flat[worst_idx]:.16e}"

0 commit comments

Comments
 (0)