Skip to content

Envelopes: one reference, and each input held to its own, in every app and the offset - #1155

Merged
udaykusupati merged 29 commits into
mainfrom
uday/envelope-reference
Nov 4, 2026
Merged

udaykusupati merged 29 commits into
mainfrom
uday/envelope-reference

Conversation

@udaykusupati

@udaykusupati udaykusupati commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

Stacked on #1150. One set of envelope rules for triwild, simwild 2D, tetwild, simwild 3D and
topological_offset 3D:

  • which geometry the envelopes are built around (one key, envelope_reference);
  • each input held to its own envelopes, in the optimizer and in the input simplification;
  • the containment rules in the engine base classes, not in per-app copies.

Terms:

  • Envelope: the region within eps of an input that the output's tracked faces (2D: segments)
    must stay in. One-sided: it bounds where the output is, not that it covers the input.
  • Tube: the same for a curve (3D: an order-2 or feature edge). Anchor: a ball holding a
    curve end or junction.
  • Input: an input file (tetwild, triwild), a label (simwild), a face tag or the tag of a region
    a face bounds (topological_offset).
  • Input set: the inputs a tracked simplex lies on.
  • Order-2 edge (3D): an edge with 1 tracked face (an open boundary) or 3 and more (a
    non-manifold edge).
  • s: the input simplification's eps. eps: the optimizer's.

Behaviour after this PR

before after
geometry the envelopes, tubes and anchors are built around differed by app and by kind (tetwild: surfaces around the input, tubes and anchors around the simplified) envelope_reference: "input" (default) or "simplified", the same for every kind in every app
"input" - around the input at eps. Where the simplification made a curve the input lacks, held around it as inserted. s >= eps refused
"simplified" around the simplified at eps - s, full eps with a warning when s >= eps the simplified geometry replaces the input: around it at the full eps, any s. Refused when no simplification runs
s tetwild/triwild: simplify_envelope_ratio x eps; simwild: eps_simplify_rel x diagonal all apps: eps_simplify_rel x diagonal, or absolute eps_simplify. Defaults 5e-4 (tetwild, triwild), 2e-4 (simwild)
simplification on/off skip_simplify simplify (default true)
3D containment of a tracked face per-app copies (tetwild, simwild), different on an empty input set engine: the envelopes of its input set; empty input set throws, as in 2D
input simplification every face/segment held to the union of all inputs each held to its own input's envelope
one shared envelope instead of per input - per_input_envelopes false (default true), every kind at once
3D collapse of an edge between two surface vertices on no tracked face no test of the edge the edge must be inside the envelope, as in 2D
3D smoothing pull two identical per-app copies engine default
unknown keys refused by simwild only refused by all apps
topological_offset 3D one union envelope for every held face, no tubes, no anchors, wall in the envelope and smoothed per-label envelopes, order-2 tubes and anchors as in simwild, wall held by the box rule as in the wilds

Removed keys (no aliases; the strict key check refuses them): skip_simplify,
simplify_envelope_ratio, optimize_envelope_around_simplified. New keys: envelope_reference,
simplify, eps_simplify_rel / eps_simplify (tetwild, triwild), per_input_envelopes,
order2_envelope_ratio (topological_offset).

