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.
- {smoothify-0.3.1 → smoothify-0.3.2}/CHANGELOG.md +5 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/PKG-INFO +1 -1
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/_version.py +3 -3
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/smoothify_core.py +75 -5
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/PKG-INFO +1 -1
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/SOURCES.txt +2 -0
- smoothify-0.3.2/smoothify.egg-info/scm_file_list.json +68 -0
- smoothify-0.3.2/smoothify.egg-info/scm_version.json +8 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_smoothify_core.py +101 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.github/workflows/ci.yml +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.github/workflows/publish.yml +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.gitignore +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.pre-commit-config.yaml +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.python-version +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/.vscode/settings.json +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/LICENSE +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/README.md +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/RELEASING.md +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/benchmarks/bench_water.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/Water.gpkg +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/Water_Smoothed.gpkg +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/merge_holes_examples.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/real_world_water_example.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/smoothify_vs_shapely_comparison.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/examples/usage_examples.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/__init__.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/generators.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/oracle.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/fuzz/runner.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/example_1_polygon.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/example_2_linestring.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/example_3_iterations.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/example_4_merging.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_example_images.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_pipeline_graphic.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/generate_readme_image.ipynb +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/pipeline_steps.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/smoothify_hero.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/images/smoothify_logo.png +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/pyproject.toml +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/pytest.ini +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/scripts/fuzz_run.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/scripts/fuzz_visualize.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/setup.cfg +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/__init__.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/coordinator.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/geometry_ops.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify/py.typed +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/dependency_links.txt +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/requires.txt +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/smoothify.egg-info/top_level.txt +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/README.md +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/__init__.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/conftest.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_all_geometry_types.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_area_tolerance.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_auto_segment_length.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_chaikin.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_congruence_dedup.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_convexity_artifacts.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_corner_rounding.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_data/convex_pixel_rectangle.gpkg +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_data/naip_owm_water_bodies.geojson +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_edge_cases_coverage.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_fuzz.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_geometry_types.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_invalid_polygon.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_merge_holes.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_real_world_data.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_self_intersecting_variant.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_smoothify_api.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_synthetic_blob_fuzz.py +0 -0
- {smoothify-0.3.1 → smoothify-0.3.2}/tests/test_water_quality_sweep.py +0 -0
- {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
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
21
|
+
__version__ = version = '0.3.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
|
207
|
+
"""Restore original polygon area after smoothing via buffering.
|
|
202
208
|
|
|
203
|
-
Smoothing
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|