Skip to content

Commit ce43abf

Browse files
authored
Core data-structure overhaul: interned simplices, facet adjacency, incremental hull (#5)
* refactor: split PyO3 surface out of triangulation.rs into src/py.rs 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. * perf: intern simplices, maintain facet adjacency and hull incrementally 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.
1 parent 3d236b1 commit ce43abf

6 files changed

Lines changed: 1377 additions & 1035 deletions

File tree

‎Cargo.lock‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ numpy = "0.23"
2222
nalgebra = "0.33"
2323
thiserror = "2"
2424
rustc-hash = "2"
25+
smallvec = "1"
2526

2627
[profile.release]
2728
lto = true

‎benches/bench_triangulation.rs‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ fn generate_points(dim: usize, count: usize, seed: u64) -> Vec<Vec<f64>> {
3232

3333
fn first_simplex(triangulation: &Triangulation) -> Simplex {
3434
triangulation
35-
.simplices
36-
.iter()
35+
.simplices()
3736
.next()
3837
.cloned()
3938
.expect("triangulation should not be empty")
@@ -72,9 +71,7 @@ fn outside_point(triangulation: &Triangulation) -> Vec<f64> {
7271
}
7372

7473
fn insert_pending_vertex(triangulation: &mut Triangulation, point: Vec<f64>) -> usize {
75-
triangulation.vertex_to_simplices.push(Default::default());
76-
triangulation.vertices.push(point);
77-
triangulation.vertices.len() - 1
74+
triangulation.add_vertex(point)
7875
}
7976

8077
fn constructor_benches(c: &mut Criterion) {
@@ -101,7 +98,7 @@ fn constructor_benches(c: &mut Criterion) {
10198
let coords = points.clone();
10299
let start = Instant::now();
103100
let triangulation = Triangulation::new(coords).unwrap();
104-
black_box(triangulation.simplices.len());
101+
black_box(triangulation.num_simplices());
105102
total += start.elapsed();
106103
}
107104
total
@@ -130,7 +127,7 @@ fn add_point_bench(c: &mut Criterion) {
130127
for point in &to_insert {
131128
triangulation.add_point(point.clone(), None, None).unwrap();
132129
}
133-
black_box(triangulation.simplices.len());
130+
black_box(triangulation.num_simplices());
134131
total += start.elapsed();
135132
}
136133
total
@@ -191,7 +188,7 @@ fn locate_and_containing_benches(c: &mut Criterion) {
191188
group.measurement_time(Duration::from_secs(8));
192189

193190
let triangulation = Triangulation::new(generate_points(2, 2048, 0xFACE)).unwrap();
194-
let simplices: Vec<Simplex> = triangulation.simplices.iter().take(256).cloned().collect();
191+
let simplices: Vec<Simplex> = triangulation.simplices().take(256).cloned().collect();
195192
let queries: Vec<Vec<f64>> = simplices
196193
.iter()
197194
.map(|simplex| simplex_centroid(&triangulation, simplex))

‎src/lib.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! documented in [`tolerances`].
55
66
pub mod geometry;
7+
pub mod py;
78
pub mod tolerances;
89
pub mod triangulation;
910

@@ -12,7 +13,7 @@ use pyo3::prelude::*;
1213
use pyo3::types::{PyAny, PyModule, PyTuple};
1314

1415
use crate::geometry as geom;
15-
use crate::triangulation::{
16+
use crate::py::{
1617
numpy_linalg_error, parse_point, parse_points_sized, point_tuple, PyFacesIter,
1718
PySimplicesProxy, PyTriangulation, PyVertexToSimplicesIter, PyVertexToSimplicesProxy,
1819
PyVerticesIter, PyVerticesProxy,

0 commit comments

Comments
 (0)