Give the remaining partial sort comparators a total order - #953
Open
danielepanozzo wants to merge 1 commit into
Open
Give the remaining partial sort comparators a total order#953danielepanozzo wants to merge 1 commit into
danielepanozzo wants to merge 1 commit into
Conversation
A sweep of every std::sort / std::unique / priority-queue comparator in src/wmtk
and the components, looking for the failure this repository keeps hitting: a
comparator that treats two *distinct* elements as equivalent. std::sort is not
stable, so the order it leaves such elements in is unspecified and differs between
libc++, libstdc++ and MSVC -- and in each case below that order reaches the output.
Six sites. Four share one idiom: sort on a partial key, std::unique to collapse
duplicates, and then read the very fields the comparator ignored. That reads as
correct, because the grouping and the deduplication both work; only the choice of
which duplicate survives is left to the standard library.
TetMesh::get_edges -- entries are (v0, v1, tuple) and every tet incident to an
edge contributes one, so an edge shared by k tets appears k times. The unique
below kept an arbitrary one, so *which tet the returned tuple lives in* was
unspecified, and get_edges feeds the operation queues. Tie-break on the tuple,
whose operator< already orders totally.
TetMeshTriangleInsertionConn, old_face_vids and new_face_vids -- arrays of
{v0,v1,v2,tid,l_fid} ordered on the vids alone. The survivor is consumed as
tuple_from_face(info[3], info[4]), so the discarded fields are the output.
Comparing the whole array keeps the vid grouping the unique relies on -- the
first three elements still dominate -- and adds (tid, l_fid) as the tie-break.
orig/EdgeSplitter.h, cmp_es -- the split priority queue ordered on edge length
and ignored v_ids, so equal-length edges, which symmetric input produces in
quantity, were popped in an unspecified order, and that is the order they are
split in. Its siblings cmp_ec and cmp_er already tie-break on v_ids; this one
was missed.
TopoOffsetTetMesh.h and TopoOffsetTriMesh.h, sort_edges_by_length -- ordered on
length alone, and the order is the marching-tets split order, so it decides new
vertex ids and the frontier vertex list that labels offset tets.
Sorts whose comparator is the default one over a fully ordered type are left alone:
equivalent elements there are indistinguishable, so no order over them is
observable. That covers the great majority of the ~150 call sites.
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.
Give the remaining partial sort comparators a total order
A sweep of every
std::sort/std::unique/ priority-queue comparator insrc/wmtkand the components, looking for the failure this repository keeps hitting.The bug class.
std::sortis not stable. When a comparator treats two distinct elements as equivalent, the order it leaves them in is unspecified — and differs between libc++ (macOS), libstdc++ (Linux) and MSVC. In each case below that order reaches the output, so the same input produces a different mesh depending on the platform. This has already been hit four times before:TetMesh::get_edges,igl::sortrowsin the vertex dedup,sort_edges_by_length, and the morton partition sort in #952.Six sites
TetMesh::get_edgesstd::uniquekept an arbitrary one, so which tet the returned tuple lives in was unspecified;get_edgesfeeds the operation queuesTetMeshTriangleInsertionConnold_face_vids(tid, l_fid)of{v0,v1,v2,tid,l_fid}tuple_from_face(info[3], info[4])— the discarded fields are the outputTetMeshTriangleInsertionConnnew_face_vidstriangle_insertion_afterorig/EdgeSplitter.hcmp_esv_ids, keeping only the edge lengthTopoOffsetTetMesh.hsort_edges_by_lengthTopoOffsetTriMesh.hsort_edges_by_lengthTwo things the sweep surfaced
Four of the six share one idiom: sort on a partial key,
std::uniqueto collapse duplicates, then read the very fields the comparator ignored. It reads as correct — the grouping works and the deduplication works; only the choice of which duplicate survives is left to the standard library. Where the fix widens the sort comparator to the whole array, theuniquepredicate still compares the vids alone, so the grouping it relies on is unchanged and the wider comparison only orders within each group.cmp_eshad two siblings that were already right.cmp_ec(EdgeCollapser.h) andcmp_er(EdgeRemover.h) both tie-break onv_ids; only the splitter was missed. That is an oversight with a clear fingerprint rather than a deliberate choice.Scope
~150
std::sortcall sites were examined; six were changed. Sorts whose comparator is the default one over a fully ordered type (size_t,std::array<size_t,N>,std::pair) are deliberately left alone: equivalent elements there are indistinguishable, so no order over them is observable, and tie-breaking them would be noise. Two further sites were examined and left:unique_face_tuplesalready usesstd::stable_sort(so its survivor is deterministic without a key), andunique_directed_edge_tuplesis dead code — both lambdasthrowon entry.A note for whoever merges this with #949
The tie-breaks here are unconditional. On
danielepanozzo/integration-test-hashes(#949), the equivalents forTetMesh::get_edgesandsort_edges_by_lengthsit behind#ifdef WMTK_FP_STRICT— that macro does not exist on this stack at all. Those hunks will conflict textually, and the merge forces a policy decision: is a total-order comparator a correctness property (always on) or a reproducibility one (strict builds only)? The cost is one integer comparison on a tie; the symptom is a mesh that differs by platform.Testing
51/51 unit tests and
Integration_Testspass.Worth being explicit about what that does not show: these fixes are reasoned, not measured. The golden-hash tests are what would prove them, and they live on #949 — a different stack — while most of these paths are multithreaded-sensitive anyway.
Integration_Testsonly checks termination, so green means nothing regressed, not that determinism improved.Stacked on #952.
🤖 Generated with Claude Code