Discussion points

  1. What "input" means. Without preserve_topology the simplification can glue rims away and
    make open boundaries, non-manifold edges and curve ends the input does not have. This PR holds
    them around themselves as inserted (meaning I: the input where it has the structure, as
    inserted where it does not). The alternative (II: the input only) holds only what the input
    has, and leaves the rest to the one-sided surface envelope, where it can erode. The guarantee
    (output within eps of the input) holds either way; the difference is what is protected from
    erosion. Whichever is chosen applies to every kind.
  2. tetwild holds open boundaries only, not non-manifold edges (holds_order2_edge()). On
    Thingi10K 111388, 278 of 329 non-manifold edges appear after simplification and insertion, and
    holding them hit the iteration cap (Shared holding of curves and points in the 2D and 3D bases (triwild, tetwild, simwild 2D and 3D) #1150). Under meaning I that is an exception.
  3. topological_offset's domain wall is now frozen by the box rule, as in tetwild and simwild.
    It was held in the envelope and smoothed since 2026-08: with the wall frozen and its edges
    unsplittable, a tet resting on the box bisected onto the plane (max AMIPS 6.9e21 against 364).
    Wall edges split now. The offset configs' max AMIPS is the check below; holding the wall in an
    envelope of its own is the fallback.

Measurements

All on uk-kirby, single runs unless stated.

  • wmtk, tetwild, simwild, triwild, tracking, topological_offset and shortest_edge_collapse suites
    pass at every verified step.
  • Integration and challenging configs (64), before the per-input simplification against after:
    63 byte-identical; simwild_double_sphere_notop_3d differs (two overlapping spheres: 244
    collapses instead of 251; holding the edges where they meet: 378 tagged, 197 anchors, final max
    energy 9.28 -> 8.08).
  • Per-input simplification, two inputs within eps of each other: tetwild wave sheet near a flat
    sheet, union 12 and 11 vertices and both runs time out at 1500 s, per input 62 and 83 vertices in
    155 s and 129 s. triwild wave curve near a segment: union repeats a failing split until the
    timeout, per input 15 s. Two-input fixtures whose inputs stay apart: outputs byte-identical,
    simplification 3% to 20% slower (96% of the envelope time is the cover search; the other
    input's prisms covered part of a face cheaply). per_input_envelopes false gives that time
    back.
  • 3D collapsing-edge test: on tetwild's orientation configurations, 1956 such collapses were tried
    before it and none went through, refused by other tests (quality 976, inversion 967, faces'
    check 13). With it, 13 of 82 runs change, all finish.
  • eps_simplify_rel default 5e-4 equals 0.5 x eps at the default eps_rel 1e-3 bit-for-bit; all
    57 tetwild and triwild data configs run at the default.
  • topological_offset: the suite passes. The data repository's 9 3D cube offset configs, this
    branch against main (which holds and smooths the wall): main finishes 8 and times out on
    cube_small_offset_conv0.025 at 3600 s; this branch converges on the same 8, final max AMIPS 7.0
    to 73 against main's 10.7 to 70, so freezing the wall brings no bisection cascade back. The 9
    2D configs are refused on both: the 2D offset is disabled. These runs found a crash, fixed in
    e2ebdb3.
  • The offset commits change no output outside the offset: the 82 integration, challenging and
    fixture configs are byte-identical before and after them.

Data

Pins data2 at 6dc625a, the merge of data2 #13 (simwild_double_sphere_notop_3d drops
skip_simplify), which also holds main's pin de49170.

Left for later PRs

  • Envelope IO between components: tags as the only currency; tetwild writes oriented face tags per
    input (separate PR, branch uday/envelope-io).
  • Simplification route under preserve_topology (before vs after insertion), simwild 2D included;
    reproducer first.
  • Simplify after insertion always: to be discussed.
  • tetwild and simwild: slide box-wall vertices within a wall envelope, if the offset needs it back.
  • smoothing_mode "exact" (the envelope energy term): rethink or remove.
  • simwild's init check under "input"; triwild's endless retry of a failing split.
  • topological_offset: per-input construction, fields and distances.

🤖 Generated with Claude Code

udaykusupati and others added 29 commits October 31, 2026 15:37
…s, as each face is

One tube per kind held every tagged edge, around all the kind's curves at once, while each
tracked face carries its input set and is held to its own inputs' envelopes. Now each tagged edge
carries, per kind, the inputs it tiles -- feature: the input_edges file of its input edge;
order2: the inputs of the tracked faces at it, taken in tag_order2_edges() -- and is held to
those inputs' tubes, one at a time (there is no 3D segment test for an intersection envelope). An
edge with an empty set, or a kind whose edges come from one input, keeps the kind's union tube.

- FeatureEdgeAttributes: m_feature_inputs, m_order2_inputs; inputs(k), merge(), remove(k).
- m_feature_input_envelopes, m_order2_input_envelopes, built by
  init_tagged_edge_input_envelopes() around each input's tagged edges at init, only with two or
  more inputs, as faces build theirs.
- tagged_edge_leaves_tube() takes the edge's tags; the collapse guard, the smoothing containment,
  tagged_edges_outside() and tetwild's sanity warning go through it.
- tagged_edge_pull_envelope() pulls toward the farthest of the vertex's inputs' tubes, as faces
  pull (InputEnvelopes::pull).
- The split, collapse and swap caches keep the whole attribute: a split gives both halves the
  parent's sets; remap_edge_tags() merges the sets of two edges a collapse welds into one.
- init_feature_envelope() folds into tag_feature_edges(placed, input, eps, exact), as
  tag_order2_edges(); tetwild's init_from_Volumeremesher() takes the feature input.

Tests: feature-curves-held-to-their-own-tubes (two curves from two files 0.07 apart, tubes 0.05:
every tagged edge within its own file's tube after mesh_improvement(3); with the per-input tubes
ignored, edges end 1.08-1.64 tube radii off their own curve); test_feature_tags checks the sets
through splits, swaps and collapses.

Measured on kirby against ea24a71: wmtk_tests, tetwild and simwild suites pass; 43 of the 44
integration configs byte-identical; simwild_double_sphere_notop_3d (2 inputs) changes, same 332
order-2 edges, same max energy 9.684, avg 3.5827366 -> 3.5827103.

For the envelope PR; not on uday/shared-order2-protection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… or all around the simplification

Which geometry the optimizer's envelopes were built around differed by app and by structure.
tetwild built the surface envelope around the original input, but the order-2 tube and the curve
anchors around the simplified surface, so an open boundary could drift 1.5 eps at the defaults;
triwild its curve envelope around the input and its curve anchors around the simplified curves.
simwild built everything around the simplified geometry whatever
optimize_envelope_around_simplified said. One key now decides it for every envelope, tube and
anchor, in triwild, tetwild and simwild alike: envelope_reference, "input" (the default) or
"simplified". optimize_envelope_around_simplified is removed.

"input": the original input, at eps times each one's ratio. Without preserve_topology the
simplification glues rims of the input away and makes open boundaries, non-manifold edges and
curve ends of its own. Those it made are held by the surface envelope alone; those it removed are
lost, as the topology change it is allowed to make.
- TetOptimizerMesh::InputCurves (set_input_curves()): per input, the edges of the input's own
  faces holds_order2_edge() holds, and the points where those and the feature curves end or three
  or more meet. Set by the app before init when a simplification ran before the insertion.
- tag_order2_edges(): with InputCurves, the tubes are built around the input's curves, and an edge
  is tagged only if every input of its set has a curve there and the edge lies in its tube.
- anchor_feature_points() (2D and 3D): with the input's curve points, a curve end or junction is
  anchored only if one of them lies in its ball, and the ball is centred there. Input points are
  anchored as before.
- simwild: read_mesh and read_curves hand over the input as read for the envelope;
  SimWildMesh::simplify() puts the envelopes around the input back after simplifying, instead of
  rebuilding them around its result. Where two inputs meet, after a simplification before the
  insertion, the edges are left to the surface envelopes: the input's curves there are the
  intersections, which only an arrangement of the input would give.
- utils::check_simplification_inside_envelope(): a simplification eps at or above the
  optimizer's is refused before any work, since the simplified geometry could start outside the
  envelope it is held to. simwild checks the eps the simplification actually runs at: read_mesh
  measures the inputs' own diagonal, Parameters::eps_simplify the padded box (1.22x apart on
  simwild_double_sphere_notop_3d, whose eps_simplify_rel equals its eps_rel).

