Core data-structure overhaul: interned simplices, facet adjacency, incremental hull#5
Merged
Merged
Conversation
Pure mechanical move, no logic changes: the pure-Rust Triangulation core stays in src/triangulation.rs; argument parsing, result conversion, TriangulationError::into_pyerr, the proxy/iterator views, and the PyTriangulation class move to the new src/py.rs. Prepares for reworking the core data structures without PyO3 noise in the diff.
Replace the FxHashSet<Vec<usize>>-everywhere layout (which cloned and re-hashed whole simplices in every hot path) with interned storage: - a slab assigns each simplex a small integer SimplexId; the sorted vertex list is stored once and referenced by id everywhere else - vertex_to_simplices becomes vertex -> FxHashSet<SimplexId> (integer sets instead of sets of cloned Vecs) - a new facet index maps every (dim-1)-face to the ids of its incident simplices, maintained by the single link/unlink pair of mutation primitives, together with the derived boundary-facet set (hull) and an overfull-facet count (corruption detector) This turns the hot paths from set intersections over cloned vectors into single hash lookups: - locate_point walk steps and containing() facet queries are one facet-map lookup - the bowyer_watson cascade finds neighbours via dim+1 facet lookups instead of unioning and scanning every simplex around all vertices - extend_hull reads the maintained boundary set instead of recounting every face of every simplex per outside-hull insertion (was the O(n^2) bottleneck for hull-heavy workloads), and hull() is O(hull) - reference_invariant() now also cross-checks the facet index, boundary set, and overfull count against a recount Behavior is unchanged: the full pytest cross-validation suite against the Python reference passes, and stress sweeps (mixed-scale, off-origin, near-duplicate, random) show the same pass/fail profile as main. A/B benchmark (best of 3, same machine, incremental insertion): 2D 5000 pts 0.296s -> 0.143s, 3D 2000 pts 0.540s -> 0.163s, 4D 500 pts 1.959s -> 0.241s, hull-heavy 2D 3000 pts 3.271s -> 1.058s, hull-heavy 3D 1500 pts 3.395s -> 0.698s.
This was referenced Jun 10, 2026
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.
Summary
Reworks the core triangulation storage for performance and maintainability. The Python API is unchanged and drop-in compatible with
adaptive.learner.triangulation; all 64 cross-validation tests pass unmodified.Commit 1 mechanically splits the PyO3 surface (argument parsing, error conversion, proxies,
PyTriangulation) out oftriangulation.rsinto a newsrc/py.rs, leaving the core pure Rust.Commit 2 replaces the
FxHashSet<Vec<usize>>-everywhere layout — which cloned and re-hashed whole simplices in every hot path — with interned storage:u32SimplexId; the sorted vertex list is stored once and referenced by id everywhere elsevertex_to_simplicesbecomesvertex → FxHashSet<SimplexId>(integer sets instead of sets of clonedVecs)link_simplex/unlink_simplexpair, together with the derived boundary-facet set (the convex hull) and an overfull-facet count (corruption detector)This addresses three known hot spots:
locate_pointwalk steps andcontaining()facet queries are now one hash lookup (previously a set intersection over clonedVecs)bowyer_watsoncascade finds neighbours via dim+1 facet lookups instead of unioning and scanning every simplex around all cavity verticesextend_hullreads the maintained boundary set instead of recounting every face of every simplex per outside-hull insertion — this was the O(n²) bottleneck for hull-heavy workloads — andhull()is now O(hull)reference_invariant()additionally cross-checks the facet index, boundary set, and overfull count against a recount, so the stress suites verify the incremental bookkeeping.Benchmarks
A/B against unmodified main, same machine, best of 3, incremental insertion (
add_pointloop):examples/adaptive_learnernd.pyend-to-end: 3.5× vs pure-Python LearnerND (unchanged from main).Verification
pytest: 64 passed (cross-validation vs Python reference on randomized 2/3/4D inputs)cargo test --lib --tests: 18 passed (incl. new tests for facet-index consistency, incremental hull, andcontainingfacet queries)src/tolerances.rswhere the Python reference fails more often; robustness work is a separate follow-up PR)