Skip to content

perf: median-consensus variant merge + arc-length start rotation#9

Merged
wrignj08 merged 2 commits into
mainfrom
perf/median-combine
Jul 2, 2026
Merged

perf: median-consensus variant merge + arc-length start rotation#9
wrignj08 merged 2 commits into
mainfrom
perf/median-combine

Conversation

@wrignj08

@wrignj08 wrignj08 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Two performance changes to the polygon smoothing pipeline that together make it ~1.5–1.6× faster end-to-end on examples/Water.gpkg versus 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_union superimposed 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.roll for a slice-based cyclic shift in the hot loops (bit-identical), and removes the dead pre-simplify segmentize scaffolding.

Speed (Water, interleaved, median)

median
0.3.1 3058 ms
0.3.2 2923 ms
this branch 1951 ms

0.3.2 → this branch ≈ 1.50×; 0.3.1 → this branch ≈ 1.57×.

Correctness

  • 436 tests pass (added: arc-length rotation preserves geometry + lands at the requested fraction; _roll0 matches np.roll bit-for-bit).
  • Per-polygon area drift ≤ 0.01%, worst concave turn unchanged.
  • ruff / ruff-format / mypy clean.

Docs

  • README "How It Works" rewritten to align 1:1 with the pipeline figure; pipeline_steps.png and its generator regenerated.
  • Stale comments/docstrings refreshed (union → median, segmentize removed).
  • CHANGELOG [Unreleased] documents both changes.
  • Example notebooks re-run; smoothed water output regenerated.

🤖 Generated with Claude Code

wrignj08 and others added 2 commits July 2, 2026 10:31
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>
@wrignj08
wrignj08 merged commit 7bef9ba into main Jul 2, 2026
10 checks passed
@wrignj08
wrignj08 deleted the perf/median-combine branch July 2, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant