smoothify 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {smoothify-0.3.1 → smoothify-0.3.2}/CHANGELOG.md +5 -0
  2. {smoothify-0.3.1 → smoothify-0.3.2}/PKG-INFO +1 -1
  3. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/_version.py +3 -3
  4. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/smoothify_core.py +75 -5
  5. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/PKG-INFO +1 -1
  6. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/SOURCES.txt +2 -0
  7. smoothify-0.3.2/smoothify.egg-info/scm_file_list.json +68 -0
  8. smoothify-0.3.2/smoothify.egg-info/scm_version.json +8 -0
  9. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_smoothify_core.py +101 -0
  10. {smoothify-0.3.1 → smoothify-0.3.2}/.github/workflows/ci.yml +0 -0
  11. {smoothify-0.3.1 → smoothify-0.3.2}/.github/workflows/publish.yml +0 -0
  12. {smoothify-0.3.1 → smoothify-0.3.2}/.gitignore +0 -0
  13. {smoothify-0.3.1 → smoothify-0.3.2}/.pre-commit-config.yaml +0 -0
  14. {smoothify-0.3.1 → smoothify-0.3.2}/.python-version +0 -0
  15. {smoothify-0.3.1 → smoothify-0.3.2}/.vscode/settings.json +0 -0
  16. {smoothify-0.3.1 → smoothify-0.3.2}/LICENSE +0 -0
  17. {smoothify-0.3.1 → smoothify-0.3.2}/README.md +0 -0
  18. {smoothify-0.3.1 → smoothify-0.3.2}/RELEASING.md +0 -0
  19. {smoothify-0.3.1 → smoothify-0.3.2}/benchmarks/bench_water.py +0 -0
  20. {smoothify-0.3.1 → smoothify-0.3.2}/examples/Water.gpkg +0 -0
  21. {smoothify-0.3.1 → smoothify-0.3.2}/examples/Water_Smoothed.gpkg +0 -0
  22. {smoothify-0.3.1 → smoothify-0.3.2}/examples/merge_holes_examples.ipynb +0 -0
  23. {smoothify-0.3.1 → smoothify-0.3.2}/examples/real_world_water_example.ipynb +0 -0
  24. {smoothify-0.3.1 → smoothify-0.3.2}/examples/smoothify_vs_shapely_comparison.ipynb +0 -0
  25. {smoothify-0.3.1 → smoothify-0.3.2}/examples/usage_examples.ipynb +0 -0
  26. {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/__init__.py +0 -0
  27. {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/generators.py +0 -0
  28. {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/oracle.py +0 -0
  29. {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/runner.py +0 -0
  30. {smoothify-0.3.1 → smoothify-0.3.2}/images/example_1_polygon.png +0 -0
  31. {smoothify-0.3.1 → smoothify-0.3.2}/images/example_2_linestring.png +0 -0
  32. {smoothify-0.3.1 → smoothify-0.3.2}/images/example_3_iterations.png +0 -0
  33. {smoothify-0.3.1 → smoothify-0.3.2}/images/example_4_merging.png +0 -0
  34. {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_example_images.ipynb +0 -0
  35. {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_pipeline_graphic.py +0 -0
  36. {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_readme_image.ipynb +0 -0
  37. {smoothify-0.3.1 → smoothify-0.3.2}/images/pipeline_steps.png +0 -0
  38. {smoothify-0.3.1 → smoothify-0.3.2}/images/smoothify_hero.png +0 -0
  39. {smoothify-0.3.1 → smoothify-0.3.2}/images/smoothify_logo.png +0 -0
  40. {smoothify-0.3.1 → smoothify-0.3.2}/pyproject.toml +0 -0
  41. {smoothify-0.3.1 → smoothify-0.3.2}/pytest.ini +0 -0
  42. {smoothify-0.3.1 → smoothify-0.3.2}/scripts/fuzz_run.py +0 -0
  43. {smoothify-0.3.1 → smoothify-0.3.2}/scripts/fuzz_visualize.py +0 -0
  44. {smoothify-0.3.1 → smoothify-0.3.2}/setup.cfg +0 -0
  45. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/__init__.py +0 -0
  46. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/coordinator.py +0 -0
  47. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/geometry_ops.py +0 -0
  48. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/py.typed +0 -0
  49. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/dependency_links.txt +0 -0
  50. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/requires.txt +0 -0
  51. {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/top_level.txt +0 -0
  52. {smoothify-0.3.1 → smoothify-0.3.2}/tests/README.md +0 -0
  53. {smoothify-0.3.1 → smoothify-0.3.2}/tests/__init__.py +0 -0
  54. {smoothify-0.3.1 → smoothify-0.3.2}/tests/conftest.py +0 -0
  55. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_all_geometry_types.py +0 -0
  56. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_area_tolerance.py +0 -0
  57. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_auto_segment_length.py +0 -0
  58. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_chaikin.py +0 -0
  59. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_congruence_dedup.py +0 -0
  60. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_convexity_artifacts.py +0 -0
  61. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_corner_rounding.py +0 -0
  62. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_data/convex_pixel_rectangle.gpkg +0 -0
  63. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_data/naip_owm_water_bodies.geojson +0 -0
  64. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_edge_cases_coverage.py +0 -0
  65. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_fuzz.py +0 -0
  66. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_geometry_types.py +0 -0
  67. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_invalid_polygon.py +0 -0
  68. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_merge_holes.py +0 -0
  69. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_real_world_data.py +0 -0
  70. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_self_intersecting_variant.py +0 -0
  71. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_smoothify_api.py +0 -0
  72. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_synthetic_blob_fuzz.py +0 -0
  73. {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_water_quality_sweep.py +0 -0
  74. {smoothify-0.3.1 → smoothify-0.3.2}/tests/visual_tests.ipynb +0 -0
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.2] - 2026-06-30
8
+
9
+ ### Changed
10
+ - Area preservation is faster with no change to output. The buffer-distance search now seeds from the Steiner area expansion `A(d) ≈ A₀ + L·d + π·d²` — solving that quadratic for the initial offset instead of using a linear estimate — and refines with Newton's method using the measured boundary length as the derivative (the rate of area change of an outward offset equals its perimeter), so it no longer brackets the root before solving. This cuts buffer operations per ring from roughly 5–6 to 1–2. On `examples/Water.gpkg` the full single-core pipeline is ~1.3x faster at the default 5 iterations (5.1s → 3.8s), with the speedup scaling up with iteration count and the polygon share of the workload (down to ~1.0x on hole-dominated inputs). Output is unchanged within the area tolerance (per-polygon symmetric difference ≤ 0.005%, area-preservation accuracy identical). Awkward shapes where Newton stalls (pinch-offs or topology changes near the root) fall back to the previous bracketed Brent's-method search, so robustness is unchanged.
11
+
7
12
  ## [0.3.1] - 2026-06-15
8
13
 
9
14
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smoothify
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Transform pixelated geometries from raster data into smooth natural looking features
5
5
  Author-email: Nick Wright <nicholas.wright@dpird.wa.gov.au>
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.1'
22
- __version_tuple__ = version_tuple = (0, 3, 1)
21
+ __version__ = version = '0.3.2'
22
+ __version_tuple__ = version_tuple = (0, 3, 2)
23
23
 
24
- __commit_id__ = commit_id = 'ga092a4c6c'
24
+ __commit_id__ = commit_id = 'gb2e33b7b2'
@@ -1,3 +1,4 @@
1
+ import math
1
2
  from typing import cast
2
3
 
3
4
  import numpy as np
@@ -193,16 +194,30 @@ def _generate_starting_point_variants(
193
194
  )
194
195
 
195
196
 
197
+ # Most rings land within tolerance in one or two Newton steps; allow a few
198
+ # more for awkward shapes before handing off to the bracketed fallback.
199
+ _AREA_NEWTON_MAX_STEPS = 6
200
+
201
+
196
202
  def _preserve_area_with_buffer(
197
203
  polygon: Polygon,
198
204
  target_area: float,
199
205
  tolerance: float = 1e-6,
200
206
  ) -> Polygon:
201
- """Restore original polygon area after smoothing via iterative buffering.
207
+ """Restore original polygon area after smoothing via buffering.
202
208
 
203
- Smoothing operations can slightly change polygon area. This function uses
204
- Brent's method (root-finding algorithm) to find the optimal buffer distance
205
- that restores the original area within the specified tolerance."""
209
+ Smoothing slightly changes polygon area; this finds the buffer distance that
210
+ restores the original area to within ``tolerance``.
211
+
212
+ The buffered area follows the Steiner expansion ``A(d) ~= A0 + L*d + pi*d^2``
213
+ (``L`` = perimeter), so we seed the search by solving that quadratic instead
214
+ of the cruder linear estimate, then refine with Newton's method. The area
215
+ swept by an outward offset changes at a rate equal to the boundary length
216
+ (the coarea identity), so each buffered candidate yields a near-exact
217
+ derivative for free and no bracketing is needed -- typically one or two
218
+ buffers per ring instead of the half-dozen a bracketed root-find spends.
219
+ Awkward shapes (pinch-offs, topology changes near the root) fall back to the
220
+ robust Brent's-method search."""
206
221
 
207
222
  if polygon.is_empty:
208
223
  return polygon
@@ -211,8 +226,63 @@ def _preserve_area_with_buffer(
211
226
  if abs(current_area - target_area) <= tolerance:
212
227
  return polygon
213
228
 
214
- # Approximate buffer distance needed (assuming circular shape)
215
229
  perimeter = polygon.length
230
+ if perimeter <= 0:
231
+ return polygon
232
+
233
+ # Seed from the Steiner quadratic pi*d^2 + L*d + (A0 - target) = 0, taking
234
+ # the root nearest zero. When the parabola never reaches the target (a deep
235
+ # shrink past its vertex) fall back to the first-order estimate and let
236
+ # Newton walk in.
237
+ area_gap = current_area - target_area
238
+ discriminant = perimeter * perimeter - 4.0 * math.pi * area_gap
239
+ if discriminant >= 0:
240
+ distance = (-perimeter + math.sqrt(discriminant)) / (2.0 * math.pi)
241
+ else:
242
+ distance = -area_gap / perimeter
243
+
244
+ best_result: Polygon | None = None
245
+ best_error = float("inf")
246
+ for _ in range(_AREA_NEWTON_MAX_STEPS):
247
+ candidate = polygon.buffer(distance)
248
+ candidate_area = candidate.area
249
+ error = abs(candidate_area - target_area)
250
+ if error < best_error:
251
+ best_result, best_error = candidate, error
252
+ if error <= tolerance:
253
+ return candidate
254
+ # dA/dd == boundary length of the current candidate (coarea identity).
255
+ slope = candidate.length
256
+ if slope <= 0:
257
+ break
258
+ next_distance = distance - (candidate_area - target_area) / slope
259
+ if not math.isfinite(next_distance):
260
+ break
261
+ distance = next_distance
262
+
263
+ # Newton stalled before reaching tolerance: defer to the bracketed search,
264
+ # but keep whichever result is actually closer to the target.
265
+ fallback = _preserve_area_brentq(
266
+ polygon, target_area, tolerance, current_area, perimeter
267
+ )
268
+ if fallback is not None and abs(fallback.area - target_area) < best_error:
269
+ return fallback
270
+ return best_result if best_result is not None else polygon
271
+
272
+
273
+ def _preserve_area_brentq(
274
+ polygon: Polygon,
275
+ target_area: float,
276
+ tolerance: float,
277
+ current_area: float,
278
+ perimeter: float,
279
+ ) -> Polygon | None:
280
+ """Bracketed Brent's-method area restoration (robust fallback).
281
+
282
+ Slower than the Newton path (it brackets the root before solving) but does
283
+ not rely on a good initial estimate, so it covers shapes where Newton
284
+ stalls. Returns ``None`` only if no usable buffer could be produced."""
285
+
216
286
  initial_guess = (target_area - current_area) / perimeter if perimeter > 0 else 0
217
287
 
218
288
  # Cache evaluations: brentq re-evaluates the bracket endpoints, and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smoothify
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Transform pixelated geometries from raster data into smooth natural looking features
5
5
  Author-email: Nick Wright <nicholas.wright@dpird.wa.gov.au>
6
6
  License-Expression: MIT
@@ -43,6 +43,8 @@ smoothify.egg-info/PKG-INFO
43
43
  smoothify.egg-info/SOURCES.txt
44
44
  smoothify.egg-info/dependency_links.txt
45
45
  smoothify.egg-info/requires.txt
46
+ smoothify.egg-info/scm_file_list.json
47
+ smoothify.egg-info/scm_version.json
46
48
  smoothify.egg-info/top_level.txt
47
49
  tests/README.md
48
50
  tests/__init__.py
@@ -0,0 +1,68 @@
1
+ {
2
+ "files": [
3
+ "RELEASING.md",
4
+ ".pre-commit-config.yaml",
5
+ "README.md",
6
+ ".python-version",
7
+ "LICENSE",
8
+ "pyproject.toml",
9
+ "CHANGELOG.md",
10
+ ".gitignore",
11
+ "pytest.ini",
12
+ "scripts/fuzz_run.py",
13
+ "scripts/fuzz_visualize.py",
14
+ ".vscode/settings.json",
15
+ "smoothify/geometry_ops.py",
16
+ "smoothify/py.typed",
17
+ "smoothify/__init__.py",
18
+ "smoothify/smoothify_core.py",
19
+ "smoothify/coordinator.py",
20
+ "images/example_2_linestring.png",
21
+ "images/generate_pipeline_graphic.py",
22
+ "images/generate_example_images.ipynb",
23
+ "images/pipeline_steps.png",
24
+ "images/smoothify_hero.png",
25
+ "images/generate_readme_image.ipynb",
26
+ "images/example_4_merging.png",
27
+ "images/example_3_iterations.png",
28
+ "images/smoothify_logo.png",
29
+ "images/example_1_polygon.png",
30
+ "fuzz/__init__.py",
31
+ "fuzz/oracle.py",
32
+ "fuzz/runner.py",
33
+ "fuzz/generators.py",
34
+ "tests/test_smoothify_core.py",
35
+ "tests/test_area_tolerance.py",
36
+ "tests/README.md",
37
+ "tests/__init__.py",
38
+ "tests/test_edge_cases_coverage.py",
39
+ "tests/test_water_quality_sweep.py",
40
+ "tests/test_congruence_dedup.py",
41
+ "tests/test_geometry_types.py",
42
+ "tests/test_convexity_artifacts.py",
43
+ "tests/visual_tests.ipynb",
44
+ "tests/test_corner_rounding.py",
45
+ "tests/test_fuzz.py",
46
+ "tests/test_merge_holes.py",
47
+ "tests/test_invalid_polygon.py",
48
+ "tests/conftest.py",
49
+ "tests/test_all_geometry_types.py",
50
+ "tests/test_chaikin.py",
51
+ "tests/test_smoothify_api.py",
52
+ "tests/test_auto_segment_length.py",
53
+ "tests/test_real_world_data.py",
54
+ "tests/test_synthetic_blob_fuzz.py",
55
+ "tests/test_self_intersecting_variant.py",
56
+ "tests/test_data/convex_pixel_rectangle.gpkg",
57
+ "tests/test_data/naip_owm_water_bodies.geojson",
58
+ "benchmarks/bench_water.py",
59
+ "examples/real_world_water_example.ipynb",
60
+ "examples/merge_holes_examples.ipynb",
61
+ "examples/Water_Smoothed.gpkg",
62
+ "examples/Water.gpkg",
63
+ "examples/usage_examples.ipynb",
64
+ "examples/smoothify_vs_shapely_comparison.ipynb",
65
+ ".github/workflows/ci.yml",
66
+ ".github/workflows/publish.yml"
67
+ ]
68
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.2",
3
+ "distance": 0,
4
+ "node": "gb2e33b7b2668f231572f65e1e723aef9cf567171",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-30"
8
+ }
@@ -2,10 +2,13 @@
2
2
 
3
3
  import pytest
4
4
  from shapely.geometry import LineString, Polygon
5
+ from shapely.geometry.base import BaseGeometry
5
6
 
7
+ from smoothify import smoothify_core
6
8
  from smoothify.smoothify_core import (
7
9
  _generate_starting_point_variants,
8
10
  _join_adjacent,
11
+ _preserve_area_brentq,
9
12
  _preserve_area_with_buffer,
10
13
  _rotate_polygon_start,
11
14
  _smoothify_geometry,
@@ -89,6 +92,104 @@ class TestPreserveAreaWithBuffer:
89
92
  assert abs(preserved.area - target_area) < 1e-3
90
93
  assert preserved.area < large_polygon.area
91
94
 
95
+ def test_preserve_area_empty(self):
96
+ """Empty input is returned unchanged."""
97
+ empty = Polygon()
98
+ assert _preserve_area_with_buffer(empty, target_area=10.0).is_empty
99
+
100
+ def test_preserve_area_concave_shape(self):
101
+ """Newton path reaches tolerance on a concave (non-convex) polygon."""
102
+ # L-shape: the pi*d^2 Steiner seed assumes total turning of 2*pi, which
103
+ # a reflex corner violates, so this exercises the Newton correction.
104
+ l_shape = Polygon([(0, 0), (10, 0), (10, 4), (4, 4), (4, 10), (0, 10)])
105
+ for target in (l_shape.area * 1.05, l_shape.area * 0.95):
106
+ preserved = _preserve_area_with_buffer(
107
+ l_shape, target_area=target, tolerance=1e-4
108
+ )
109
+ assert abs(preserved.area - target) < 1e-4
110
+
111
+ def test_preserve_area_uses_few_buffers(self):
112
+ """The Newton solve should reach tolerance in far fewer buffers than the
113
+ bracketed fallback would (guards the optimisation against regressions)."""
114
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
115
+ calls = {"n": 0}
116
+ original_buffer = BaseGeometry.buffer
117
+
118
+ def counting_buffer(self, *args, **kwargs):
119
+ calls["n"] += 1
120
+ return original_buffer(self, *args, **kwargs)
121
+
122
+ BaseGeometry.buffer = counting_buffer
123
+ try:
124
+ preserved = _preserve_area_with_buffer(
125
+ polygon, target_area=polygon.area * 1.1, tolerance=1e-4
126
+ )
127
+ finally:
128
+ BaseGeometry.buffer = original_buffer
129
+
130
+ assert abs(preserved.area - polygon.area * 1.1) < 1e-4
131
+ assert calls["n"] <= 4
132
+
133
+ def test_preserve_area_falls_back_to_brentq(self, monkeypatch):
134
+ """When Newton is denied any steps the function must still reach
135
+ tolerance via the bracketed Brent's-method fallback."""
136
+ monkeypatch.setattr(smoothify_core, "_AREA_NEWTON_MAX_STEPS", 0)
137
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
138
+ for target in (polygon.area * 1.1, polygon.area * 0.9):
139
+ preserved = _preserve_area_with_buffer(
140
+ polygon, target_area=target, tolerance=1e-3
141
+ )
142
+ assert abs(preserved.area - target) < 1e-3
143
+
144
+ def test_newton_and_brentq_agree(self):
145
+ """The Newton path and the bracketed fallback land on the same area."""
146
+ polygon = Polygon([(0, 0), (8, 0), (8, 8), (0, 8)])
147
+ target = polygon.area * 1.07
148
+ newton = _preserve_area_with_buffer(polygon, target_area=target, tolerance=1e-4)
149
+ brentq = _preserve_area_brentq(
150
+ polygon,
151
+ target_area=target,
152
+ tolerance=1e-4,
153
+ current_area=polygon.area,
154
+ perimeter=polygon.length,
155
+ )
156
+ assert brentq is not None
157
+ assert abs(newton.area - target) < 1e-4
158
+ assert abs(brentq.area - target) < 1e-4
159
+ assert abs(newton.area - brentq.area) < 1e-3
160
+
161
+
162
+ class TestPreserveAreaBrentq:
163
+ """Test suite for the bracketed Brent's-method fallback."""
164
+
165
+ def test_brentq_grows_polygon(self):
166
+ """Fallback expands a polygon to a larger target area."""
167
+ polygon = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
168
+ result = _preserve_area_brentq(
169
+ polygon,
170
+ target_area=100.0,
171
+ tolerance=1e-3,
172
+ current_area=polygon.area,
173
+ perimeter=polygon.length,
174
+ )
175
+ assert result is not None
176
+ assert abs(result.area - 100.0) < 1e-3
177
+ assert result.area > polygon.area
178
+
179
+ def test_brentq_shrinks_polygon(self):
180
+ """Fallback shrinks a polygon to a smaller target area."""
181
+ polygon = Polygon([(0, 0), (20, 0), (20, 20), (0, 20)])
182
+ result = _preserve_area_brentq(
183
+ polygon,
184
+ target_area=100.0,
185
+ tolerance=1e-3,
186
+ current_area=polygon.area,
187
+ perimeter=polygon.length,
188
+ )
189
+ assert result is not None
190
+ assert abs(result.area - 100.0) < 1e-3
191
+ assert result.area < polygon.area
192
+
92
193
 
93
194
  class TestJoinAdjacent:
94
195
  """Test suite for joining adjacent geometries."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes