Skip to content

triwild: simplify the input curves before the arrangement - #960

Open
danielepanozzo wants to merge 1 commit into
wmtk-dedupfrom
triwild-simplify
Open

triwild: simplify the input curves before the arrangement#960
danielepanozzo wants to merge 1 commit into
wmtk-dedupfrom
triwild-simplify

Conversation

@danielepanozzo

@danielepanozzo danielepanozzo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Stacked on #959. Adds the phase triwild was missing: the 2D counterpart of tetwild's input simplification.

Why

tetwild coarsens its input surface before inserting it — shortest-edge collapse inside half the envelope — and hands the arrangement the simplified version. triwild fed the raw curves straight to embed_seg_in_tri_mesh. That costs twice over: every input vertex becomes a constrained vertex of the exact arrangement, and then a surface vertex of the initial mesh.

And the inputs really are over-refined. Measured against the default length_rel of 5e-2:

input #segments median segment target edge length shorter than target
puzzle 440 3.57 40.9 92%
triwild_blob1 256 0.768 4.87 100%
triwild_circle 128 0.796 2.27 100%
triwild_blob2 212 0.663 4.10 100%
triwild_ellipse 88 0.175 0.583 100%
triwild_rectangle 32 5.03 2.54 0%

The simplifier

wmtk::utils::simplify_segments is a plain routine on (V, E), not a mesh subclass — wmtk has no edge-mesh class and a curve network does not need one. It repeatedly collapses the shortest segment whose removal keeps every affected segment inside the envelope, with two deliberate differences from the 3D version:

  • Frozen vertices are valence != 2 — open endpoints, junctions, and (they end up with valence >= 4) anything shared between two input curves. But a segment with one frozen endpoint still collapses, onto that endpoint's exact position, so junction geometry is preserved bit-exactly. Only both-endpoints-frozen is rejected, since merging two frozen vertices has to move one of them. (An earlier version of this text claimed rejecting outright "leaves a ring of short segments around every junction" — that is wrong. It strands exactly one un-collapsible vertex on each chain between two frozen ones, so twice the segments on a network of short chains. sec: collapse onto a frozen vertex instead of rejecting the collapse #961 measures the same effect in 3D and applies the same rule there.)
  • The queue is keyed on (squared length, segment id). The id is not decoration: a uniformly sampled curve is full of exactly-equal lengths, and a partial key there would make the result depend on heap order.

Reordering, and what changes

The tolerance has to exist before the arrangement runs, so triwild's front end moves to tetwild's order. Two consequences:

  • The bounding box now comes from the input curves. It used to come from the arrangement output, which includes the background grid and is ~13% larger, so eps and the target edge length shrink accordingly.
  • The envelope is built at eps/2 around the original input and shared by the simplification and the optimizer, as tetwild does with surf_mesh.m_envelope. init_mesh takes the envelope source explicitly for that reason — the optimizer has to stay near what the user gave us, not near the simplified version. Its existing "all surface edges are inside the envelope" check thereby stops being a tautology and becomes a real test of the simplification.

Together the envelope tightens by roughly 2.3x. Both integration configs still pass with throw_on_fail.

The bbox tagging in init_mesh now derives the domain box from the arrangement output instead of m_params, which is the box of the input and no longer the same thing — otherwise interior edges get mistaken for boundary ones (which is exactly what happened, and how the bug was found).

Measured

Serial, raw dense contour, input dedup disabled so the simplification is doing the work:

input length_rel skip_simplify=true skip_simplify=false arrangement #V
10k segments 5e-3 2.327s 0.789s (2.9x) 10270 -> 585
100k segments 5e-2 5.114s 2.320s (2.2x) 100270 -> 587

On the bundled models it is roughly neutral end to end — puzzle goes 438 -> 110 input vertices and the arrangement 657 -> 329, but at 0.4s total the arrangement was never the bottleneck. The win is on dense inputs, which is what traced contours and image boundaries are.

Worth knowing: remove_duplicate_eps already removes much of the redundancy on inputs sampled below that tolerance. On my first measurement it had quietly done most of the coarsening, and the simplification looked like a 24% regression; with it disabled the 2.9x above is the honest number. The two together matter more than either alone.