"simplified": around the simplified geometry, at the remaining tolerance (eps - simplify_eps)
times each one's ratio. The order-2 tubes and the curve anchors now follow the surface envelope
there (OptimizerParameters::order2_envelope_eps); in 3D they took the full eps. simwild's
simplification after the insertion holds the order-2 edges within order2_envelope_ratio x
eps_simplify, and they are tagged again around its result; the anchors stay centred on the input's
points at the reduced radius.

Tests: open-boundary-tags-only-on-the-inputs-own (input: an open square; inserted: that square
with a hole cut out. Around the input 164 outer-rim edges are tagged and 0 on the hole; without
InputCurves, 164 and 4; no tagged edge outside its tube after mesh_improvement(2)).
simwild-simplification-boundary-envelope now sets envelope_reference "simplified", since it reads
the simplification through InputData::V_envelope; its assertions are unchanged. The triwild
input-sets test runs both values.

Measured on kirby against a9b0f1e: wmtk_tests and the tetwild, simwild, triwild, tracking and
topological_offset suites pass. Of the 44 integration configs, 24 are byte-identical and 20 change,
and every one reports 0 order-2 edges outside their tube. Order-2 edges at insertion lying in the
tubes around the input's own curves: crown 0 of 386, 314748 19 of 20, 103197 443 of 443 (23 curve
ends with no input point in reach); 101168, 1017012, 104187, 104188, 104509 and 111388 are closed
inputs, so none is tagged. Iterations and wall time (single runs, 22 at once): 103197 56 -> 41,
450 -> 287 s; 104509 48 -> 40, 302 -> 179 s; crown 12 -> 12, 261 -> 201 s; 111388 29 -> 37,
17 -> 19 s; 100827 8 -> 12. simwild_double_sphere_notop_3d: 332 order-2 edges where the spheres
meet -> 0, 16 -> 4 iterations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uts' own diagonal

simwild measured its tolerances against two diagonals. The optimizer's eps, eps_simplify and
target edge length were relative to the embedding's box, which pads the inputs' bounding box by a
fifteenth of its diagonal on each side; the simplification before the insertion (preserve_topology
off) was relative to the inputs' own. So eps_rel meant something else than in tetwild and triwild,
and the two simplifications ran at different eps for the same eps_simplify_rel: 1.22x apart on
simwild_double_sphere_notop_3d, 1.42x on simwild_curves_2d.

Now every one is relative to the inputs' own bounding box, the feature curves and points
included, as in tetwild and triwild. read_mesh and read_curves measure it before any
simplification (InputData::diag), and Parameters::init() takes it while the padded box still sets
the domain (bbox-face tags). eps_simplify is derived from it once, by Parameters' rule -- the
absolute eps_simplify if given, else eps_simplify_rel of the diagonal; read_mesh used to let the
relative one win -- so the check that refuses a simplification at or above eps under
envelope_reference "input" reads the eps both routes run at. A mesh input (.msh) keeps its own
box, as before.

At the same keys, a surface or curve input gets a finer mesh: eps and length shrink by 1.22-1.42.
To keep the old resolution, multiply eps_rel and length_rel by that.

