Repository navigation
topological_offset 3D: hold every region boundary and a passed envelope; the march carries the edge tags - #1159
Open
udaykusupati wants to merge 15 commits into
Open
udaykusupati wants to merge 15 commits into
udaykusupati wants to merge 15 commits into
Conversation
…ld and simwild do Until now the loop held only the input complex's boundary; every other region boundary was tracked (the operations kept its topology) but held by nothing, so the band pushed it aside, and only the final TetWild pass held it where the loop had left it. Since 1c247b2 (2026-10-13). Now every tracked face but the domain wall (the box rule's) and the front is held to the envelopes of its own labels, in the loop as in a TetWild pass. No key: the free behaviour is gone. - face_is_held(): every tracked face off the wall and the front. build_final_envelopes() adds only the front's envelope; the region boundaries are held where build_envelopes() captured them, before the march. face_is_complex_boundary(), release_final_envelopes() and the final pass's envelope swap (m_held_*, m_loop_*, m_final_envelopes) are removed: nothing else read them. - After the march, the load-time tracking rule again (face_is_tag_boundary(), untrack_covered_boundaries()): the retagging writes cell tags only, so a region boundary the band covers -- band cells on both sides -- kept its tracked flag, and held now it would be a surface inside the band. One rule for cell and face tags: on a face the band covers, the face tags and the sheet group are cleared unless protected_tags names them, as the cells' tags are; never on the input complex's own faces. - The face split (simplicial embedding) gives the split face's three pieces its attributes: a piece of a split simplex lies on every surface the simplex was on, as the edge split's halves and the 2D edge split's do. The pieces got fresh attributes, so a tracked face split there lost its tracking. The new vertex is flagged on the surface, its orientation converted per piece. - A front vertex held only by another region's boundary keeps the offset term and is reachable (vertex_is_held_on_input()): the term is left out, and the vertex pinned, only on the input complex, where d = 0 and has a kink. Otherwise the front could not settle where it meets another region. Measured against main (ab4fe46): the offset suite passes; the data repository's 9 3D cube configs give byte-identical outputs where they finish (8; cube_small_offset_conv0.025 times out on both). double_sphere (offset tag_0, tag_1 the other region): tag_1's boundary moves 0.25 eps (main pushes it 20.3 eps). OPEN: double_sphere does not converge (5 turns, and not in 15): 40 front faces end turned more than 30 degrees from the field (main 1), 28 of them touching tag_1's held boundary and the rest beside them, in two clusters where the front, which can no longer push tag_1 aside, ends on its surface at a shallow angle; the exit's fold test counts them. To discuss: accept it as the cost of holding, or what a fold means for a face at a held junction. Tests: held_faces_and_their_envelopes (every region boundary held), band-covered-region-boundaries and band-covered-face-tags (untracked unless protected; a face-tag selection's own faces kept), face-split-keeps-the-surface (the corner pocket). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their own vertices The march's edge split restored the split face's attributes as they were on both halves. A face tag's orientation is stored against the face's ascending vertex ids, so the copy was right on a half only when the new vertex sorted into the place of the corner it replaced; otherwise the half came out turned over, and so did the triangle the output orients from it (io::collect_input_set_tags()). Each half now gets the parent's orientation along its own vertex order, as the engine's split converts it (TetOptimizerMeshSplit.cpp). The 2D edge split had the same copy and gets the same conversion; it changes no 2D result today (the 2D offset sets no orientation, and the driver refuses 2D). Test band-crossed-sheet-keeps-its-orientation: a protected face-tagged sheet the band crosses keeps its normal on every split half, inside the band and beyond it. It fails on the previous commit (2 of 17 assertions) and passes here; the offset suite passes (114 test cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… protected, hidden double_sphere, offset of tag_0 at 0.05 with tag_1 held (every region boundary is held since 2026-11-04) or protected: the front meets tag_1's surface along a junction curve. Neither variant converges reliably until the junction is handled (2026-11-04, 15 turns, 4 threads: held 13 folds left, protected 3), so the case is hidden ([.]) and recorded by the hidden runs; it moves into CI once it passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…route
TetOptimizerMesh::require_box_boundary() and its 2D twin on TriOptimizerMesh throw unless
the mesh fills its box: every boundary face (2D: edge) a face of the box (m_is_bbox_fs >= 0)
and untracked, naming the count of each kind found otherwise. tetwild's
init_from_tagged_mesh() and triwild's tagged route call it in place of their own copies;
the messages keep their wording ("tracked faces of the tagged mesh lie on its boundary",
filter "none"). simwild keeps its own check of the tracked kind only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does not fill its box is refused The offset tracked every boundary face as a region boundary (face_is_tag_boundary()) and then exempted it from the envelopes (face_is_held(), 2fa364b): the box rule held the wall, as in tetwild and simwild, while the operations still kept it as a tracked surface. Now a boundary face bounds no second region and is untracked, unless a face tag or the sheet tracks it -- a filtered mesh's boundary -- and the driver refuses a mesh that does not fill its box right after loading it (TetOptimizerMesh::require_box_boundary(), tetwild's and triwild's check). What relied on the wall being tracked: - vertex_is_on_surface() counted the wall through vertex_is_on_region(); the substructure queries then found a wall vertex with no tracked face (TetMesh::compute_vertex_order() threw "Vertex must have incindent surface faces" in frame-gather-threads-identical). It counts the tracked faces only now: a region boundary or the offset surface. - The march's edge and face splits raised m_is_on_surface for a midpoint on the wall; they raise it for a region boundary only, as no wall vertex carries it at load. - m_is_on_region was kept off a tracked face without a tet across (the wall); every tracked face is a region boundary now. - Comments describing the wall as tracked or held, here and in the header. Tests: held_faces_and_their_envelopes checks the wall untracked (it checked it tracked and not held); "Offset accepts named tagged meshes" takes the unit cube in six tets, a single tet's fourth face lying in no plane of its box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e offset surface An `envelope` that is a labelled reference (InputData::envelope_reference), the first component's input, was carried unchanged while the offset built its envelopes around the mesh it received. It is held to now, as simwild's reference route holds one (simwild.cpp's run_3D, the engine's checks from #1157): - Its triangles are not the offset's envelope surface group (init_from_image()'s V_env/F_env, m_sheet_*): the driver passes none, and keeps the reference's rows of this mesh's labels (io::held_rows()) on the mesh (m_reference_V/F/labels, m_has_reference). - build_envelopes() builds m_envelope and the label envelopes around those rows instead of the held faces, after require_reference_face_labels(), and runs check_reference_containment() before anything moves. label_name() names the labels as the output does. - The order-2 tubes and curve-end anchors (hold_curves_and_points()) around the reference's curves: the driver calls set_input_curves(), as simwild does. No feature tube: the offset takes no feature curve, so init_reference_feature_tubes() would hold nothing. - Handed on: the reference's rows plus the final offset surface's triangles (a band cell against a cell neither band nor input complex), labelled by the band's tags (offset_output_tags, as io::tag_labels() names them): the offset surface is new, so it is its own reference. Built with io::append_envelope_block() / append_envelope_points(); unchanged where there is no offset surface. File route: <output>_envelope.msh, as before. Docs: the /envelope spec doc and the tutorial; the /input doc also states the box requirement of the previous commit. python test_envelope_is_passed_on_only_by_the_caller gives the offset simwild's eps as envelope_size: at the default (1e-3 of the diagonal, against simwild's 1e-2) the offset now refuses simwild's faces, 391 outside, as tetwild and simwild refuse a narrower eps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ffset's refusals - The offset holds tetwild's envelope (memory route, filter none, at tetwild's eps) and hands it on with labels a, b, off: a's and b's rows as given, off's exactly the faces between the band and the rest; the file route writes the same as o_envelope.msh; simwild holds the offset's mesh to it and returns it as given. - The offset refuses tetwild's envelope moved by 10 of its eps (faces of a and b outside, by label) and one with b renamed c (no triangle labelled b). - The offset refuses a filtered tetwild output: a nonzero count of tracked boundary faces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TopoOffsetTetMesh::swap_face_before(), the loop's copy of the base's face swap, cached the two cells' face attributes but not their edge tags (feature_edges_cache()), which the base's swap_face_after() -- the offset has none of its own -- restores on the three new cells: they took whatever the thread's previous operation had left in the cache. hold_curves_and_points() tags the order-2 edges, so a loop 2-3 swap could drop or misplace a curve's tags. Measured on the rod branch (33548e2, the same copy): 2 tagged edges at the start of the loop, 8 rod edges at its end with 1 still tagged. Test swap-face-keeps-edge-tags: the admitted swap of swap-face-gate-on-energy with (a,d) tagged order-2; before the fix the tag and its input were gone after the swap (0 tagged edges), after it (a,d) is the one tagged edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The offset handed on, under the band's labels, only the offset surface. Where the band wraps a region, the band's boundary also has an inner part, its interface with that region, and the next component labels each face there with both the region and the band: it must lie in both envelopes. With the offset surface alone, simwild found those faces outside the band's envelope (72 on a unit tet). Every face between a band cell and a cell outside the band is now handed on, as every other label's envelope covers its region's boundary. The spec and the tutorial say so. Test: simwild -> topological_offset of region a -> simwild; failed before (72 faces outside off), passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data2 #14 drops the group's two slowest cube cases, cube_small_offset_conv0.1 (13 min, 1.0M tets) and conv0.025 (32 min, 2.85M tets), 91% of its 49 min, and parks the nine 2D cases, which throw while the offset refuses 2D input. The seven cube cases left take 4.3 min. The test's comment says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fsets with the optimization hold_curves_and_points() tags the order-2 edges (open boundaries) before marching_tets(), and the march's edge split cached only the offset's own edge attributes. The tags are stored by edge slot, so after the march each sat on whatever edge took its slot over, and the tubes held the wrong edges. Measured on an open sheet offset with the optimization: at the loop's start none of its 16 open edges was tagged and 16 tagged edges lay outside their tube; the construction pass then cut its boundary (area 1.0 -> 0.894, corners up to 0.087 off at eps 0.0035), 7% of the sheet lost at the end. The march's edge split now caches and restores the tags as the base's split does, its halves inheriting the split edge's tags: every open edge stays tagged, the area is 0.9999, the worst boundary point 0.0012 off. The face and tet splits run only in the simplicial embedding, before the tagging. Main has the same order. With that, the refusal of a face-tag selection under optimize_offset (the optimization did not hold face tags, efbf410) is lifted. Its Python test becomes the regression test: the offset of an open sheet converges, and the sheet, the other face tag and the region stay within the envelope. The component chain tests run the offset with the optimization, as users do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simwild refused a tagged mesh whose tracked faces (2D: edges) lie on its boundary, and took one whose untracked boundary lies in no plane of its box -- an object's own tet mesh -- whose boundary nothing then held: smooth_before() freezes only the vertices on the box's planes. Its tagged route (init_from_image(), both overloads, 3D and 2D) now runs the engine's require_box_boundary(), as tetwild's and triwild's tagged routes and the offset do, after init_surfaces_and_boundaries() rather than inside it, so the conformance tests that build a tet ring by hand keep testing the operations. Tests: three component_io tests gave simwild meshes that do not fill their box (one tet, two tets); they now use the unit square and the unit cube in six tets (the 3D curve becomes the cube's main diagonal, an interior edge of the tagged cell), their checks unchanged. New: simwild refuses one tagged tet (triangle) with its boundary as the envelope, naming the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simwild given a tagged mesh of one region and no envelope tracks nothing, and builds its envelope from the tracked faces (2D: edges): none. Envelope::init() then crashed in SimpleBVH::BVH::init, in 2D and in 3D. init() now builds neither the BVH nor the exact structure when given no primitives (Envelope::empty()); such an envelope contains nothing, so every is_outside() answers true, and it has no nearest point, which the nearest-point and distance queries refuse with a message (require_primitives()), as the primitive queries refused an envelope no init() had filled in. Test: simwild remeshes the unit square (2 triangles) and the unit cube (6 tets), one region, no envelope given; segfaulted before in both dimensions, passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_memory_io.py gave simwild one tagged triangle and one tagged tet, and the offset one tagged tet: meshes that do not fill their box, which simwild and the offset now refuse (require_box_boundary()). They use the unit square in two triangles and the unit cube in six tets, with their boundaries as the envelope, as the C++ component IO tests do; tagged() tags every cell. Linux CI failed python_memory_io on them; ctest 262/262 locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
The topological offset (3D) holds every region boundary in its loop and holds a passed envelope, as tetwild and simwild do, so it can sit in a chain of components: within eps of the first component's input. On the way, two defects that are on main too: the march scrambled the edge tags, and simwild 2D/3D crashed building an envelope around nothing.
Commits
require_box_boundary(). Every boundary face a face of the box, untracked; tetwild's and triwild's tagged routes and the offset call it (tetwild and triwild had their own copies).feature_edges_cachewas missing inswap_face_before): 2–3 swaps dropped or misplaced order-2 tags.[offset]group drops its two slowest cube cases (45 of its 49 min) and parks the 2D cases while the offset refuses 2D input.optimize_offsetis lifted; its Python test is the regression test.require_box_boundary(): an object's own tet mesh (boundary in no plane of the box) was taken and its boundary held by nothing. Three toy-mesh tests now use the unit square / the unit cube in six tets, their checks unchanged; a new test checks the refusal.SimpleBVH::BVH::init(2D and 3D).Envelope::empty():is_outside()answers true, nearest-point queries refuse with a message.Behaviour changes
envelopegivenoptimize_offsetTests
macOS arm64, Release: topological_offset 115/115, component_io 57/57, simwild 50/50, tetwild 50/50, triwild 61/61, wmtk_tests 188/188, Integration_Tests (55 configs),
python/tests62 passed. Hidden run (every[.]case) on the mac-mini: running; the result follows as a comment, before the merge.🤖 Generated with Claude Code