Verification

  • 7 new unit tests, 1696 assertions: a 1000-point circle collapses below 100 segments with every original vertex still within eps; a densely sampled square keeps its four corners exactly; an open chain collapses to one segment and keeps both endpoints; a Y keeps its junction at valence 3; free (unreferenced) points survive; the output has no degenerate or duplicated segments; two runs agree bit for bit.
  • tetwild and simwild untouched: 23/23 output files still byte-identical across the 15 deterministic non-triwild integration configs.
  • Full integration suite 33/33; every component test binary unchanged (wmtk_test_triwild now 1751 assertions in 14 cases).

skip_simplify joins the shared option set, leaving five keys that genuinely have no 2D meaning: use_sample_envelope, use_legacy_code, allow_surface_swap, check_surface_topology, DEBUG_euler.

🤖 Generated with Claude Code

tetwild coarsens its input surface before inserting it -- shortest-edge collapse
inside half the envelope -- and hands the arrangement the simplified version.
triwild had no equivalent: it fed the raw curves straight to
embed_seg_in_tri_mesh. That is expensive twice over, because every input vertex
becomes a constrained vertex of the exact arrangement and then a surface vertex
of the initial mesh, and inputs are routinely far finer than the target edge
length. Measured against the default length_rel, 92% of puzzle.obj's segments
and 100% of every other bundled 2D model's are shorter than the target.

New wmtk::utils::simplify_segments is the 1D counterpart. It is a plain routine
on (V, E) rather than a mesh subclass -- wmtk has no edge-mesh class and a curve
network does not need one. It repeatedly collapses the shortest segment whose
removal keeps every affected segment inside the envelope, with two deliberate
differences from the 3D version:

- Vertices of valence != 2 are frozen -- open endpoints, junctions, and (they end
  up with valence >= 4) anything shared between two input curves. But a segment
  with one frozen endpoint still collapses, onto that endpoint's exact position,
  so junction geometry is preserved bit-exactly. Rejecting outright, as the 3D
  code does, would leave a ring of short segments around every junction.
- The queue is keyed on (squared length, segment id). The id is not decoration:
  a uniformly sampled curve is full of exactly-equal lengths, and a partial key
  there would make the result depend on heap order.

Wiring it in required reordering triwild's front end to tetwild's order, since
the tolerance has to exist before the arrangement runs. Two consequences worth
knowing:

- The bounding box now comes from the input curves. It used to come from the
  arrangement output, which includes the background grid and is about 13%
  larger, so eps and the target edge length shrink accordingly.
- The envelope is built at eps/2 around the *original* input and shared by the
  simplification and the optimizer, as tetwild does with surf_mesh.m_envelope.
  init_mesh takes the envelope source explicitly for that reason: the optimizer
  has to stay near what the user gave us, not near the simplified version.
  Its existing "all surface edges are inside the envelope" check thereby stops
  being a tautology and becomes a real test of the simplification.

The bbox tagging in init_mesh now derives the domain box from the arrangement
output instead of m_params, which is the box of the input and no longer the same
thing -- otherwise interior edges get mistaken for boundary ones.

Together the envelope tightens by roughly 2.3x. Both integration configs still
pass with throw_on_fail.

Measured, serial, on a raw dense contour with input dedup disabled:

  10k segments, length_rel 5e-3:  2.327s -> 0.789s  (2.9x), arrangement
                                  #V 10270 -> 585
  100k segments, length_rel 5e-2: 5.114s -> 2.320s  (2.2x), arrangement
                                  #V 100270 -> 587

On the bundled models it is roughly neutral end to end -- puzzle 438 -> 110
input vertices and the arrangement 657 -> 329, but at 0.4s total the arrangement
was never the bottleneck. The win is on dense inputs, which is what traced
contours and image boundaries are. Note that remove_duplicate_eps already
removes much of the redundancy on inputs sampled below that tolerance, so the
two together matter more than either alone.

tetwild and simwild are untouched: 23/23 output files still byte-identical
across the 15 deterministic non-triwild integration configs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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