Measured on kirby against 66ce8de: simwild, tracking and topological_offset suites pass. Of
the 16 simwild integration configs the 12 mesh inputs are byte-identical. The optimizer's eps,
old -> new, and the final max energy: simwild_curves_2d 4.773 -> 3.351 (its simplification stays
at 0.3351, as the old one before the insertion measured the same diagonal), 9.68 -> 7.32;
simwild_curves_two_inputs_2d 5.469 -> 3.849, 16.9 -> 17.9; simwild_double_sphere_3d 0.0321 ->
0.0261, 7.52 -> 7.69, 3 -> 4 iterations. simwild_double_sphere_notop_3d set eps_simplify_rel
equal to eps_rel, now a simplification at the full eps, and is refused; data2 1cf750d halves it,
and the pin in cmake/wmtk_data.cmake moves there: 4 iterations, final max energy 8.80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de are held around themselves

Under envelope_reference "input", an order-2 edge (open boundary, non-manifold edge) or curve end
the simplification made, and the input does not have, was left to the surface envelope alone. The
surface envelope is one-sided -- the output inside the envelope of the input -- so it cannot keep a
curve from eroding from its end, and the input on that curve erodes with it. Measured as the input
samples (vertices, face centroids and edge midpoints in 3D; points every eps/4 along the curves in
2D) farther than eps from the output, held -> left free: tetwild 104509 239 -> 317, crown
554 -> 695, 104187 1058 -> 1222, 104188 962 -> 1105, 1017012 8 -> 36; triwild 202260
927 -> 1315, 191265 148259 -> 181805, 196407 2.29M -> 2.41M of 2.54M.

Now, after a simplification before the insertion:
- TetOptimizerMesh::tag_order2_edges(): an order-2 edge of one input outside the tubes around the
  input's own curves is tagged too, in a tube around its own curve as the insertion leaves it.
  The two kinds share one tube per input, so an edge stays within eps of an order-2 curve of its
  own input, the input's or the simplification's. An edge where two or more inputs meet is left
  to the surface envelopes, as before: the input's curves there are the intersections, which only
  an arrangement of the input would give.
- anchor_feature_points() (2D and 3D): a curve end or junction with none of the input's own curve
  points in reach is anchored at its own position. With one in reach, the ball is centred there,
  as before.

Tests: open-boundary-tags-only-on-the-inputs-own becomes
open-boundary-tags-around-the-input-and-the-simplification: the hole's rim, which the input does
not have, is now tagged with the input's curves set as without them (164 outer, 4 hole), every
tagged edge stays in its tube after mesh_improvement(2), and the hole keeps its rim.

Measured on kirby against 31c46c1: wmtk_tests and the tetwild, simwild, triwild, tracking and
topological_offset suites pass. Of the 64 integration and challenging configs, 41 are
byte-identical and 23 change; every one runs, with 0 order-2 edges outside their tube. Input
samples farther than eps, before -> after: seven tetwild configs return to their values
before envelope_reference (101168 1238 -> 1137, 1017012 36 -> 8, 104187 1222 -> 1058, 104188
1105 -> 962, 104509 317 -> 239, crown 695 -> 554, 111388 12 -> 10), 103197 39 -> 56, 101881
38 -> 304 (its value before), 46024 136 -> 112; triwild 14 of 16 configs return to their values
before, 202260 1315 -> 963 and 191265 181805 -> 149007. Iterations: 101168 65 -> 53, 101881
73 -> 49, 104187 57 -> 45, 104509 40 -> 48, 111388 37 -> 29, crown 12 -> 12;
simwild_double_sphere_notop_3d 4 -> 12 (final max energy 8.80 -> 9.28: 44 rims the
simplification made tagged, 80 curve ends anchored, 376 edges where the spheres meet left free).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s envelopes in every app

The 2D engine holds each tracked segment to the envelopes of its input set
(TriOptimizerMesh::m_input_envelopes, surface_envelope_for_edge()); in 3D the same rule lived
in two per-app copies, tetwild's and simwild's, and they differed: simwild held a tracked face
with an empty input set to the union envelope, where tetwild and triwild throw. Now the 3D engine
has the 2D rule:
- TetOptimizerMesh::m_input_envelopes, built by the application (tetwild: one per input file;
  simwild: one per label, SimWildMesh::init_label_envelopes());
- TetOptimizerMesh::surface_envelope_for_face(): a face that is not tracked is held to the union,
  m_envelope; a tracked face to the envelopes of its input set, and one with an empty input set
  throws (InputEnvelopes), as an insertion or propagation defect.
The overrides in TetWildMesh, SimWildMesh and SimWildMeshTri are removed.

An empty input set on a tracked face or segment never occurs in simwild: 0 such faces or
segments, and 0 queries answered by the union, in 16 of 16 simwild configurations of the
integration and challenging sets (instrumented build on 31c46c1).

Measured: the same change as a prototype on 31c46c1, 64 of 64 integration and challenging
configs byte-identical, and the wmtk, tetwild, simwild, triwild, tracking and topological_offset
suites pass. On 740f3ae together with the next commit: 63 of 64 byte-identical; the one that
differs, simwild_double_sphere_notop_3d, differs from its input simplification on, which is the
next commit's change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…velope

