From bdf91c448a20fe7a4e7e00d75b4b08de4dcbd853 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sun, 8 Feb 2026 20:32:06 -0800 Subject: [PATCH 1/3] Changed: Improves flip algorithm with topology index Improves flip algorithm by introducing a topology index to efficiently check for duplicate cells and non-manifold facets. This avoids redundant scans of the triangulation data structure, especially during repair operations, by pre-computing and storing facet and cell signatures. This change is internal. Refs: perf/optimize-flips --- benches/ci_performance_suite.rs | 100 ++++++- src/core/algorithms/flips.rs | 336 ++++++++++++++++++----- src/core/cell.rs | 5 +- src/core/triangulation_data_structure.rs | 48 ++-- src/core/vertex.rs | 5 +- src/geometry/point.rs | 19 +- tests/proptest_tds.rs | 16 ++ 7 files changed, 429 insertions(+), 100 deletions(-) diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index 11d54ff2..a479f100 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -25,10 +25,12 @@ //! - 3D-5D: Higher-dimensional triangulations as documented in README.md use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use delaunay::core::delaunay_triangulation::{ConstructionOptions, RetryPolicy}; use delaunay::geometry::util::generate_random_points_seeded; use delaunay::prelude::DelaunayTriangulation; use delaunay::vertex; use std::hint::black_box; +use std::num::NonZeroUsize; use tracing::error; /// Common sample sizes used across all CI performance benchmarks @@ -40,6 +42,19 @@ fn bench_logging_enabled() -> bool { .unwrap_or(false) } +fn bench_seed_search_enabled() -> bool { + std::env::var("DELAUNAY_BENCH_SEED_SEARCH") + .map(|value| value != "0") + .unwrap_or(false) +} + +fn bench_seed_search_limit() -> usize { + std::env::var("DELAUNAY_BENCH_SEED_SEARCH_LIMIT") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(2000) +} + /// Fixed seeds for deterministic triangulation generation across benchmark runs. /// Using seeded random number generation reduces variance in performance measurements /// and improves regression detection accuracy in CI environments. @@ -49,6 +64,10 @@ fn bench_logging_enabled() -> bool { macro_rules! benchmark_tds_new_dimension { ($dim:literal, $func_name:ident, $seed:literal) => { /// Benchmark triangulation creation for D-dimensional triangulations + #[expect( + clippy::too_many_lines, + reason = "Keep benchmark configuration, seed search, and error reporting together" + )] fn $func_name(c: &mut Criterion) { let counts = COUNTS; let mut group = c.benchmark_group(concat!("tds_new_", stringify!($dim), "d")); @@ -66,18 +85,85 @@ macro_rules! benchmark_tds_new_dimension { group.bench_with_input(BenchmarkId::new("tds_new", count), &count, |b, &count| { // Reduce variance: pre-generate deterministic inputs outside the measured loop, // then benchmark only triangulation construction. - let points = - generate_random_points_seeded::(count, (-100.0, 100.0), $seed) + // + // Note: Use per-count seeds so that each benchmark case has its own deterministic + // point set. This avoids a single pathological input (e.g. 3D/50) aborting the + // entire suite. + let bounds = (-100.0, 100.0); + let seed = ($seed as u64).wrapping_add(count as u64); + + // Opt-in helper for discovering stable seeds without paying Criterion warmup/ + // measurement cost per seed. + if bench_seed_search_enabled() { + let limit = bench_seed_search_limit(); + for offset in 0..limit { + let candidate_seed = seed.wrapping_add(offset as u64); + let points = generate_random_points_seeded::( + count, + bounds, + candidate_seed, + ) .expect(concat!( "generate_random_points_seeded failed for ", stringify!($dim), "D" )); + let vertices = points.iter().map(|p| vertex!(*p)).collect::>(); + + let options = + ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts: NonZeroUsize::new(6) + .expect("retry attempts must be non-zero"), + base_seed: Some(candidate_seed), + }); + + if DelaunayTriangulation::<_, (), (), $dim>::new_with_options( + &vertices, + options, + ) + .is_ok() + { + println!( + "seed_search_found dim={} count={} seed={}", + $dim, count, candidate_seed + ); + std::process::exit(0); + } + } + + println!( + "seed_search_failed dim={} count={} start_seed={} limit={}", + $dim, + count, + seed, + limit + ); + std::process::exit(1); + } + + let points = generate_random_points_seeded::(count, bounds, seed) + .expect(concat!( + "generate_random_points_seeded failed for ", + stringify!($dim), + "D" + )); let vertices = points.iter().map(|p| vertex!(*p)).collect::>(); let sample_points = points.iter().take(5).collect::>(); + // In benchmarks we compile in release mode, where the default retry policy is + // disabled. For deterministic CI benchmarks we opt into a small number of + // shuffled retries to avoid aborting the suite on rare non-convergent repair + // cases. + let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts: NonZeroUsize::new(6).expect("retry attempts must be non-zero"), + base_seed: Some(seed), + }); + b.iter(|| { - match DelaunayTriangulation::<_, (), (), $dim>::new(&vertices) { + match DelaunayTriangulation::<_, (), (), $dim>::new_with_options( + &vertices, + options, + ) { Ok(dt) => { black_box(dt); } @@ -87,8 +173,8 @@ macro_rules! benchmark_tds_new_dimension { error!( dim = $dim, count, - seed = $seed, - bounds = ?(-100.0, 100.0), + seed, + bounds = ?bounds, sample_points = ?sample_points, error = %error, "DelaunayTriangulation::new failed" @@ -99,8 +185,8 @@ macro_rules! benchmark_tds_new_dimension { $dim, $dim, count, - $seed, - (-100.0, 100.0) + seed, + bounds ); } } diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index cd4cdbad..fe9be981 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -38,6 +38,7 @@ use crate::core::facet::{AllFacetsIter, FacetHandle, facet_key_from_vertices}; use crate::core::traits::data_type::DataType; use crate::core::triangulation::TopologyGuarantee; use crate::core::triangulation_data_structure::{CellKey, Tds, VertexKey}; +use crate::core::util::stable_hash_u64_slice; use crate::core::vertex::Vertex; use crate::geometry::kernel::Kernel; use crate::geometry::point::Point; @@ -265,19 +266,25 @@ where } } - if flip_would_duplicate_cell_any(tds, &vertices, removed_cells) { + new_cell_vertices.push(vertices); + } + + let topology_index = build_flip_topology_index( + tds, + &new_cell_vertices, + removed_cells, + inserted_face_vertices, + ); + + for vertices in &new_cell_vertices { + if flip_would_duplicate_cell_any(tds, vertices, &topology_index) { return Err(FlipError::DuplicateCell); } - if flip_would_create_nonmanifold_facets_any( - tds, - &vertices, - removed_cells, - inserted_face_vertices, - ) { + if flip_would_create_nonmanifold_facets_any(vertices, &topology_index) { return Err(FlipError::NonManifoldFacet); } - let points = vertices_to_points(tds, &vertices)?; + let points = vertices_to_points(tds, vertices)?; let orientation = kernel .orientation(&points) .map_err(|e| FlipError::PredicateFailure { @@ -298,8 +305,6 @@ where return Err(FlipError::DegenerateCell); } } - - new_cell_vertices.push(vertices); } for vertices in new_cell_vertices { @@ -3710,99 +3715,241 @@ where Ok(points) } -fn flip_would_duplicate_cell_any( - tds: &Tds, +#[derive(Debug, Default)] +struct FlipTopologyIndex { + /// Map from candidate cell signature to the first existing cell that matches it. + duplicate_signature_to_cell: FastHashMap, + /// Map from candidate facet hash to topology metadata. + candidate_facet_info: FastHashMap, +} + +#[derive(Debug, Clone, Copy)] +struct CandidateFacetInfo { + internal: bool, + existing_count: u8, + last_cell: Option, +} + +fn sorted_vertex_key_values( vertices: &[VertexKey], - removed: &[CellKey], -) -> bool +) -> SmallBuffer { + let mut key_values: SmallBuffer = + vertices.iter().map(|key| key.data().as_ffi()).collect(); + key_values.sort_unstable(); + key_values +} + +fn cell_signature(vertices: &[VertexKey]) -> u64 { + let key_values = sorted_vertex_key_values(vertices); + stable_hash_u64_slice(&key_values) +} + +fn build_flip_topology_index( + tds: &Tds, + new_cell_vertices: &[SmallBuffer], + removed_cells: &[CellKey], + inserted_face_vertices: &[VertexKey], +) -> FlipTopologyIndex where T: CoordinateScalar, U: DataType, V: DataType, { - let mut target: SmallBuffer = - vertices.iter().copied().collect(); - target.sort_unstable(); + let inserted_values = sorted_vertex_key_values(inserted_face_vertices); + + let mut candidate_cell_signatures: SmallBuffer = + SmallBuffer::with_capacity(new_cell_vertices.len()); + + let mut candidate_facet_info: FastHashMap = FastHashMap::default(); + candidate_facet_info.reserve(new_cell_vertices.len() * (D + 1)); + + // Seed the facet map with the facets that will exist after the flip. + for vertices in new_cell_vertices { + let cell_values = sorted_vertex_key_values(vertices); + candidate_cell_signatures.push(stable_hash_u64_slice(&cell_values)); + + let mut facet_values: SmallBuffer = + SmallBuffer::with_capacity(cell_values.len().saturating_sub(1)); + for omit_idx in 0..cell_values.len() { + facet_values.clear(); + for (i, &val) in cell_values.iter().enumerate() { + if i != omit_idx { + facet_values.push(val); + } + } + + let facet_hash = stable_hash_u64_slice(&facet_values); + let internal = inserted_values + .iter() + .all(|v| facet_values.binary_search(v).is_ok()); + if let Some(info) = candidate_facet_info.get_mut(&facet_hash) { + info.internal |= internal; + } else { + candidate_facet_info.insert( + facet_hash, + CandidateFacetInfo { + internal, + existing_count: 0, + last_cell: None, + }, + ); + } + } + } + + let mut duplicate_signature_to_cell: FastHashMap = FastHashMap::default(); + duplicate_signature_to_cell.reserve(candidate_cell_signatures.len()); + + let mut facet_values: SmallBuffer = + SmallBuffer::with_capacity(D); + + // Scan existing cells once, updating only the candidate facet set. for (cell_key, cell) in tds.cells() { - if removed.contains(&cell_key) { + if removed_cells.contains(&cell_key) { continue; } - if cell.number_of_vertices() != vertices.len() { - continue; + + let mut cell_values: SmallBuffer = cell + .vertices() + .iter() + .map(|key| key.data().as_ffi()) + .collect(); + cell_values.sort_unstable(); + + let signature = stable_hash_u64_slice(&cell_values); + if candidate_cell_signatures.contains(&signature) { + duplicate_signature_to_cell + .entry(signature) + .or_insert(cell_key); } - let mut cell_vertices: SmallBuffer = - cell.vertices().iter().copied().collect(); - cell_vertices.sort_unstable(); - if cell_vertices == target { - if std::env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() { - tracing::debug!( - "k=2 flip would duplicate existing cell {cell_key:?}; target={target:?}; existing={cell_vertices:?}" - ); + + for omit_idx in 0..cell_values.len() { + facet_values.clear(); + for (i, &val) in cell_values.iter().enumerate() { + if i != omit_idx { + facet_values.push(val); + } } - if repair_trace_enabled() { - tracing::debug!( - "[repair] flip would duplicate existing cell {cell_key:?}; target={target:?}; existing={cell_vertices:?}" - ); + let facet_hash = stable_hash_u64_slice(&facet_values); + + let Some(info) = candidate_facet_info.get_mut(&facet_hash) else { + continue; + }; + + if info.existing_count < 2 { + info.existing_count += 1; } - return true; + info.last_cell = Some(cell_key); } } - false + + FlipTopologyIndex { + duplicate_signature_to_cell, + candidate_facet_info, + } } -fn flip_would_create_nonmanifold_facets_any( +fn flip_would_duplicate_cell_any( tds: &Tds, vertices: &[VertexKey], - removed: &[CellKey], - opposite_vertices: &[VertexKey], + topology: &FlipTopologyIndex, ) -> bool where T: CoordinateScalar, U: DataType, V: DataType, { - for omit_idx in 0..vertices.len() { - let mut facet_vertices: SmallBuffer = - SmallBuffer::with_capacity(D); - for (i, &vkey) in vertices.iter().enumerate() { + let signature = cell_signature(vertices); + let Some(&cell_key) = topology.duplicate_signature_to_cell.get(&signature) else { + return false; + }; + + if std::env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() || repair_trace_enabled() { + let mut target: SmallBuffer = + vertices.iter().copied().collect(); + target.sort_unstable(); + + let existing_sorted = tds.get_cell(cell_key).map(|cell| { + let mut v: SmallBuffer = + cell.vertices().iter().copied().collect(); + v.sort_unstable(); + v + }); + + if std::env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() { + tracing::debug!( + "k=2 flip would duplicate existing cell {cell_key:?}; target={target:?}; existing={existing_sorted:?}" + ); + } + if repair_trace_enabled() { + tracing::debug!( + "[repair] flip would duplicate existing cell {cell_key:?}; target={target:?}; existing={existing_sorted:?}" + ); + } + } + + true +} + +fn flip_would_create_nonmanifold_facets_any( + vertices: &[VertexKey], + topology: &FlipTopologyIndex, +) -> bool { + let mut sorted_vertices: SmallBuffer = + vertices.iter().copied().collect(); + sorted_vertices.sort_unstable(); + + let mut facet_values: SmallBuffer = + SmallBuffer::with_capacity(sorted_vertices.len().saturating_sub(1)); + let mut facet_vertices: SmallBuffer = + SmallBuffer::with_capacity(sorted_vertices.len().saturating_sub(1)); + + for omit_idx in 0..sorted_vertices.len() { + facet_values.clear(); + facet_vertices.clear(); + + for (i, &vkey) in sorted_vertices.iter().enumerate() { if i != omit_idx { facet_vertices.push(vkey); + facet_values.push(vkey.data().as_ffi()); } } - let mut shared_count = 0usize; - for (cell_key, cell) in tds.cells() { - if removed.contains(&cell_key) { - continue; - } - if facet_vertices.iter().all(|v| cell.contains_vertex(*v)) { - shared_count += 1; - if shared_count > 1 { - if repair_trace_enabled() { - tracing::debug!( - "[repair] flip would create non-manifold facet: facet={:?} shared_count={} last_cell={cell_key:?}", - facet_vertices, - shared_count, - ); - } - return true; + let facet_hash = stable_hash_u64_slice(&facet_values); + let Some(info) = topology.candidate_facet_info.get(&facet_hash) else { + // If this happens, the index construction and facet hashing logic are inconsistent. + // Conservatively allow (old code would have scanned). This is debug-asserted. + debug_assert!(false, "missing facet hash in flip topology index"); + continue; + }; + + if info.internal { + if info.existing_count > 0 { + if repair_trace_enabled() { + tracing::debug!( + "[repair] flip would create non-manifold internal facet: facet={facet_vertices:?} shared_count={} last_cell={:?}", + info.existing_count, + info.last_cell, + ); } + return true; } + continue; } - let internal_facet = opposite_vertices.iter().all(|v| facet_vertices.contains(v)); - if internal_facet && shared_count > 0 { + if info.existing_count > 1 { if repair_trace_enabled() { tracing::debug!( - "[repair] flip would create non-manifold internal facet: facet={:?} shared_count={}", - facet_vertices, - shared_count, + "[repair] flip would create non-manifold facet: facet={facet_vertices:?} shared_count={} last_cell={:?}", + info.existing_count, + info.last_cell, ); } return true; } } + false } @@ -4682,6 +4829,71 @@ mod tests { assert!(tds.is_valid().is_ok()); } + #[test] + fn test_flip_k2_rejects_duplicate_cell() { + init_tracing(); + let mut tds: Tds = Tds::empty(); + let a = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let b = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let c = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let d = tds.insert_vertex_with_mapping(vertex!([1.0, 0.2])).unwrap(); + + let c1 = tds + .insert_cell_with_mapping(Cell::new(vec![a, b, c], None).unwrap()) + .unwrap(); + let _c2 = tds + .insert_cell_with_mapping(Cell::new(vec![a, b, d], None).unwrap()) + .unwrap(); + + // Pre-existing cell that the flip would recreate (B,C,D) + let _existing = tds + .insert_cell_with_mapping(Cell::new(vec![b, c, d], None).unwrap()) + .unwrap(); + + repair_neighbor_pointers(&mut tds).unwrap(); + + let facet = FacetHandle::new(c1, 2); // facet opposite vertex index 2 (edge AB) + let kernel = FastKernel::::new(); + let context = build_k2_flip_context(&tds, facet).unwrap(); + let result = apply_bistellar_flip_k2(&mut tds, &kernel, &context); + + assert!(matches!(result, Err(FlipError::DuplicateCell))); + assert!(tds.is_valid().is_ok()); + } + + #[test] + fn test_flip_k2_rejects_nonmanifold_internal_facet() { + init_tracing(); + let mut tds: Tds = Tds::empty(); + let v_a = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v_b = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v_c = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v_d = tds.insert_vertex_with_mapping(vertex!([1.0, 0.2])).unwrap(); + let v_e = tds.insert_vertex_with_mapping(vertex!([2.0, 2.0])).unwrap(); + + let c1 = tds + .insert_cell_with_mapping(Cell::new(vec![v_a, v_b, v_c], None).unwrap()) + .unwrap(); + let _c2 = tds + .insert_cell_with_mapping(Cell::new(vec![v_a, v_b, v_d], None).unwrap()) + .unwrap(); + + // Existing cell containing the would-be inserted diagonal (C,D). + let _cd_external = tds + .insert_cell_with_mapping(Cell::new(vec![v_c, v_d, v_e], None).unwrap()) + .unwrap(); + + repair_neighbor_pointers(&mut tds).unwrap(); + + let facet = FacetHandle::new(c1, 2); // facet opposite vertex index 2 (edge AB) + let kernel = FastKernel::::new(); + let context = build_k2_flip_context(&tds, facet).unwrap(); + let result = apply_bistellar_flip_k2(&mut tds, &kernel, &context); + + assert!(matches!(result, Err(FlipError::NonManifoldFacet))); + assert!(tds.is_valid().is_ok()); + } + #[test] fn test_flip_k2_3d_two_to_three() { init_tracing(); diff --git a/src/core/cell.rs b/src/core/cell.rs index 03b46818..ee8d0f40 100644 --- a/src/core/cell.rs +++ b/src/core/cell.rs @@ -6,8 +6,9 @@ //! //! # Key Features //! -//! - **Generic Coordinate Support**: Works with any floating-point type (`f32`, `f64`, etc.) -//! that implements the `CoordinateScalar` trait +//! - **Geometry when you need it**: the `Cell` type does not require `T: CoordinateScalar` +//! at the type level, but geometric operations, validation, and serialization are only +//! available when `T: CoordinateScalar` (e.g. `f32`, `f64`) //! - **Unique Identification**: Each cell has a UUID for consistent identification //! - **Vertices Management**: Stores vertices that form the simplex //! - **Neighbor Tracking**: Maintains references to neighboring cells diff --git a/src/core/triangulation_data_structure.rs b/src/core/triangulation_data_structure.rs index f6a02b2d..c8d05f80 100644 --- a/src/core/triangulation_data_structure.rs +++ b/src/core/triangulation_data_structure.rs @@ -1,23 +1,29 @@ //! Data and operations on d-dimensional triangulation data structures. //! -//! This module provides the `Tds` (Triangulation Data Structure) struct which represents -//! a D-dimensional finite simplicial complex with geometric vertices, cells, and their -//! topological relationships. The implementation closely follows the design principles -//! of [CGAL Triangulation](https://doc.cgal.org/latest/Triangulation/index.html). +//! This module provides the `Tds` (Triangulation Data Structure): a key-based, +//! CGAL-inspired representation of the **combinatorial** topology of a D-dimensional +//! finite simplicial complex (vertices, cells, and adjacency). The implementation +//! closely follows the design principles of +//! [CGAL Triangulation](https://doc.cgal.org/latest/Triangulation/index.html). +//! +//! The crate follows a layered architecture: `Tds` is topology-focused, while geometric +//! predicates and Delaunay-specific operations live in higher layers (`Triangulation` / +//! `DelaunayTriangulation`) and `core::algorithms`. //! //! # Key Features //! -//! - **Generic Coordinate Support**: Works with any floating-point type (`f32`, `f64`, etc.) -//! that implements the `CoordinateScalar` trait +//! - **CGAL-style layering**: topology in `Tds`, geometry/predicates in higher layers +//! - **Relaxed coordinate bounds**: most `Tds` methods require only `U: DataType` and +//! `V: DataType`; methods that validate or serialize geometric values are gated behind +//! `T: CoordinateScalar` //! - **Arbitrary Dimensions**: Supports triangulations in any dimension D ≥ 1 -//! - **Delaunay Triangulation**: Implements Bowyer-Watson algorithm for Delaunay triangulation //! - **Hierarchical Cell Structure**: Stores maximal D-dimensional cells and infers lower-dimensional //! simplices (vertices, edges, facets) from the maximal cells //! - **Neighbor Relationships**: Maintains adjacency information between cells for efficient -//! traversal and geometric queries -//! - **Validation Support**: Comprehensive validation of triangulation properties including -//! neighbor consistency and geometric validity -//! - **Serialization Support**: Full serde support for persistence and data exchange +//! topological traversal +//! - **Validation Support**: Structural invariant validation (Level 2) plus cumulative element +//! validation (Levels 1–2; requires `T: CoordinateScalar`) +//! - **Serialization Support**: Serde support for persistence (available when `T: CoordinateScalar`) //! - **Optimized Storage**: Internal key-based storage with UUIDs for external identity //! //! # Geometric Structure @@ -698,8 +704,9 @@ new_key_type! { /// /// - `vertices`: A storage map that stores vertices with stable keys for efficient access. /// Each [`Vertex`] has a point of type T, vertex data of type U, and a constant D representing the dimension. -/// - `cells`: The `cells` property is a storage map that stores [`Cell`] objects with stable keys. -/// Each [`Cell`] has one or more [`Vertex`] objects with cell data of type V. +/// - `cells`: A storage map that stores maximal [`Cell`] objects with stable keys. +/// Each [`Cell`] stores [`VertexKey`]s (keys into `vertices`) and optional neighbor [`CellKey`]s, +/// plus cell data of type `V`. /// Note the dimensionality of the cell may differ from D, though the [`Tds`] /// only stores cells of maximal dimensionality D and infers other lower /// dimensional cells (cf. Facets) from the maximal cells and their vertices. @@ -715,14 +722,19 @@ new_key_type! { /// /// A similar pattern holds for higher dimensions. /// -/// In general, vertices are embedded into D-dimensional Euclidean space, -/// and so the [`Tds`] is a finite simplicial complex. +/// In typical usage, vertices carry coordinates in D-dimensional Euclidean space. +/// However, the core `Tds` API is designed to be **combinatorial**: most methods do +/// not require `T: CoordinateScalar` and operate purely on keys and adjacency. /// /// # Usage /// -/// The `Tds` struct is the primary entry point for creating and manipulating -/// Delaunay triangulations. It is initialized with a set of vertices and -/// automatically computes the triangulation. +/// `Tds` is the low-level topology container used by +/// [`Triangulation`](crate::core::triangulation::Triangulation) and +/// [`DelaunayTriangulation`](crate::core::delaunay_triangulation::DelaunayTriangulation). +/// +/// Most users should construct triangulations via `DelaunayTriangulation` and access the +/// underlying `Tds` via `dt.tds()`. Use [`Tds::empty`](Self::empty) for low-level or test +/// scenarios where you want to manipulate the topology directly. /// /// ```rust /// use delaunay::prelude::triangulation::*; diff --git a/src/core/vertex.rs b/src/core/vertex.rs index be753fd5..5b028027 100644 --- a/src/core/vertex.rs +++ b/src/core/vertex.rs @@ -6,8 +6,9 @@ //! //! # Key Features //! -//! - **Generic Coordinate Support**: Works with any floating-point type (`f32`, `f64`, etc.) -//! that implements the `CoordinateScalar` trait +//! - **Geometry when you need it**: the `Vertex` type does not require `T: CoordinateScalar` +//! at the type level, but geometric operations, validation, and serialization are only +//! available when `T: CoordinateScalar` (e.g. `f32`, `f64`) //! - **Unique Identification**: Each vertex has a UUID for consistent identification //! - **Optional Data Storage**: Supports attaching user data of any type `U` that implements [`DataType`], or use `()` for no data //! - **Incident Cell Tracking**: Maintains references to containing cells diff --git a/src/geometry/point.rs b/src/geometry/point.rs index d1170604..d3dbb49e 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -41,18 +41,19 @@ use std::marker::PhantomData; /// /// # Generic Type Support /// -/// The Point struct supports any floating-point type `T` that implements the -/// `CoordinateScalar` trait, including `f32`, `f64`, and other floating-point -/// types. This generalization allows for flexibility in precision requirements -/// and memory usage across different applications. +/// `Point` stores a fixed-size coordinate array `[T; D]`. +/// +/// The type itself does **not** require `T: CoordinateScalar`, which allows the +/// surrounding combinatorial data structures (e.g. `Tds`) to avoid geometry +/// bounds at the type level. +/// +/// Most geometric APIs (construction via [`Coordinate`], validation, hashing/ordering, +/// and serialization) are only available when `T: CoordinateScalar`. /// /// # Properties /// -/// * `coords`: `coords` is a private property of the [Point]. It is an array of -/// type `T` with a length of `D`. The type `T` is a generic type parameter -/// constrained to implement `CoordinateScalar`, ensuring it has all necessary -/// traits for coordinate operations. The length `D` is a constant unsigned -/// integer known at compile time. +/// * `coords`: A private `[T; D]` coordinate array (length `D` is known at compile time). +/// The field is private to keep points immutable once created. /// /// Points are intended to be immutable once created, so the `coords` field is /// private to prevent modification after instantiation. diff --git a/tests/proptest_tds.rs b/tests/proptest_tds.rs index 110bfb58..2095f8e1 100644 --- a/tests/proptest_tds.rs +++ b/tests/proptest_tds.rs @@ -21,11 +21,27 @@ //! //! All tests use `dt.tds().is_valid()` (Level 2 structural validation). +use delaunay::core::triangulation_data_structure::Tds; use delaunay::core::util::jaccard::jaccard_index; use delaunay::prelude::query::*; use proptest::prelude::*; use std::collections::HashSet; +#[test] +fn tds_empty_does_not_require_coordinate_scalar() { + #[derive(Clone, Copy, Debug)] + struct NotAScalar; + + let tds: Tds = Tds::empty(); + + assert_eq!(tds.number_of_vertices(), 0); + assert_eq!(tds.number_of_cells(), 0); + assert_eq!(tds.dim(), -1); + assert!(tds.vertex_keys().next().is_none()); + assert!(tds.cell_keys().next().is_none()); + assert!(tds.is_valid().is_ok()); +} + // ============================================================================= // TEST CONFIGURATION // ============================================================================= From 8890222aa2b9e07d3ffea23c483e4a2f084967f3 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sun, 8 Feb 2026 22:34:50 -0800 Subject: [PATCH 2/3] Changed: Use `SmallBuffer` for flip topology index maps Refactors the flip topology index to use `SmallBuffer` instead of `FastHashMap` for storing candidate cell signatures and facet info. This change optimizes memory usage and improves performance by leveraging the stack allocation and reduced overhead of `SmallBuffer` for small datasets, which are common in this context. Removes the 'internal' flag from the `CandidateFacetInfo` struct as boundary facets are no longer tracked. Refs: perf/optimize-flips --- benches/ci_performance_suite.rs | 10 +++ src/core/algorithms/flips.rs | 151 ++++++++++++++++++++------------ 2 files changed, 103 insertions(+), 58 deletions(-) diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index a479f100..843d933b 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -94,6 +94,16 @@ macro_rules! benchmark_tds_new_dimension { // Opt-in helper for discovering stable seeds without paying Criterion warmup/ // measurement cost per seed. + // + // NOTE: This helper is intentionally per (dim, count) benchmark case. + // It `exit(0)`s on the first successful seed (and `exit(1)`s on failure), + // so it is meant to be run with a Criterion filter that selects a single + // case, for example: + // + // cargo bench --bench ci_performance_suite -- 'tds_new_3d/tds_new/50' + // + // Because the base seed is derived from `count`, a seed that works for one + // count may still fail for a different count. if bench_seed_search_enabled() { let limit = bench_seed_search_limit(); for offset in 0..limit { diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index fe9be981..a8e28b2c 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -3717,15 +3717,25 @@ where #[derive(Debug, Default)] struct FlipTopologyIndex { - /// Map from candidate cell signature to the first existing cell that matches it. - duplicate_signature_to_cell: FastHashMap, - /// Map from candidate facet hash to topology metadata. - candidate_facet_info: FastHashMap, + /// Candidate cell signature → the first existing cell that matches it. + /// + /// The number of candidate cells per flip is small (≤ D+1), so a flat buffer is + /// faster than a `HashMap` in this hot path. + duplicate_signature_to_cell: SmallBuffer<(u64, CellKey), MAX_PRACTICAL_DIMENSION_SIZE>, + + /// Candidate *internal* facet hash → topology metadata, sorted by hash for binary search. + /// + /// We only track internal facets (facets that contain the inserted face). Boundary facets + /// lie on the cavity boundary and cannot become non-manifold when the surrounding topology is + /// valid. + candidate_facet_info: SmallBuffer< + (u64, CandidateFacetInfo), + { MAX_PRACTICAL_DIMENSION_SIZE * MAX_PRACTICAL_DIMENSION_SIZE }, + >, } #[derive(Debug, Clone, Copy)] struct CandidateFacetInfo { - internal: bool, existing_count: u8, last_cell: Option, } @@ -3760,8 +3770,10 @@ where let mut candidate_cell_signatures: SmallBuffer = SmallBuffer::with_capacity(new_cell_vertices.len()); - let mut candidate_facet_info: FastHashMap = FastHashMap::default(); - candidate_facet_info.reserve(new_cell_vertices.len() * (D + 1)); + let mut candidate_facet_info: SmallBuffer< + (u64, CandidateFacetInfo), + { MAX_PRACTICAL_DIMENSION_SIZE * MAX_PRACTICAL_DIMENSION_SIZE }, + > = SmallBuffer::new(); // Seed the facet map with the facets that will exist after the flip. for vertices in new_cell_vertices { @@ -3783,45 +3795,71 @@ where .iter() .all(|v| facet_values.binary_search(v).is_ok()); - if let Some(info) = candidate_facet_info.get_mut(&facet_hash) { - info.internal |= internal; - } else { - candidate_facet_info.insert( - facet_hash, - CandidateFacetInfo { - internal, - existing_count: 0, - last_cell: None, - }, - ); + // Only internal facets can become non-manifold: boundary facets are part of the cavity + // boundary and already exist in the surrounding triangulation. + if !internal { + continue; + } + + if candidate_facet_info + .iter() + .any(|(hash, _info)| *hash == facet_hash) + { + continue; } + + candidate_facet_info.push(( + facet_hash, + CandidateFacetInfo { + existing_count: 0, + last_cell: None, + }, + )); } } - let mut duplicate_signature_to_cell: FastHashMap = FastHashMap::default(); - duplicate_signature_to_cell.reserve(candidate_cell_signatures.len()); + candidate_facet_info.sort_unstable_by_key(|(hash, _info)| *hash); + + let mut duplicate_signature_to_cell: SmallBuffer<(u64, CellKey), MAX_PRACTICAL_DIMENSION_SIZE> = + SmallBuffer::new(); let mut facet_values: SmallBuffer = SmallBuffer::with_capacity(D); + let mut cell_values: SmallBuffer = + SmallBuffer::with_capacity(D + 1); - // Scan existing cells once, updating only the candidate facet set. + // Scan existing cells once. + // + // Both duplicate cells and existing internal facets must contain all inserted-face vertices. for (cell_key, cell) in tds.cells() { if removed_cells.contains(&cell_key) { continue; } - - let mut cell_values: SmallBuffer = cell - .vertices() + if !inserted_face_vertices .iter() - .map(|key| key.data().as_ffi()) - .collect(); + .all(|v| cell.contains_vertex(*v)) + { + continue; + } + + cell_values.clear(); + for key in cell.vertices() { + cell_values.push(key.data().as_ffi()); + } cell_values.sort_unstable(); let signature = stable_hash_u64_slice(&cell_values); - if candidate_cell_signatures.contains(&signature) { - duplicate_signature_to_cell - .entry(signature) - .or_insert(cell_key); + if candidate_cell_signatures.contains(&signature) + && !duplicate_signature_to_cell + .iter() + .any(|(s, _cell_key)| *s == signature) + { + duplicate_signature_to_cell.push((signature, cell_key)); + } + + // If there are no internal facets to check, skip facet hashing. + if candidate_facet_info.is_empty() { + continue; } for omit_idx in 0..cell_values.len() { @@ -3833,9 +3871,12 @@ where } let facet_hash = stable_hash_u64_slice(&facet_values); - let Some(info) = candidate_facet_info.get_mut(&facet_hash) else { + let Ok(idx) = + candidate_facet_info.binary_search_by_key(&facet_hash, |(hash, _info)| *hash) + else { continue; }; + let info = &mut candidate_facet_info[idx].1; if info.existing_count < 2 { info.existing_count += 1; @@ -3861,7 +3902,11 @@ where V: DataType, { let signature = cell_signature(vertices); - let Some(&cell_key) = topology.duplicate_signature_to_cell.get(&signature) else { + let Some(cell_key) = topology + .duplicate_signature_to_cell + .iter() + .find_map(|(s, ck)| (*s == signature).then_some(*ck)) + else { return false; }; @@ -3896,52 +3941,42 @@ fn flip_would_create_nonmanifold_facets_any( vertices: &[VertexKey], topology: &FlipTopologyIndex, ) -> bool { + let sorted_values = sorted_vertex_key_values(vertices); + let mut sorted_vertices: SmallBuffer = vertices.iter().copied().collect(); - sorted_vertices.sort_unstable(); + sorted_vertices.sort_unstable_by_key(|v| v.data().as_ffi()); let mut facet_values: SmallBuffer = - SmallBuffer::with_capacity(sorted_vertices.len().saturating_sub(1)); + SmallBuffer::with_capacity(sorted_values.len().saturating_sub(1)); let mut facet_vertices: SmallBuffer = SmallBuffer::with_capacity(sorted_vertices.len().saturating_sub(1)); - for omit_idx in 0..sorted_vertices.len() { + for omit_idx in 0..sorted_values.len() { facet_values.clear(); facet_vertices.clear(); - for (i, &vkey) in sorted_vertices.iter().enumerate() { + for (i, &value) in sorted_values.iter().enumerate() { if i != omit_idx { - facet_vertices.push(vkey); - facet_values.push(vkey.data().as_ffi()); + facet_values.push(value); + facet_vertices.push(sorted_vertices[i]); } } let facet_hash = stable_hash_u64_slice(&facet_values); - let Some(info) = topology.candidate_facet_info.get(&facet_hash) else { - // If this happens, the index construction and facet hashing logic are inconsistent. - // Conservatively allow (old code would have scanned). This is debug-asserted. - debug_assert!(false, "missing facet hash in flip topology index"); + let Ok(idx) = topology + .candidate_facet_info + .binary_search_by_key(&facet_hash, |(hash, _info)| *hash) + else { + // Boundary facet: not tracked in the index. continue; }; + let info = &topology.candidate_facet_info[idx].1; - if info.internal { - if info.existing_count > 0 { - if repair_trace_enabled() { - tracing::debug!( - "[repair] flip would create non-manifold internal facet: facet={facet_vertices:?} shared_count={} last_cell={:?}", - info.existing_count, - info.last_cell, - ); - } - return true; - } - continue; - } - - if info.existing_count > 1 { + if info.existing_count > 0 { if repair_trace_enabled() { tracing::debug!( - "[repair] flip would create non-manifold facet: facet={facet_vertices:?} shared_count={} last_cell={:?}", + "[repair] flip would create non-manifold internal facet: facet={facet_vertices:?} shared_count={} last_cell={:?}", info.existing_count, info.last_cell, ); From d381a476ae7f5a7a453bd01074196361a559e13d Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 9 Feb 2026 08:40:00 -0800 Subject: [PATCH 3/3] Changed: Optimizes flip algorithm for improved performance Refactors the flip algorithm to use more efficient data structures (SmallBuffer instead of FastHashMap/Vec) and hashing techniques (hash-only dedup) to improve performance. Also refactors benchmark seed search to avoid `std::process::exit`. These changes significantly reduce the time spent in the flip algorithm, leading to faster triangulation construction. (internal) Refs: perf/optimize-flips --- Cargo.lock | 1 + Cargo.toml | 6 +- benches/ci_performance_suite.rs | 144 +++++++++++++++++------------- src/core/algorithms/flips.rs | 25 ++++-- src/geometry/matrix.rs | 4 +- src/geometry/predicates.rs | 6 +- src/geometry/robust_predicates.rs | 6 +- tests/circumsphere_debug_tools.rs | 2 +- 8 files changed, 113 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40904182..32273e88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1161,6 +1161,7 @@ checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", + "rand 0.9.2", "serde_core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 8cc62b04..a26d3d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,16 +22,14 @@ derive_builder = "0.20.2" la-stack = "0.1.3" tracing = "0.1.44" rustc-hash = "2.1.1" # Fast non-cryptographic hashing for performance -smallvec = { version = "1.15.1", features = [ - "serde", -] } # Stack allocation for small collections +smallvec = { version = "1.15.1", features = [ "serde" ] } # Stack allocation for small collections num-traits = "0.2.19" ordered-float = { version = "5.1.0", features = [ "serde" ] } rand = "0.10.0" serde = { version = "1.0.228", features = [ "derive" ] } slotmap = { version = "1.1.1", features = [ "serde" ] } thiserror = "2.0.18" -uuid = { version = "1.20.0", features = [ "v4", "serde" ] } +uuid = { version = "1.20.0", features = [ "v4", "serde", "fast-rng" ] } [dev-dependencies] approx = "0.5.1" diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index 843d933b..5f8f6445 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -25,9 +25,8 @@ //! - 3D-5D: Higher-dimensional triangulations as documented in README.md use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use delaunay::core::delaunay_triangulation::{ConstructionOptions, RetryPolicy}; use delaunay::geometry::util::generate_random_points_seeded; -use delaunay::prelude::DelaunayTriangulation; +use delaunay::prelude::{ConstructionOptions, DelaunayTriangulation, RetryPolicy}; use delaunay::vertex; use std::hint::black_box; use std::num::NonZeroUsize; @@ -70,6 +69,88 @@ macro_rules! benchmark_tds_new_dimension { )] fn $func_name(c: &mut Criterion) { let counts = COUNTS; + + // Opt-in helper for discovering stable seeds without paying Criterion warmup/ + // measurement cost per seed. + // + // NOTE: This helper is intentionally per (dim, count) benchmark case. + // It returns early on the first successful seed (and panics on failure), + // so it is meant to be run with a Criterion filter that selects a single + // case, for example: + // + // cargo bench --bench ci_performance_suite -- 'tds_new_3d/tds_new/50' + // + // Because the base seed is derived from `count`, a seed that works for one + // count may still fail for a different count. + // + // We avoid `std::process::exit` here so that destructors run and Criterion + // can clean up state on both success and failure. + if bench_seed_search_enabled() { + let bounds = (-100.0, 100.0); + let filters: Vec = std::env::args() + .skip(1) + .filter(|arg| !arg.starts_with('-')) + .collect(); + + for &count in counts { + let bench_id = + format!("tds_new_{}d/tds_new/{}", stringify!($dim), count); + + if !filters.is_empty() && !filters.iter().any(|filter| bench_id.contains(filter)) { + continue; + } + + let seed = ($seed as u64).wrapping_add(count as u64); + let limit = bench_seed_search_limit(); + + for offset in 0..limit { + let candidate_seed = seed.wrapping_add(offset as u64); + let points = generate_random_points_seeded::( + count, + bounds, + candidate_seed, + ) + .expect(concat!( + "generate_random_points_seeded failed for ", + stringify!($dim), + "D" + )); + let vertices = points.iter().map(|p| vertex!(*p)).collect::>(); + + let options = + ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts: NonZeroUsize::new(6) + .expect("retry attempts must be non-zero"), + base_seed: Some(candidate_seed), + }); + + if DelaunayTriangulation::<_, (), (), $dim>::new_with_options( + &vertices, + options, + ) + .is_ok() + { + println!( + "seed_search_found dim={} count={} seed={}", + $dim, count, candidate_seed + ); + return; + } + } + + panic!( + "seed_search_failed dim={} count={} start_seed={} limit={}", + $dim, + count, + seed, + limit + ); + } + + // No filter matched this benchmark function; do nothing. + return; + } + let mut group = c.benchmark_group(concat!("tds_new_", stringify!($dim), "d")); // Set smaller sample sizes for higher dimensions to keep CI times reasonable @@ -92,65 +173,6 @@ macro_rules! benchmark_tds_new_dimension { let bounds = (-100.0, 100.0); let seed = ($seed as u64).wrapping_add(count as u64); - // Opt-in helper for discovering stable seeds without paying Criterion warmup/ - // measurement cost per seed. - // - // NOTE: This helper is intentionally per (dim, count) benchmark case. - // It `exit(0)`s on the first successful seed (and `exit(1)`s on failure), - // so it is meant to be run with a Criterion filter that selects a single - // case, for example: - // - // cargo bench --bench ci_performance_suite -- 'tds_new_3d/tds_new/50' - // - // Because the base seed is derived from `count`, a seed that works for one - // count may still fail for a different count. - if bench_seed_search_enabled() { - let limit = bench_seed_search_limit(); - for offset in 0..limit { - let candidate_seed = seed.wrapping_add(offset as u64); - let points = generate_random_points_seeded::( - count, - bounds, - candidate_seed, - ) - .expect(concat!( - "generate_random_points_seeded failed for ", - stringify!($dim), - "D" - )); - let vertices = points.iter().map(|p| vertex!(*p)).collect::>(); - - let options = - ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { - attempts: NonZeroUsize::new(6) - .expect("retry attempts must be non-zero"), - base_seed: Some(candidate_seed), - }); - - if DelaunayTriangulation::<_, (), (), $dim>::new_with_options( - &vertices, - options, - ) - .is_ok() - { - println!( - "seed_search_found dim={} count={} seed={}", - $dim, count, candidate_seed - ); - std::process::exit(0); - } - } - - println!( - "seed_search_failed dim={} count={} start_seed={} limit={}", - $dim, - count, - seed, - limit - ); - std::process::exit(1); - } - let points = generate_random_points_seeded::(count, bounds, seed) .expect(concat!( "generate_random_points_seeded failed for ", diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index a8e28b2c..ea5d550b 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -1700,8 +1700,10 @@ where return Err(FlipError::InvalidRidgeMultiplicity { found: cells.len() }); } - let mut opposite_counts: FastHashMap = FastHashMap::default(); - let mut extras_per_cell: Vec<[VertexKey; 2]> = Vec::with_capacity(3); + // k=3 flip contexts are tiny (exactly 3 cells, with 2 "extra" vertices per cell). + // Use flat buffers + linear counting to avoid HashMap/Vec overhead in this hot path. + let mut opposite_counts: SmallBuffer<(VertexKey, u8), 3> = SmallBuffer::new(); + let mut extras_per_cell: SmallBuffer<[VertexKey; 2], 3> = SmallBuffer::new(); for &ck in &cells { let cell = tds @@ -1712,22 +1714,28 @@ where return Err(FlipError::InvalidRidgeAdjacency { cell_key: ck }); } - for &v in &extras { - *opposite_counts.entry(v).or_insert(0) += 1; - } let extras_pair: [VertexKey; 2] = extras .as_slice() .try_into() .map_err(|_| FlipError::InvalidRidgeAdjacency { cell_key: ck })?; + + for &v in &extras_pair { + if let Some((_key, count)) = opposite_counts.iter_mut().find(|(key, _)| *key == v) { + *count += 1; + } else { + opposite_counts.push((v, 1)); + } + } + extras_per_cell.push(extras_pair); } - if opposite_counts.len() != 3 || !opposite_counts.values().all(|&count| count == 2) { + if opposite_counts.len() != 3 || !opposite_counts.iter().all(|(_v, count)| *count == 2) { return Err(FlipError::InvalidRidgeAdjacency { cell_key }); } let mut opposite_vertices: SmallBuffer = - opposite_counts.keys().copied().collect(); + opposite_counts.iter().map(|(v, _count)| *v).collect(); opposite_vertices.sort_unstable(); let opposite_vertices: [VertexKey; 3] = opposite_vertices .as_slice() @@ -3801,6 +3809,8 @@ where continue; } + // Intentional hash-only dedup (no vertex-level tie-break): a 64-bit collision is + // astronomically unlikely, and avoiding extra comparisons keeps this hot path fast. if candidate_facet_info .iter() .any(|(hash, _info)| *hash == facet_hash) @@ -3871,6 +3881,7 @@ where } let facet_hash = stable_hash_u64_slice(&facet_values); + // Hash-only lookup (see comment above); collision risk is astronomically low. let Ok(idx) = candidate_facet_info.binary_search_by_key(&facet_hash, |(hash, _info)| *hash) else { diff --git a/src/geometry/matrix.rs b/src/geometry/matrix.rs index c9c68642..c65f8736 100644 --- a/src/geometry/matrix.rs +++ b/src/geometry/matrix.rs @@ -196,11 +196,11 @@ pub(crate) fn matrix_set(m: &mut Matrix, r: usize, c: usize, /// use delaunay::geometry::matrix::{determinant, Matrix}; /// /// let m = Matrix::<2>::zero(); -/// assert_eq!(determinant(m), 0.0); +/// assert_eq!(determinant(&m), 0.0); /// ``` #[inline] #[must_use] -pub fn determinant(m: Matrix) -> f64 { +pub fn determinant(m: &Matrix) -> f64 { match m.det(0.0) { Ok(det) => det, Err(LaError::Singular { .. }) => 0.0, diff --git a/src/geometry/predicates.rs b/src/geometry/predicates.rs index c08def72..18605ca2 100644 --- a/src/geometry/predicates.rs +++ b/src/geometry/predicates.rs @@ -163,7 +163,7 @@ where let tolerance_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol); // Calculate determinant (singular => 0; non-finite => NaN). - let det = determinant(matrix); + let det = determinant(&matrix); if det > tolerance_f64 { Ok(Orientation::POSITIVE) @@ -423,7 +423,7 @@ where let base_tol = safe_scalar_to_f64(T::default_tolerance())?; let tolerance_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol); - let det = determinant(matrix); + let det = determinant(&matrix); let orientation = simplex_orientation(simplex_points)?; match orientation { @@ -626,7 +626,7 @@ where let tolerance_f64: f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol); // Calculate determinant (singular => 0; non-finite => NaN). - let det = determinant(matrix); + let det = determinant(&matrix); // The sign interpretation depends on both orientation and dimension parity // For the lifted matrix formulation, even and odd dimensions have opposite sign conventions diff --git a/src/geometry/robust_predicates.rs b/src/geometry/robust_predicates.rs index 1ef69ffb..b97d48e5 100644 --- a/src/geometry/robust_predicates.rs +++ b/src/geometry/robust_predicates.rs @@ -282,7 +282,7 @@ where fill_insphere_predicate_matrix(&mut matrix, simplex_points, test_point)?; let tol_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol); - let det = determinant(matrix); + let det = determinant(&matrix); Ok::<(f64, f64), CoordinateConversionError>((det, tol_f64)) })?; @@ -342,7 +342,7 @@ where } // Determinant with scale correction. - let det = determinant(matrix) * scale_factor; + let det = determinant(&matrix) * scale_factor; Ok::<(f64, f64), CoordinateConversionError>((det, tolerance_raw)) })?; @@ -445,7 +445,7 @@ where let tolerance_f64: f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol); // Calculate determinant (singular => 0; non-finite => NaN). - let det = determinant(matrix); + let det = determinant(&matrix); if det > tolerance_f64 { Ok(Orientation::POSITIVE) diff --git a/tests/circumsphere_debug_tools.rs b/tests/circumsphere_debug_tools.rs index 467eba5f..ecc97965 100644 --- a/tests/circumsphere_debug_tools.rs +++ b/tests/circumsphere_debug_tools.rs @@ -963,7 +963,7 @@ fn build_and_analyze_matrix(simplex_vertices: &[Vertex]) -> (f64, b ); } - let det = determinant(matrix); + let det = determinant(&matrix); println!(); println!("Determinant: {det:.6}");