perf: median-consensus variant merge + arc-length start rotation#9
Merged
Conversation
Replace unary_union of the 4 start-point variants with a per-point median: resample each variant to a shared point count, bring them into phase (FFT cross-correlation in the complex plane), and take the coordinate-wise median. Douglas-Peucker is start-vertex dependent, so the variants disagree on where a feature's vertices land. The union superimposed those disagreements (one peak became two, with a valley the area-preservation buffer then sharpened into a fold); the median resolves them to consensus and outvotes a single bad start-point variant. This eliminates the fold/dimple artifacts (fuzz: 4->0 dimpled outputs; Water: 8->0) and is faster than the union it replaces. The resample count scales with the variants' own detail (bounded to [240, 4000]): a fixed low count coarsens large detailed rings and makes the result diverge from the per-vertex union (up to ~120% symmetric difference on a 6k-vertex polygon), while the floor keeps phase-alignment accurate on small rings. With the scaled count the merged ring tracks the union to ~1% on large polygons. The median sits at the consensus interior rather than the union's outer envelope, so the merged ring is slightly smaller; area preservation restores it when enabled, and the few-percent reduction without it is acceptable (the water quality sweep's area-drift bound is now two-tier: tight with preservation, loose without). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Start-point variants now begin at evenly spaced arc-length positions, computed by interpolating a single new start vertex on the ring, rather than rotating to a vertex index on a fully densified ring. The old path segmentized the whole ring only so index-based rotation would land on evenly spaced points, but every vertex it added was collinear and immediately stripped by the following Douglas-Peucker. Feeding DP just the original vertices plus one cuts its input ~3.4x and the simplify step ~3.5x on examples/Water.gpkg (total single-core CPU work ~4.4s -> ~2.7s), for ~1.5x faster end-to-end than 0.3.2 with output unchanged within tolerance (per-polygon area drift <= 0.01%, worst concave turn unchanged). Also replace numpy.roll with a slice-based cyclic shift (_roll0) in the Chaikin and phase-alignment hot loops (bit-identical, fewer allocations), and remove the now-dead pre-simplify segmentize scaffolding. - Add tests: arc-length rotation preserves geometry and places the start at the requested fraction; _roll0 matches np.roll bit-for-bit. - Refresh stale comments/docstrings (union -> median, segmentize removed). - Update README "How It Works" to align 1:1 with the pipeline figure, and regenerate images/pipeline_steps.png plus its generator. - CHANGELOG: document the median-consensus merge and the arc-length speedup. - Re-run example notebooks and regenerate smoothed water output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two performance changes to the polygon smoothing pipeline that together make it ~1.5–1.6× faster end-to-end on
examples/Water.gpkgversus 0.3.2, while also eliminating the residual "dimple" fold artifact. Output is otherwise unchanged within tolerance.1. Median-consensus variant merge (
95e70f3)Douglas-Peucker is start-vertex dependent, so the four rotated start-point variants disagree slightly about where a feature's vertices land. The old
unary_unionsuperimposed those disagreements (a single peak became a double peak with a valley the area-preservation buffer then sharpened into a fold). The variants are now merged by a per-point median — resample to a common point count, phase-align via FFT cross-correlation, take the coordinate-wise median. This is a start-invariant consensus that outvotes a single bad variant, is faster than the union, and removes the dimple artifact (fuzz harness: 4 → 0 residual folds; Water: 8 → 0).2. Arc-length start rotation (
0a18fc5)Variants now start at evenly spaced arc-length positions (interpolating one new start vertex) instead of rotating to a vertex index on a fully densified ring. The old path densified the whole ring just so index rotation would land on even positions — but every added vertex was collinear and immediately stripped by the following DP. Feeding DP only the original vertices + 1 cuts its input ~3.4× and the simplify step ~3.5× (single-core CPU work ~4.4 s → ~2.7 s). Also swaps
numpy.rollfor a slice-based cyclic shift in the hot loops (bit-identical), and removes the dead pre-simplify segmentize scaffolding.Speed (Water, interleaved, median)
0.3.2 → this branch ≈ 1.50×; 0.3.1 → this branch ≈ 1.57×.
Correctness
_roll0matchesnp.rollbit-for-bit).Docs
pipeline_steps.pngand its generator regenerated.[Unreleased]documents both changes.🤖 Generated with Claude Code