The optimizer holds each tracked face or segment to the envelope of its own input (the
previous commit). The input simplification that runs before it held every face or segment to
the union of all inputs' envelopes, so where two inputs lie within eps of each other a face of
one could be collapsed onto the other. The optimizer then started with that face outside its
own envelope. Now all four simplifications hold each face or segment to its own input's
envelope when there are two or more inputs:
- tetwild and simwild 3D: ShortestEdgeCollapse::m_input_envelopes, one envelope per input
  around its own triangles at the same eps and predicate; simwild 3D now passes each
  triangle's input (EmbedSurface::simplify_surface), as tetwild already did;
- triwild and simwild 2D: simplify_segments() takes an optional envelope per segment, built by
  utils::input_segment_envelopes().
With one input nothing changes: no per-input envelope is built.

Measured on 740f3ae, the input simplification held to the union vs to its own input, single
thread, both binaries run together on uk-kirby:
- tetwild, a wave sheet within 0.7 or 0.9 eps of a flat sheet, simplify_envelope_ratio 0.9:
  held to the union, 12 and 11 vertices are left after the simplification and both runs time
  out at 1500 s; held to its own, 62 and 83 vertices, finished in 155 s and 129 s.
- tetwild, a sheet within 0.001 of another: 10 vertices vs 80, the count the same sheet
  simplifies to alone.
- triwild, a wave curve within eps of a segment: 7 vertices vs 42; held to the union the run
  repeats the same failing splits ("outside the envelope", 128938 times) until the 1500 s
  timeout; held to its own, finished in 15 s.
- simwild 2D, the same input: 7 vertices vs 43; both finish, but held to the union the wave
  curve is lost.
- 8 two-input fixtures whose inputs are far enough apart that the simplification held to the
  union keeps them apart: simplified and final outputs byte-identical; the simplification
  takes 3% to 20% longer. The slowest of them, repeated as 4 pairs run together: 16% to 22%
  longer in every pair. Asked of both envelopes for every face, each query had the same
  answer (0 disagreements), and 96% of the envelope time is the exact search for a cover of
  the face by the prisms that cut it. Where the other input's prisms reach a face, they cover
  part of it cheaply; held to its own input's envelope, that part has to be covered by its own
  input's prisms instead: 6.29 ms per search instead of 5.51, plus 1001 searches that one
  prism of the union had made unnecessary.
- One-input fixtures: byte-identical, simplification time within -1% to +4%.
Integration and challenging sets, 64 configs: 63 byte-identical. The one that differs,
simwild_double_sphere_notop_3d, is two overlapping spheres: 244 collapses instead of 251, and both
finish in 45 s. The wmtk, tetwild, simwild, triwild, tracking, topological_offset and
shortest_edge_collapse suites pass.
New tests: sec-holds-each-face-to-its-own-input and
simplify-segments-holds-each-segment-to-its-own-input. Each also runs the simplification held
to the union and checks the wrong result, so the test can tell the two apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s ask about tracked simplices only

Since 2026-06 (triwild) the 2D collapse refused to collapse an untracked edge between two surface
vertices unless that edge lay inside the union envelope. The edge disappears in the collapse, so
the test held neither the topology -- the link condition's, which under preserve_topology never
collapses an edge of lower order than both its vertices -- nor the geometry, which is the check of
every segment the collapse makes. 3D never had it.

It was the only caller asking surface_envelope_for_edge() about an untracked segment, which the 2D
rule answered with the union; the 3D rule copied that branch in the previous commits without any
caller reaching it. Both branches go: every caller (the init checks, collapse, split, the swaps,
smoothing) passes tracked simplices, and with per-input envelopes an untracked one would now throw
as a tracked one with an empty input set does.

The 2D offset (TopoOffsetTriMesh) answers by its own per-tag tubes and applied them to that edge
too; it no longer does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kip_simplify

A key that says what it does: simplify, true by default, runs the input simplification; false
inserts the input as it is. skip_simplify is gone, without an alias, and every config that sets
neither behaves as before. Tests, docs and the Python stub follow; the stub was edited to what
pybind11-stubgen writes from the specs (component_specs::describe()), checked line by line.

A config that still says skip_simplify: wmtk_app and the Python bindings warn that the app has no
such option. simwild, which validates its keys strictly, then refuses it; tetwild and triwild
ignore it, so an old skip_simplify: true config would simplify; the next commit makes them refuse
unknown keys too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit renames skip_simplify to simplify, and simwild refuses the old key. That
config set it to false, the default; data2 f5e1ddd (branch uday/notop-simplify-half, after
1cf750d) drops the line, so the run is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify_and_setup_logger() checked keys strictly for simwild only. tetwild and triwild ignored a key
their spec lacks, warned only by wmtk_app and the Python bindings and not at all by a direct call.
Such a key is a typo or a removed option, and ignoring it runs something the caller did not ask
for: since skip_simplify became simplify, an old skip_simplify: true would simplify. Now all three
refuse one, and the strict parameter, true for one caller and false for the other two, goes.

Every tetwild and triwild config of the data repository uses keys of its spec only (27 and 30,
checked at the top level).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… around itself too

Under envelope_reference "input", after a simplification before the insertion, an order-2 edge
outside the tubes around the input's own curves is on a curve the input does not have, and is held
in a tube around itself as inserted, so the one-sided surface envelope does not leave it to erode.
An edge where two or more inputs meet was the exception and left to the surface envelopes alone:
its curve in the input is the intersection of the inputs, which only an arrangement of the original
would give. It now falls under the same rule, as every order-2 edge does with no simplification
before the insertion. One rule for what the input lacks: held as inserted.

tetwild holds its open boundaries only (holds_order2_edge(), measured on Thingi10K 111388), so
this reaches simwild 3D, whose non-manifold edges are where its pieces meet.

The comment at the test that decides it says why the test needs no correspondence: an edge is held
within eps of an order-2 curve of its own inputs, not of the curve it came from, and the test is
that question, asked with the predicate that then enforces it.

Test: order2-tags-where-inputs-meet-around-themselves (two squares, each its own input, crossing:
every edge where they cross is tagged, with and without the input's curves set -- with them, 0
before this commit -- and every tagged edge stays in its tube after mesh_improvement(2)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… input, at the full eps

Two corrections to envelope_reference, in the one check every app makes before any work.

"simplified" built every envelope, tube and anchor around the simplified geometry at what the
simplification left of eps, eps - simplify_eps, so that the output stayed within eps of the input;
at or above eps it fell back to the full eps with a warning, a switch of guarantee no caller asked
for. A dense input is simplified at several times eps on purpose, which only that fallback ran.
Now "simplified" means the simplified geometry replaces the input: everything is built around it
at the full eps, the output is within eps of it and so within simplify_eps + eps of the input, and
any simplify_eps is allowed. "input" still refuses simplify_eps at or above eps: the simplified
geometry could start outside the envelope around the input it is held to.

"simplified" with no simplification -- simplify false, or simwild under preserve_topology in 2D or
outside remeshing -- has no simplified geometry to build around, and is now refused.

utils::check_envelope_reference() replaces check_simplification_inside_envelope() and makes both
refusals; optimization_envelope_eps(), which became the identity, goes, and with it
OptimizerParameters::order2_envelope_eps, the override that made the tubes and anchors follow the
charged eps. The spec docs of envelope_reference say what each value guarantees; simwild's also
says that curves where two inputs meet are held around themselves (the previous commit), and
tetwild's no longer names non-manifold edges, which tetwild does not hold.

Test: envelope_reference_refusals replaces envelope_budget, over simplify_eps from 0 to 10 eps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plify_rel or eps_simplify

tetwild and triwild set the input simplification's eps as a fraction of the optimizer's,
simplify_envelope_ratio (0.5); simwild as a length of its own, eps_simplify_rel of the inputs'
diagonal or the absolute eps_simplify. Now all three take simwild's two keys and its rule: the
absolute one when positive, else the relative one of the same diagonal eps_rel is of.
simplify_envelope_ratio is gone, without an alias; the strict key check refuses it.

The ratio suggested a link to eps that envelope_reference "simplified" no longer has: there the
simplified geometry replaces the input and any simplification eps is allowed, a coarse one for a
dense input included. Under "input" one at or above eps is still refused.

The defaults are fixed lengths, 5e-4 of the diagonal in tetwild and triwild and 2e-4 in simwild,
and do not follow eps_rel. 5e-4 is 0.5 of eps at the default eps_rel 1e-3, so every config at the
default eps_rel runs bit-identically (5e-4 x diag == (1e-3 x diag) x 0.5 in floating point); all
57 tetwild and triwild configs of the data repository do. An eps_rel at or below 5e-4 is refused
under "input" unless eps_simplify_rel is lowered with it. The docs' sweeps over eps_rel set
eps_simplify_rel to half of it, as do the two tests that set eps_rel (tetwild's coarse volume round
trip, the Python triwild memory route), so their runs are as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_link_condition warnings go

- tetwild and simwild: the tube comments still said that under envelope_reference "simplified" it
  is built at what the simplification left of eps; it takes the full radius since "simplified"
  stopped charging the simplification.
- tetwild and simwild specs, order2_envelope_ratio: the doc still described the tube built around
  the boundaries after the simplification, drifting up to (eps + the simplification's eps) from the
  input's; under "input" it is built around the input's own curves since envelope_reference.
- tetwild and triwild warned when a config still set simplify_use_link_condition. With unknown keys
  refused by the spec check, the warning can no longer be reached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the envelopes when false

With two or more inputs each is held to its own envelopes: one surface (2D: curve) envelope per
input, one order-2 and one feature tube per input, and the input simplification's envelope per
input likewise. That is the default and stays so. per_input_envelopes false shares them, one
envelope of each kind for all inputs -- the union, as with one input -- for speed where inputs come
within eps of each other (the 3D simplification measured 3% to 20% faster shared, the queries
answering the same), at the cost of letting an input end up on another within eps. It switches
every kind at once: a shared simplification followed by a per-input optimizer would hand the
optimizer faces outside their own envelopes, the timeouts the per-input simplification fixed.
Anchors are per point either way. An input is an input file in tetwild and triwild, a label in
simwild.

OptimizerParameters::per_input_envelopes, read by each app; the per-input builders return the shared
answer without it (TetOptimizerMesh's tubes, TriOptimizerMesh::init_input_envelopes(),
SimWildMesh::init_label_envelopes(), tetwild's per-input surface envelopes), and the
simplifications take it: ShortestEdgeCollapse::create_mesh() and simwild's simplify_surface() and
simplify_curves() as an argument, triwild's per-segment envelopes by not building them. The faces'
inputs still go into face_attrs, so the output's face tags are unchanged.

Test: sec-holds-each-face-to-its-own-input also runs with per_input_envelopes false, which gives the
union's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he one-sided envelope cannot

The commit before last removed the 2D collapse's test of the collapsing edge itself, on the argument
that the edge disappears in the collapse: the topology is the link condition's, the geometry the
check of every segment the collapse makes. The argument was wrong, and two tests show it:
orientation-2d-square-resting-on-a-strip lost the square (tracked area 1.0 for 4) and
orientation-2d-square-and-clockwise-square 4.8% of its area after mesh_improvement(3); with the
test back both pass (measured on kirby, the triwild suite built with and without it). The envelope
is one-sided and the union of a whole input's curves, so a curve vertex moved across a region onto
another curve of its input leaves every new segment on a curve, inside, while the region between
folds away; the collapsing edge, crossing that region, is outside. The comment there now says so.

The 2D rule's untracked branch, which answers that test, is back with it. The 3D one stays removed:
3D has no such test, and no 3D operation asks about an untracked face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as in 2D

When both ends of a collapsing edge are on the surface and the edge is on no tracked face, the
edge itself must now lie inside the envelope, as the 2D collapse has required since 2026-06.
The check of every face the collapse keeps does not cover it: the envelope is one-sided and the
union of a whole input's faces, so a surface vertex moved through a solid onto another surface of
its input keeps every face on a surface while the solid between folds away. In 2D that is what
orientation-2d-square-resting-on-a-strip showed when the test was removed for a commit.

3D had no such rule. Measured with scratch counters on tetwild's orientation-configurations (a
box resting on a plate and the rest): 1956 such collapses were tried and none went through, but
they were refused by the quality gate (976), which the coarsening pass skips, by the inversion
test (967) and by the faces' own check (13), none by a rule of their own. The test now asserts
that the new rule refuses some, besides the volumes it checked before.

The envelope comes from a hook, collapsing_edge_envelope(), the union m_envelope by default, as
2D answers an untracked segment. The offset answers none for now: its front's vertices are surface
vertices that move by design and lie outside m_envelope, so the union would refuse the front's
collapses; it gets the test with the offset following the other apps' envelope rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the envelope's own eps

Both divided by sqrt(eps2). eps2 is the sampled path's shrunk acceptance radius squared,
(eps - eps/sqrt(3))^2 = (0.42 eps)^2, kept whether or not the exact predicate is in use; the
envelope's radius, what every query guarantees, is eps. So the offset's containment audit stated
its margins 2.37x too high, and tetwild's "tetrahedralisation envelope: ... (eps ...)" line printed
0.42 eps as eps. Envelope::eps() returns the eps the envelope was built with (sampling_dist, set by
every init()), and both use it. No mesh changes; only the log text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tetwild and simwild

TetOptimizerMesh::m_s_envelope scales the envelope energy of smoothing_mode "exact" -- the squared
distance to the input -- by 1/eps^2, so it enters relative to the envelope. tetwild and simwild set
it; the offset never did, so it stayed at -1, which turns the term's penalty into a reward under
"exact". Now build_envelopes() sets it from envelope_size, the offset's eps. "projected", the
default, does not read it, so default runs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…envelopes moved to the base

The per-label envelopes were simwild's (SimWildMesh::init_label_envelopes()): member b around the
input triangles carrying label b when they are known, else around the mesh's faces carrying it.
That is the 3D twin of TriOptimizerMesh::init_input_envelopes(), and the offset needs the same.
Moved to the base unchanged, with the same name and arguments as in 2D, returning the number of
members; simwild keeps its wrapper and its log line. No output changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o copies

tetwild and simwild each overrode smoothing_energy_envelope() with the same code: a vertex on a
tagged curve toward its tube, else toward the member of its faces' input sets it is farthest from,
else the union. It is now TetOptimizerMesh's default, and both overrides go, with simwild's
Smooth.cpp, which held nothing else. topological_offset will ask it for a held vertex. No output
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es; the wall by the box rule

The offset's envelopes now follow tetwild's and simwild's rules.

- Labels. An input is a label: a face tag, or the tet tag of a region a face bounds, as simwild's
  (bit F + t - 1 for tet tag t, F face tags), and the envelope surface group's tag on a face of it.
  assign_face_labels() writes them into the tracked faces' input sets before the march, and the
  operations carry them; the output takes only the face-tag bits (collect_input_set_tags()).
- Surfaces. The held faces -- the input complex's boundary, and in a TetWild pass the other region
  boundaries -- build m_envelope, their union, and the base's per-label m_input_envelopes
  (TetOptimizerMesh::init_input_envelopes()); each is held to its own labels' envelopes by the
  base's rule, the union with one label or with per_input_envelopes false (new key, as in the other
  apps). Until now one union held them all, so a face of one label could end up on another's
  surface within envelope_size.
- Pull and smoothing containment. A held vertex is pulled by the base's rule, toward the member of
  its faces' labels it is farthest from, and each face at it is contained per face, as in the other
  apps; both were the one union.
- The domain wall. It is no longer held by an envelope and its vertices are no longer smoothed: the
  box rule holds it, as in tetwild and simwild (no smoothing, collapses only along the walls, a
  split stays on the walls both ends share). The wall was held and smoothed since 2026-08 because,
  with it frozen and its edges unsplittable, a tet resting on the box bisected onto the plane (max
  AMIPS 6.9e21 against 364); wall edges split now. Chosen for consistency with the other apps; the
  offset configs' max AMIPS is the check, and the wall envelope the fallback if the cascade returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in tetwild and simwild

The offset held no curve and no point: a rim of a sheet, or a junction where labelled regions meet,
was held by the surface envelope alone, which is one-sided and lets a curve erode or slide.
hold_curves_and_points(), right after build_envelopes() and before the march, does what simwild
does after its insertion: the base's rule picks the order-2 edges (1 tracked face, or 3 and
more), tag_order2_edges() tags them, one tube per label around them as they are -- the offset never
simplifies its input, so that is the input's own curve -- and anchor_feature_points() anchors the
curve ends and junctions. The operations carry the tags. The front does not exist yet, so it is
never among them. New key order2_envelope_ratio (default 1), the tube's half-width as a fraction
of envelope_size, as in tetwild and simwild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…faces, as in the other apps

The 3D collapse refuses an edge between two surface vertices that is on no tracked face and leaves
the envelope, so a vertex cannot cross a solid onto another surface of its input
(collapsing_edge_envelope()). The offset answered none, its front's vertices being surface vertices
that move by design outside m_envelope. Now it answers m_envelope, the held faces' union, when both
ends are on the held surfaces, and none when an end is on the front; an edge from the input complex
to the front is refused anyway (app_both_surfaces). Also rewraps the previous commit's comment to
100 columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs around a TetWild pass

InputEnvelopes is not copyable (it holds the mutex of its intersection cache), so the commit that
gave the offset per-label envelopes did not compile where build_final_envelopes() set the loop's
aside and release_final_envelopes() put them back. Keep the members instead, and restore them
through InputEnvelopes::set().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pass even when it is none

Since the wall is the box rule's (2fa364b), an input complex of curves or points holds no
face, and the loop runs with no envelope. release_final_envelopes() put the loop's envelope back
only when there was one, so after a TetWild pass such a run kept the final pass's envelope, every
region boundary held, for the rest of the loop. A flag build_final_envelopes() sets now decides.

The test held_faces_and_one_envelope asserted the rules before 2fa364b: the wall held, one
envelope for every held face. Renamed held_faces_and_their_envelopes, it asserts the wall not
held, a region boundary held to its labels' envelopes in the final pass, and the loop's envelope
-- none, in its two-tet scene -- back after the pass, which fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nvelope-reference

The data pin's two lines join at data2 6dc625a, the merge of data2 #13: main's de49170 and one
config after it, and this branch's f5e1ddd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o uday/envelope-reference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… envelope through its members

Since each held face is held to its own labels' envelopes (2fa364b), a face on two or more
labels -- a region boundary between two tags -- is held to their intersection
(InputEnvelopes::containment()), a composite that answers is_outside() and nothing else: its BVH
is null, and CompositeEnvelopes.hpp states that it must never be asked squared_distance(). The
offset's containment audit, run after construction and in the loop, asked it that for the margin
of every held face, and every 3D offset run with such a face crashed there (all 9 cube configs of
the data repository, 6dc625a). The audit now asks the members, as the 2D audit asks its tag
tubes; the face's distance and margin are the largest over them, since inside the intersection is
inside each.

The test held_faces_and_their_envelopes now audits its two-tag face in the final pass, which
crashes without the fix.

Measured, the data repository's 9 cube offset configs (6dc625a, one thread), this branch against
main 6d17d30, which holds and smooths the wall: before the fix all 9 crash, main finishes 8 and
times out on cube_small_offset_conv0.025 at 3600 s. With it, 8 of 8 finished converge, final max
AMIPS 7.0 to 73 against main's 10.7 to 70 (the wall frozen by the box rule, 2fa364b, brings no
bisection cascade back); the ninth was still running at the time of this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from uday/shared-order2-protection to main November 4, 2026 15:21
@udaykusupati
udaykusupati merged commit ab4fe46 into main Nov 4, 2026
12 checks passed
@udaykusupati
udaykusupati deleted the uday/envelope-reference branch November 4, 2026 15:25
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