From 5c4c9838760536c84f6c3c6b81341a7420417b68 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 3 Jul 2026 23:49:11 -0700 Subject: [PATCH 1/3] refactor(api)!: replace raw TDS access with owner topology queries - Expose owner-bound topology, identity, ridge, facet, locate, and validation queries on Triangulation and DelaunayTriangulation so callers no longer need raw TDS access. - Route Pachner moves through the owner transaction API with rollback, orientation repair, and post-move validation while keeping primitive flips as internal building blocks. - Publish Level 3 topology helpers for PL-manifold checks, including ridge-link and vertex-link validation, and align docs, examples, doctests, and preludes around the new API surface. - Add MCMC-backed Pachner stress and repair transaction-pressure benchmarks with diagnostics for failed long-run move chains. - Extend Semgrep rules to prevent regressions into public TDS/storage access and validation naming drift. BREAKING CHANGE: Public callers should use owner-level query and validation methods instead of `tds()` or `as_triangulation().tds()` to inspect topology storage. Closes #253 --- Cargo.lock | 10 + Cargo.toml | 1 + README.md | 2 +- benches/README.md | 44 +- benches/allocation_hot_paths.rs | 150 +- benches/ci_performance_suite.rs | 8 +- benches/common/flip_workflows.rs | 559 ++-- benches/delaunay_repair.rs | 142 +- benches/locate.rs | 16 +- benches/pachner_stress.rs | 933 +++++- benches/pl_manifold_repair.rs | 4 +- benches/profiling_suite.rs | 42 +- benches/tds_clone.rs | 29 +- docs/api_design.md | 39 +- docs/architecture/prelude_reference.md | 8 +- docs/dev/testing.md | 18 + docs/diagnostics.md | 26 +- docs/invariants.md | 12 + docs/topology.md | 36 +- docs/validation.md | 39 +- docs/workflows.md | 35 +- examples/delaunayize_repair.rs | 4 +- examples/diagnostics.rs | 38 +- examples/topology_editing.rs | 47 +- semgrep.yaml | 189 ++ src/bench_fixtures.rs | 8 +- src/core/adjacency.rs | 9 +- src/core/algorithms/flips.rs | 988 +++--- src/core/algorithms/incremental_insertion.rs | 291 +- src/core/algorithms/locate.rs | 107 +- src/core/collections/key_maps.rs | 14 +- src/core/collections/secondary_maps.rs | 6 +- src/core/edge.rs | 30 +- src/core/facet.rs | 77 +- src/core/facet_incidence.rs | 152 +- src/core/insertion.rs | 29 +- src/core/query.rs | 1303 +++++++- src/core/repair.rs | 13 +- src/core/simplex.rs | 355 +-- src/core/tds/keys.rs | 4 +- src/core/tds/mutation.rs | 254 +- src/core/tds/storage.rs | 85 +- src/core/tds/validation.rs | 13 +- src/core/traits/facet_incidence_analysis.rs | 9 +- src/core/triangulation.rs | 4 +- src/core/util/canonical_points.rs | 12 - src/core/util/facet_keys.rs | 7 +- src/core/util/facet_utils.rs | 32 +- src/core/util/jaccard.rs | 88 +- src/core/validation.rs | 310 +- src/core/vertex.rs | 9 - src/delaunay/builder.rs | 43 +- src/delaunay/construction.rs | 408 ++- src/delaunay/delaunayize.rs | 215 +- src/delaunay/deletion.rs | 155 +- src/delaunay/flips.rs | 334 +- src/delaunay/insertion.rs | 73 +- src/delaunay/property_validation.rs | 96 +- src/delaunay/query.rs | 2816 +++++++++++------ src/delaunay/repair.rs | 109 +- src/delaunay/triangulation.rs | 9 +- src/delaunay/validation.rs | 141 +- src/geometry/algorithms/convex_hull.rs | 155 +- src/geometry/kernel.rs | 6 +- src/geometry/robust_predicates.rs | 8 +- src/geometry/util/measures.rs | 5 +- src/geometry/util/triangulation_generation.rs | 4 +- src/lib.rs | 82 +- src/topology/characteristics/euler.rs | 15 +- src/topology/characteristics/validation.rs | 6 +- src/topology/manifold.rs | 2 +- src/topology/ridge.rs | 78 +- src/topology/spaces/toroidal.rs | 8 +- tests/README.md | 12 +- tests/benchmark_flip_fixtures.rs | 60 +- tests/delaunay_edge_cases.rs | 20 +- tests/delaunay_incremental_insertion.rs | 30 +- tests/delaunay_repair_fallback.rs | 36 +- tests/delaunayize_workflow.rs | 58 +- tests/euler_characteristic.rs | 39 +- tests/insert_with_statistics.rs | 4 +- tests/large_scale_debug.rs | 2 +- tests/pachner_roundtrip.rs | 270 +- tests/prelude_exports.rs | 408 ++- tests/proptest_convex_hull.rs | 23 +- tests/proptest_delaunay_triangulation.rs | 6 +- tests/proptest_euler_characteristic.rs | 17 +- tests/proptest_facet.rs | 117 +- tests/proptest_flips.rs | 2 +- tests/proptest_orientation.rs | 10 +- tests/proptest_serialization.rs | 8 +- tests/proptest_simplex.rs | 4 +- tests/proptest_tds.rs | 38 +- tests/proptest_triangulation.rs | 65 +- tests/public_topology_api.rs | 105 +- tests/regressions.rs | 8 +- tests/semgrep/docs/validation_levels.md | 21 + tests/semgrep/src/project_rules/rust_style.rs | 144 + tests/serialization_vertex_preservation.rs | 50 +- tests/trait_bound_ergonomics.rs | 60 + tests/triangulation_builder.rs | 54 +- 101 files changed, 8408 insertions(+), 4671 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38020a81..abfb4f95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,7 @@ dependencies = [ "arc-swap", "criterion", "la-stack", + "markov-chain-monte-carlo", "num-traits", "ordered-float", "pastey", @@ -439,6 +440,15 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "markov-chain-monte-carlo" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00d59c60c04dd1225aa5758207755b2bc673472241c356dc52dc77f1636bed6" +dependencies = [ + "rand 0.10.2", +] + [[package]] name = "matchers" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index cd2db2f9..98e9bb93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ uuid = { version = "1.23.4", features = [ "v4", "serde", "fast-rng" ] } [dev-dependencies] approx = "0.5.1" criterion = { version = "0.8.2", features = [ "html_reports" ] } +markov-chain-monte-carlo = "0.4.0" pastey = "0.2.3" proptest = "1.11.0" serde_json = "1.0.150" diff --git a/README.md b/README.md index 1ee21ab9..7ce35b36 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ and [`docs/numerical_robustness_guide.md`](docs/numerical_robustness_guide.md). | Level | Validates | Primary API | |---|---|---| | 1 | Vertex, simplex, and facet element invariants | `vertex.is_valid()` / `simplex.is_valid()` | -| 2 | TDS keys, incidences, and neighbor links | `dt.tds().is_valid()` / `dt.tds().structure_report()` | +| 2 | TDS keys, incidences, and neighbor links | `dt.validate_structure()` / `dt.structure_report()` | | 3 | Manifold topology, ridge links, and Euler consistency | `dt.as_triangulation().is_valid_topology()` / `dt.as_triangulation().topology_report()` | | 4 | Faithful embedding | `dt.as_triangulation().is_valid_embedding()` / `dt.as_triangulation().embedding_report()` | | 5 | Delaunay property via local predicates | `dt.is_valid_delaunay()` / `dt.delaunay_report()` | diff --git a/benches/README.md b/benches/README.md index d73b3ec9..b987d3b9 100644 --- a/benches/README.md +++ b/benches/README.md @@ -12,8 +12,8 @@ predicates fast across 2D-5D. | `ci_performance_suite.rs` | Public workflow regression contract | Calibrated 2D-5D canaries | ~5-10 min | CI, baselines, `just perf-no-regressions` | | `circumsphere_containment.rs` | Compare circumsphere predicate methods | 2D-5D fixed, 3D random, edge cases | ~5 min | Predicate tuning, summaries | | `cold_path_predicates.rs` | Track hot/cold predicate paths | 2D-5D hot queries, near-boundary cases | ~2-5 min | Predicate optimization work | -| `delaunay_repair.rs` | Flip-based Delaunay repair on prepared Levels 1-4 fixtures | 2D-5D repair-convergent fixtures | ~2-5 min | Repair tuning | -| `pachner_stress.rs` | Unified Pachner move API stress | Accepted 4D k=1/k=2/k=3 forward/inverse moves | <1 min | Monte-Carlo move workflow tuning | +| `delaunay_repair.rs` | Flip-based Delaunay repair plus transaction-pressure cases | 2D-5D repair-convergent fixtures | ~2-5 min | Repair tuning | +| `pachner_stress.rs` | Unified Pachner move API stress | Accepted 4D microcases plus 3D/4D Monte Carlo sequences | Manual | Monte-Carlo move workflow tuning | | `pl_manifold_repair.rs` | Over-shared facet and targeted topology repair | 2D/3D synthetic repair fixtures | <1 min | PL-manifold repair tuning | | `profiling_suite.rs` | Large-scale construction, memory, query, validation profiling | 2D/3D 10k, 4D 3k, 5D 1k | ~2-3 hr | Manual/monthly | | `delete_vertex.rs` | Vertex deletion and rollback cost | 2D-5D fixed cases | ~1-5 min | Vertex deletion | @@ -42,6 +42,7 @@ predicates fast across 2D-5D. | Predicate comparison | `cargo bench --profile perf --bench circumsphere_containment -- --noplot` | | Predicate cold-path work | `cargo bench --profile perf --bench cold_path_predicates -- --noplot` | | Flip-based Delaunay repair | `cargo bench --profile perf --bench delaunay_repair -- --noplot` | +| Flip-repair transaction pressure | `cargo bench --profile perf --bench delaunay_repair -- repair_transaction_pressure --noplot` | | Unified Pachner move stress | `cargo bench --profile perf --bench pachner_stress -- --noplot` | | PL-manifold repair path | `cargo bench --profile perf --features bench --bench pl_manifold_repair -- --noplot` | | Large-scale scaling suite | `cargo bench --profile perf --bench profiling_suite -- --noplot` | @@ -184,6 +185,45 @@ DELAUNAY_BENCH_EXPORT_METRICS=1 \ Use `just bench-perf-summary` for release summaries; it runs the full perf profile summary workflow and captures the construction metrics automatically. +## Pachner Stress + +```bash +cargo bench --profile perf --bench pachner_stress -- --noplot +``` + +`pachner_stress.rs` contains two layers: + +- accepted-move microcases for the unified 4D Pachner API facade +- manual Monte Carlo stress cases for 3D and 4D long-run topology stability + +The Monte Carlo cases default to 10,000 vertices in 3D and 1,000 vertices in +4D, with 100,000 attempted random Pachner moves and topology validation every +1,000 attempts. They validate topology plus the Level 4 embedding invariant +that arbitrary Pachner moves are expected to preserve; Level 5 Delaunay +validity is not a postcondition of random topology edits. The move stream runs +through the `markov-chain-monte-carlo` delayed proposal API with a flat target, +so successfully planned Pachner proposals commit with 100% acceptance while +invalid local candidates are recorded as no-proposal self-loops. Each measured +sequence emits a `pachner_stress_metric` line with accepted/rejected attempts, +proposal diagnostics, validation time, final simplex count, and RSS memory +counters. Validation failures include recent MCMC trace rows so long chains can +be diagnosed by step, outcome, and topology size. + +Useful overrides: + +```bash +DELAUNAY_PACHNER_STRESS_ATTEMPTS=10000 \ +DELAUNAY_PACHNER_STRESS_VERTICES_4D=250 \ +cargo bench --profile perf --bench pachner_stress -- "monte_carlo/4d" +``` + +Supported override families are `DELAUNAY_PACHNER_STRESS_VERTICES`, +`DELAUNAY_PACHNER_STRESS_ATTEMPTS`, +`DELAUNAY_PACHNER_STRESS_VALIDATE_EVERY`, +`DELAUNAY_PACHNER_STRESS_KEY_REFRESH_EVERY`, and +`DELAUNAY_PACHNER_STRESS_SEED`. Append `_3D` or `_4D` for a +dimension-specific value. + ## Circumsphere Containment ```bash diff --git a/benches/allocation_hot_paths.rs b/benches/allocation_hot_paths.rs index a88982ef..2c28dbe8 100644 --- a/benches/allocation_hot_paths.rs +++ b/benches/allocation_hot_paths.rs @@ -20,17 +20,15 @@ mod bench_utils; mod allocation_contracts { use allocation_counter::AllocationInfo; use approx::assert_relative_eq; - use criterion::{BatchSize, BenchmarkGroup, BenchmarkId, Criterion, measurement::WallTime}; - use delaunay::prelude::algorithms::{LocateResult, locate_with_stats}; + use criterion::{BenchmarkGroup, BenchmarkId, Criterion, measurement::WallTime}; + use delaunay::prelude::algorithms::LocateResult; use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, RetryPolicy, Vertex, }; use delaunay::prelude::generators::generate_random_points_in_range_seeded; - use delaunay::prelude::geometry::{ - AdaptiveKernel, CoordinateRange, FastKernel, Point, simplex_volume, - }; + use delaunay::prelude::geometry::{AdaptiveKernel, CoordinateRange, Point, simplex_volume}; use delaunay::prelude::query::measure_with_result; - use delaunay::prelude::tds::{SimplexKey, Tds, TdsError, VertexKey, facet_key_from_vertices}; + use delaunay::prelude::tds::{SimplexKey, TdsError, VertexKey, facet_key_from_vertices}; use delaunay::try_vertices_from_points; use std::assert_matches; use std::{hint::black_box, num::NonZeroUsize, time::Duration}; @@ -47,11 +45,7 @@ mod allocation_contracts { const CANARY_SEED_4D: u64 = 531; const CANARY_SEED_5D: u64 = 816; const SAMPLE_SIZE: usize = 32; - const REMOVAL_BATCH_SIZES: [usize; 6] = [1, 2, 4, 8, 16, 32]; - const INLINE_REMOVAL_RECORD_CAPACITY: usize = 16; - type BenchTriangulation = DelaunayTriangulation, (), (), D>; - type BenchTds = Tds<(), (), D>; #[derive(Debug, Error)] enum AllocationBenchError { @@ -81,7 +75,6 @@ mod allocation_contracts { struct DimensionFixture { dt: BenchTriangulation, simplex_key: SimplexKey, - removal_keys: Vec, facet_vertices: [VertexKey; D], query: Point, simplex_count: usize, @@ -108,8 +101,8 @@ mod allocation_contracts { fn first_simplex_key( dt: &BenchTriangulation, ) -> Result { - dt.tds() - .simplex_keys() + dt.simplices() + .map(|(simplex_key, _)| simplex_key) .next() .ok_or(AllocationBenchError::MissingSimplex { dimension: D }) } @@ -118,13 +111,11 @@ mod allocation_contracts { dt: &BenchTriangulation, simplex_key: SimplexKey, ) -> Result>, AllocationBenchError> { - let tds = dt.tds(); - - tds.simplex_vertices(simplex_key)? + dt.simplex_vertices(simplex_key)? .iter() .copied() .map(|vertex_key| { - tds.vertex(vertex_key).map(|vertex| *vertex.point()).ok_or( + dt.vertex(vertex_key).map(|vertex| *vertex.point()).ok_or( AllocationBenchError::MissingVertex { dimension: D, vertex_key, @@ -139,7 +130,7 @@ mod allocation_contracts { ) -> Result { let mut best: Option<(SimplexKey, f64)> = None; - for simplex_key in dt.tds().simplex_keys() { + for (simplex_key, _) in dt.simplices() { let points = simplex_points(dt, simplex_key)?; let Ok(volume) = simplex_volume(&points) else { continue; @@ -162,7 +153,7 @@ mod allocation_contracts { dt: &BenchTriangulation, simplex_key: SimplexKey, ) -> Result<[VertexKey; D], AllocationBenchError> { - let vertices = dt.tds().simplex_vertices(simplex_key)?; + let vertices = dt.simplex_vertices(simplex_key)?; if vertices.len() < D { return Err(AllocationBenchError::SimplexTooSmall { dimension: D, @@ -188,16 +179,14 @@ mod allocation_contracts { .build() .or_abort(); let simplex_key = representative_simplex_key(&dt).or_abort(); - let removal_keys = dt.tds().simplex_keys().take(32).collect(); let facet_vertices = first_facet_vertices(&dt, simplex_key).or_abort(); let query = dt.simplex_barycenter(simplex_key).or_abort(); - let simplex_count = dt.tds().simplices().count(); - let vertex_count = dt.tds().vertices().count(); + let simplex_count = dt.number_of_simplices(); + let vertex_count = dt.number_of_vertices(); DimensionFixture { dt, simplex_key, - removal_keys, facet_vertices, query, simplex_count, @@ -248,29 +237,10 @@ mod allocation_contracts { } } - const fn remove_simplices_allocation_budget(batch_size: usize) -> u64 { - let expected_frontier_len = batch_size * (D + 1); - if batch_size <= INLINE_REMOVAL_RECORD_CAPACITY - && expected_frontier_len <= INLINE_REMOVAL_RECORD_CAPACITY - { - return 0; - } - - // Larger frontiers intentionally use hash-backed affected-vertex and - // candidate maps. Batches beyond the inline removal capacity also need - // hash membership, duplicate tracking, and heap-backed removal records. - if batch_size <= INLINE_REMOVAL_RECORD_CAPACITY { - 3 - } else { - 7 - } - } - fn bench_public_iterators( group: &mut BenchmarkGroup<'_, WallTime>, fixture: &DimensionFixture, ) { - let tds = fixture.dt.tds(); let tri = fixture.dt.as_triangulation(); let simplex_count = fixture.simplex_count; let vertex_count = fixture.vertex_count; @@ -281,10 +251,6 @@ mod allocation_contracts { b.iter(|| { let (counts, info) = measure_with_result(|| { black_box(( - tds.simplices().count(), - tds.vertices().count(), - tds.simplex_keys().count(), - tds.vertex_keys().count(), tri.simplices().count(), tri.vertices().count(), fixture.dt.simplices().count(), @@ -294,45 +260,35 @@ mod allocation_contracts { assert_eq!( counts, - ( - simplex_count, - vertex_count, - simplex_count, - vertex_count, - simplex_count, - vertex_count, - simplex_count, - vertex_count, - ) - ); - assert_zero_allocations( - &info, - "TDS and public simplices()/vertices() iterators", + (simplex_count, vertex_count, simplex_count, vertex_count,) ); + assert_zero_allocations(&info, "public simplices()/vertices() iterators"); }); }, ); } - fn bench_tds_simplex_vertices( + fn bench_simplex_vertices( group: &mut BenchmarkGroup<'_, WallTime>, fixture: &DimensionFixture, ) { - let tds = fixture.dt.tds(); let simplex_key = fixture.simplex_key; group.bench_function( BenchmarkId::new( - format!("zero_alloc/tds_simplex_vertices_{D}d"), + format!("zero_alloc/simplex_vertices_{D}d"), fixture.vertex_count, ), |b| { b.iter(|| { let (vertex_count, info) = measure_with_result(|| { - tds.simplex_vertices(simplex_key).map(<[VertexKey]>::len) + fixture + .dt + .simplex_vertices(simplex_key) + .map(<[VertexKey]>::len) }); assert_eq!(vertex_count.or_abort(), D + 1); - assert_zero_allocations(&info, "Tds::simplex_vertices"); + assert_zero_allocations(&info, "DelaunayTriangulation::simplex_vertices"); }); }, ); @@ -371,8 +327,8 @@ mod allocation_contracts { group: &mut BenchmarkGroup<'_, WallTime>, fixture: &DimensionFixture, ) { - let tds = fixture.dt.tds(); - let simplex = tds + let simplex = fixture + .dt .simplex(fixture.simplex_key) .or_abort(format!("{D}D benchmark simplex should exist")); @@ -385,11 +341,16 @@ mod allocation_contracts { b.iter(|| { let (uuid_count, info) = measure_with_result(|| { simplex - .vertex_uuid_iter(tds) - .try_fold(0usize, |count, uuid| uuid.map(|_| count + 1)) + .vertices() + .iter() + .copied() + .filter(|&vertex_key| { + fixture.dt.vertex_uuid_from_key(vertex_key).is_some() + }) + .count() }); - assert_eq!(uuid_count.or_abort(), D + 1); - assert_zero_allocations(&info, "Simplex::vertex_uuid_iter"); + assert_eq!(uuid_count, D + 1); + assert_zero_allocations(&info, "DelaunayTriangulation::vertex_uuid_from_key"); }); }, ); @@ -416,52 +377,10 @@ mod allocation_contracts { ); } - fn bench_tds_remove_simplices_by_keys( - group: &mut BenchmarkGroup<'_, WallTime>, - fixture: &DimensionFixture, - ) { - for batch_size in REMOVAL_BATCH_SIZES { - if fixture.removal_keys.len() < batch_size { - continue; - } - - let removal_keys = &fixture.removal_keys[..batch_size]; - let allocation_budget = remove_simplices_allocation_budget::(batch_size); - - group.bench_function( - BenchmarkId::new( - format!("bounded_alloc/tds_remove_simplices_by_keys_{D}d"), - format!( - "vertices_{}_simplices_{}_batch_{}", - fixture.vertex_count, fixture.simplex_count, batch_size - ), - ), - |b| { - b.iter_batched( - || fixture.dt.tds().clone(), - |mut tds: BenchTds| { - let (removed, info) = - measure_with_result(|| tds.remove_simplices_by_keys(removal_keys)); - assert_eq!(removed.or_abort(), batch_size); - assert_allocation_budget( - &info, - "Tds::remove_simplices_by_keys", - allocation_budget, - ); - black_box(tds); - }, - BatchSize::SmallInput, - ); - }, - ); - } - } - fn bench_locate_with_hint_fast_path( group: &mut BenchmarkGroup<'_, WallTime>, fixture: &DimensionFixture, ) { - let kernel = FastKernel::::new(); let simplex_key = fixture.simplex_key; group.bench_function( @@ -472,7 +391,7 @@ mod allocation_contracts { |b| { b.iter(|| { let (locate_result, info) = measure_with_result(|| { - locate_with_stats(fixture.dt.tds(), &kernel, &fixture.query, Some(simplex_key)) + fixture.dt.locate_with_stats(&fixture.query, Some(simplex_key)) }); let (location, stats) = locate_result.or_abort(); @@ -497,11 +416,10 @@ mod allocation_contracts { let fixture = prepare_fixture::(count, seed); bench_public_iterators(group, &fixture); - bench_tds_simplex_vertices(group, &fixture); + bench_simplex_vertices(group, &fixture); bench_simplex_barycenter(group, &fixture); bench_simplex_vertex_uuid_iter(group, &fixture); bench_facet_key_from_vertices(group, &fixture); - bench_tds_remove_simplices_by_keys(group, &fixture); bench_locate_with_hint_fast_path(group, &fixture); } diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index ebb17b6a..f61ee1d2 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -683,7 +683,7 @@ fn bench_k1_roundtrip_case( base_dt: &FlipTriangulation, simplex_key: SimplexKey, ) { - flip_workflows::verify_k1_roundtrip(base_dt, simplex_key, name).or_abort(); + flip_workflows::verify_k1_roundtrip(base_dt, simplex_key).or_abort(); group.bench_function(name, |b| { b.iter_batched( || base_dt.clone(), @@ -703,6 +703,7 @@ fn bench_k2_forward_case( base_dt: &FlipTriangulation, facet: FacetHandle, ) { + flip_workflows::verify_k2_forward(base_dt, facet).or_abort(); group.bench_function(name, |b| { b.iter_batched( || base_dt.clone(), @@ -722,7 +723,7 @@ fn bench_k2_roundtrip_case( base_dt: &FlipTriangulation, facet: FacetHandle, ) { - flip_workflows::verify_k2_roundtrip(base_dt, facet, name).or_abort(); + flip_workflows::verify_k2_roundtrip(base_dt, facet).or_abort(); group.bench_function(name, |b| { b.iter_batched( || base_dt.clone(), @@ -742,6 +743,7 @@ fn bench_k3_forward_case( base_dt: &FlipTriangulation, ridge: RidgeHandle, ) { + flip_workflows::verify_k3_forward(base_dt, ridge).or_abort(); group.bench_function(name, |b| { b.iter_batched( || base_dt.clone(), @@ -761,7 +763,7 @@ fn bench_k3_roundtrip_case( base_dt: &FlipTriangulation, ridge: RidgeHandle, ) { - flip_workflows::verify_k3_roundtrip(base_dt, ridge, name).or_abort(); + flip_workflows::verify_k3_roundtrip(base_dt, ridge).or_abort(); group.bench_function(name, |b| { b.iter_batched( || base_dt.clone(), diff --git a/benches/common/flip_workflows.rs b/benches/common/flip_workflows.rs index bbac7c87..0eb3f13c 100644 --- a/benches/common/flip_workflows.rs +++ b/benches/common/flip_workflows.rs @@ -17,11 +17,9 @@ use delaunay::prelude::construction::{ vertex, }; use delaunay::prelude::geometry::{CoordinateConversionError, Point, RobustKernel, simplex_volume}; -use delaunay::prelude::query::{JaccardComputationError, format_jaccard_report}; -use delaunay::prelude::tds::{EdgeKeyError, FacetError, InvariantError, TdsError, VertexKey}; -use delaunay::prelude::topology::validation::{ - ManifoldError, RidgeCandidate, RidgeCandidateError, ridge_star_simplices, -}; +use delaunay::prelude::query::{JaccardComputationError, QueryError, format_jaccard_report}; +use delaunay::prelude::tds::{EdgeKeyError, FacetError, InvariantError, VertexKey}; +use delaunay::prelude::topology::validation::{RidgeCandidate, RidgeCandidateError}; use delaunay::prelude::validation::DelaunayTriangulationValidationError; use thiserror::Error; use uuid::Uuid; @@ -68,30 +66,20 @@ pub enum FlipWorkflowError { vertex_key: VertexKey, }, - /// Facet handle construction failed before candidate inspection. - #[error("failed to construct facet handle {simplex_key:?}:{facet_index}: {source}")] - FacetHandleConstruction { - /// Candidate simplex key. - simplex_key: SimplexKey, - /// Candidate facet index. - facet_index: u8, - /// Underlying facet-handle construction failure. + /// Facet query failed before candidate inspection. + #[error("failed to iterate facet candidates: {source}")] + FacetIteration { + /// Underlying facet query failure. #[source] source: FacetError, }, - /// Ridge handle construction failed before candidate inspection. - #[error("failed to construct ridge handle {simplex_key:?}:({omit_a}, {omit_b}): {source}")] - RidgeHandleConstruction { - /// Candidate simplex key. - simplex_key: SimplexKey, - /// First omitted index. - omit_a: u8, - /// Second omitted index. - omit_b: u8, - /// Underlying ridge-handle construction failure. + /// Ridge query failed before candidate inspection. + #[error("failed to iterate ridge candidates: {source}")] + RidgeIteration { + /// Underlying ridge query failure. #[source] - source: Box, + source: QueryError, }, /// Ridge vertices could not be parsed into a valid ridge candidate. @@ -104,16 +92,6 @@ pub enum FlipWorkflowError { source: RidgeCandidateError, }, - /// Ridge-star support collection failed. - #[error("failed to collect ridge star for {ridge:?}: {source}")] - RidgeStar { - /// Ridge handle being inspected. - ridge: RidgeHandle, - /// Underlying manifold helper failure. - #[source] - source: Box, - }, - /// Snapshot collection found a dangling simplex-to-vertex incidence. #[error("simplex references missing vertex key {vertex_key:?}")] DanglingSnapshotVertex { @@ -155,8 +133,8 @@ pub enum FlipWorkflowError { actual topology: {actual:#?}" )] TopologyMismatch { - /// Roundtrip context label. - context: String, + /// Topology comparison context. + context: FlipWorkflowContext, /// Jaccard diagnostics for vertex UUIDs. vertex_report: String, /// Jaccard diagnostics for simplex incidence. @@ -320,11 +298,21 @@ pub enum FlipWorkflowError { source: TriangleHandleError, }, + /// A forward-only flip produced an invalid underlying triangulation. + #[error("{context} produced invalid triangulation topology: {source}")] + InvalidAfterForward { + /// Forward-only context. + context: FlipWorkflowContext, + /// Underlying triangulation invariant failure. + #[source] + source: Box, + }, + /// A roundtrip produced a triangulation that failed validation. #[error("{context} produced invalid triangulation: {source}")] InvalidAfterRoundtrip { - /// Roundtrip context label. - context: String, + /// Roundtrip context. + context: FlipWorkflowContext, /// Underlying validation failure. #[source] source: DelaunayTriangulationValidationError, @@ -372,6 +360,58 @@ impl fmt::Display for FlipMoveKind { } } +/// Typed benchmark/test context for exact topology comparisons. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlipWorkflowContext { + /// A forward flip was intentionally compared against the pre-flip topology. + ForwardOnly { + /// Fixture dimension. + dimension: usize, + /// Move kind that was applied. + move_kind: FlipMoveKind, + }, + /// A forward flip followed by its inverse should recover the pre-flip topology. + Roundtrip { + /// Fixture dimension. + dimension: usize, + /// Move kind that was roundtripped. + move_kind: FlipMoveKind, + }, +} + +impl FlipWorkflowContext { + /// Constructs a forward-only topology comparison context. + pub const fn forward_only(move_kind: FlipMoveKind) -> Self { + Self::ForwardOnly { + dimension: D, + move_kind, + } + } + + /// Constructs an exact roundtrip topology comparison context. + pub const fn roundtrip(move_kind: FlipMoveKind) -> Self { + Self::Roundtrip { + dimension: D, + move_kind, + } + } +} + +impl fmt::Display for FlipWorkflowContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ForwardOnly { + dimension, + move_kind, + } => write!(f, "{dimension}D {move_kind} forward-only workflow"), + Self::Roundtrip { + dimension, + move_kind, + } => write!(f, "{dimension}D {move_kind} n=1 ergodicity roundtrip"), + } + } +} + /// Rejected candidate detail retained while searching for a benchmark support. /// /// Candidate errors are nested inside [`FlipWorkflowError::NoFlippableFacet`] @@ -511,21 +551,20 @@ pub fn build_flip_dt( pub fn snapshot_topology( dt: &FlipTriangulation, ) -> FlipWorkflowResult { - let tds = dt.tds(); - let mut vertex_uuids = tds + let mut vertex_uuids = dt .vertices() .map(|(_, vertex)| vertex.uuid()) .collect::>(); vertex_uuids.sort(); - let mut simplex_vertex_uuids = tds + let mut simplex_vertex_uuids = dt .simplices() .map(|(_, simplex)| { simplex .vertices() .iter() .map(|&vertex_key| { - tds.vertex(vertex_key) + dt.vertex(vertex_key) .map(Vertex::uuid) .ok_or(FlipWorkflowError::DanglingSnapshotVertex { vertex_key }) }) @@ -559,7 +598,7 @@ pub fn snapshot_topology( pub fn assert_same_topology( actual_dt: &FlipTriangulation, expected: &TopologySnapshot, - context: &str, + context: FlipWorkflowContext, ) -> FlipWorkflowResult<()> { let actual = snapshot_topology(actual_dt)?; if actual == *expected { @@ -605,7 +644,7 @@ pub fn assert_same_topology( })?; Err(FlipWorkflowError::TopologyMismatch { - context: context.to_string(), + context, vertex_report, simplex_report, expected: Box::new(expected.clone()), @@ -668,80 +707,66 @@ pub fn flippable_k2_facet( filter: CandidateFilter, ) -> FlipWorkflowResult { let mut last_error = None; - for (simplex_key, simplex) in dt.simplices() { - let Some(neighbors) = simplex.neighbors() else { + for facet in dt.facets() { + let facet = facet + .map_err(|source| FlipWorkflowError::FacetIteration { source })? + .handle(); + if facet_neighbor_key(dt, facet)?.is_none() { continue; - }; - - for (facet_index, neighbor) in neighbors.enumerate() { - if neighbor.is_none() { - continue; - } - let Ok(facet_index) = u8::try_from(facet_index) else { - continue; - }; - let facet = - FacetHandle::try_new(dt.tds(), simplex_key, facet_index).map_err(|source| { - FlipWorkflowError::FacetHandleConstruction { - simplex_key, - facet_index, - source, - } - })?; - let support = facet_support_points(dt, facet)?; - if !filter.accepts(&support) { - continue; - } + } + let support = facet_support_points(dt, facet)?; + if !filter.accepts(&support) { + continue; + } - let mut trial = dt.clone(); - match trial.flip_k2(facet) { - Ok(info) => { - if info.inserted_face_vertices.len() != 2 { - last_error = Some(Box::new( - FlipCandidateError::UnexpectedInsertedFaceVertexCount { - move_kind: FlipMoveKind::K2, - observed: info.inserted_face_vertices.len(), - expected: 2, - }, - )); - continue; - } - let edge = match EdgeKey::try_new( - trial.tds(), - info.inserted_face_vertices[0], - info.inserted_face_vertices[1], - ) { - Ok(edge) => edge, - Err(source) => { - last_error = Some(Box::new(FlipCandidateError::InvalidInsertedEdge { - move_kind: FlipMoveKind::K2, - source, - })); - continue; - } - }; - if let Err(source) = trial.as_triangulation().validate() { - last_error = Some(Box::new(FlipCandidateError::InvalidAfterForwardFlip { + let mut trial = dt.clone(); + match trial.flip_k2(facet) { + Ok(info) => { + if info.inserted_face_vertices.len() != 2 { + last_error = Some(Box::new( + FlipCandidateError::UnexpectedInsertedFaceVertexCount { move_kind: FlipMoveKind::K2, - source: Box::new(source), - })); - continue; - } - if require_inverse && let Err(source) = trial.flip_k2_inverse_from_edge(edge) { - last_error = Some(Box::new(FlipCandidateError::InverseFlipFailed { + observed: info.inserted_face_vertices.len(), + expected: 2, + }, + )); + continue; + } + let edge = match find_live_edge( + &trial, + info.inserted_face_vertices[0], + info.inserted_face_vertices[1], + ) { + Ok(edge) => edge, + Err(source) => { + last_error = Some(Box::new(FlipCandidateError::InvalidInsertedEdge { move_kind: FlipMoveKind::K2, - source: Box::new(source), + source, })); continue; } - return Ok(facet); + }; + if let Err(source) = trial.as_triangulation().validate() { + last_error = Some(Box::new(FlipCandidateError::InvalidAfterForwardFlip { + move_kind: FlipMoveKind::K2, + source: Box::new(source), + })); + continue; } - Err(source) => { - last_error = Some(Box::new(FlipCandidateError::FlipFailed { + if require_inverse && let Err(source) = trial.flip_k2_inverse_from_edge(edge) { + last_error = Some(Box::new(FlipCandidateError::InverseFlipFailed { move_kind: FlipMoveKind::K2, source: Box::new(source), })); + continue; } + return Ok(facet); + } + Err(source) => { + last_error = Some(Box::new(FlipCandidateError::FlipFailed { + move_kind: FlipMoveKind::K2, + source: Box::new(source), + })); } } } @@ -770,83 +795,63 @@ pub fn flippable_k3_ridge( filter: CandidateFilter, ) -> FlipWorkflowResult { let mut last_error = None; - for (simplex_key, simplex) in dt.simplices() { - let vertex_count = simplex.number_of_vertices(); - for i in 0..vertex_count { - for j in (i + 1)..vertex_count { - let Ok(omit_a) = u8::try_from(i) else { - continue; - }; - let Ok(omit_b) = u8::try_from(j) else { - continue; - }; - let ridge = RidgeHandle::try_new(dt.tds(), simplex_key, omit_a, omit_b).map_err( - |source| FlipWorkflowError::RidgeHandleConstruction { - simplex_key, - omit_a, - omit_b, - source: Box::new(source), - }, - )?; - let support = ridge_support_points(dt, ridge)?; - if !filter.accepts(&support) { + for ridge in dt.ridge_handles() { + let ridge = ridge.map_err(|source| FlipWorkflowError::RidgeIteration { source })?; + let support = ridge_support_points(dt, ridge)?; + if !filter.accepts(&support) { + continue; + } + + let mut trial = dt.clone(); + match trial.flip_k3(ridge) { + Ok(info) => { + if info.inserted_face_vertices.len() != 3 { + last_error = Some(Box::new( + FlipCandidateError::UnexpectedInsertedFaceVertexCount { + move_kind: FlipMoveKind::K3, + observed: info.inserted_face_vertices.len(), + expected: 3, + }, + )); continue; } - - let mut trial = dt.clone(); - match trial.flip_k3(ridge) { - Ok(info) => { - if info.inserted_face_vertices.len() != 3 { - last_error = Some(Box::new( - FlipCandidateError::UnexpectedInsertedFaceVertexCount { - move_kind: FlipMoveKind::K3, - observed: info.inserted_face_vertices.len(), - expected: 3, - }, - )); - continue; - } - let triangle = match TriangleHandle::try_new( - info.inserted_face_vertices[0], - info.inserted_face_vertices[1], - info.inserted_face_vertices[2], - ) { - Ok(triangle) => triangle, - Err(source) => { - last_error = - Some(Box::new(FlipCandidateError::InvalidInsertedTriangle { - move_kind: FlipMoveKind::K3, - source, - })); - continue; - } - }; - if let Err(source) = trial.as_triangulation().validate() { - last_error = - Some(Box::new(FlipCandidateError::InvalidAfterForwardFlip { - move_kind: FlipMoveKind::K3, - source: Box::new(source), - })); - continue; - } - if require_inverse - && let Err(source) = trial.flip_k3_inverse_from_triangle(triangle) - { - last_error = Some(Box::new(FlipCandidateError::InverseFlipFailed { - move_kind: FlipMoveKind::K3, - source: Box::new(source), - })); - continue; - } - return Ok(ridge); - } + let triangle = match TriangleHandle::try_new( + info.inserted_face_vertices[0], + info.inserted_face_vertices[1], + info.inserted_face_vertices[2], + ) { + Ok(triangle) => triangle, Err(source) => { - last_error = Some(Box::new(FlipCandidateError::FlipFailed { + last_error = Some(Box::new(FlipCandidateError::InvalidInsertedTriangle { move_kind: FlipMoveKind::K3, - source: Box::new(source), + source, })); + continue; } + }; + if let Err(source) = trial.as_triangulation().validate() { + last_error = Some(Box::new(FlipCandidateError::InvalidAfterForwardFlip { + move_kind: FlipMoveKind::K3, + source: Box::new(source), + })); + continue; + } + if require_inverse + && let Err(source) = trial.flip_k3_inverse_from_triangle(triangle) + { + last_error = Some(Box::new(FlipCandidateError::InverseFlipFailed { + move_kind: FlipMoveKind::K3, + source: Box::new(source), + })); + continue; } + return Ok(ridge); + } + Err(source) => { + last_error = Some(Box::new(FlipCandidateError::FlipFailed { + move_kind: FlipMoveKind::K3, + source: Box::new(source), + })); } } } @@ -910,8 +915,8 @@ pub fn roundtrip_k1( })?; let new_key = dt - .tds() - .vertex_key_from_uuid(&new_uuid) + .vertices() + .find_map(|(vertex_key, vertex)| (vertex.uuid() == new_uuid).then_some(vertex_key)) .ok_or(FlipWorkflowError::MissingInsertedVertex { uuid: new_uuid })?; dt.flip_k1_remove(new_key) .map_err(|source| FlipWorkflowError::InverseFlipFailed { @@ -948,8 +953,8 @@ pub fn roundtrip_k2( expected: 2, }); } - let edge = EdgeKey::try_new( - dt.tds(), + let edge = find_live_edge( + dt, info.inserted_face_vertices[0], info.inserted_face_vertices[1], ) @@ -966,6 +971,25 @@ pub fn roundtrip_k2( .map(|_| ()) } +/// Verifies that a selected public k=2 forward flip preserves invariants. +/// +/// This is the forward-only setup assertion used before benchmark timings are +/// emitted. +/// +/// # Errors +/// +/// Returns an error when the k=2 forward flip or +/// [`DelaunayTriangulation::validate`] validation fails. +pub fn verify_k2_forward( + base_dt: &FlipTriangulation, + facet: FacetHandle, +) -> FlipWorkflowResult<()> { + let context = FlipWorkflowContext::forward_only::(FlipMoveKind::K2); + let mut trial = base_dt.clone(); + forward_k2(&mut trial, facet)?; + validate_forward_topology(&trial, context) +} + /// Executes a selected public k=3 flip without its inverse. /// /// # Errors @@ -1046,6 +1070,32 @@ pub fn roundtrip_k3( .map(|_| ()) } +/// Verifies that a selected public k=3 forward flip preserves invariants. +/// +/// This is the forward-only setup assertion used before benchmark timings are +/// emitted. +/// +/// # Errors +/// +/// Returns an error when the k=3 forward flip or +/// [`DelaunayTriangulation::validate`] validation fails. +#[cfg_attr( + not(feature = "slow-tests"), + allow( + dead_code, + reason = "k=3 forward-only setup is exercised by high-dimensional slow benchmark fixtures" + ) +)] +pub fn verify_k3_forward( + base_dt: &FlipTriangulation, + ridge: RidgeHandle, +) -> FlipWorkflowResult<()> { + let context = FlipWorkflowContext::forward_only::(FlipMoveKind::K3); + let mut trial = base_dt.clone(); + forward_k3(&mut trial, ridge)?; + validate_forward_topology(&trial, context) +} + /// Verifies exact topology recovery for a selected k=1 roundtrip. /// /// This is the n=1 ergodicity assertion used before benchmark timings are @@ -1059,8 +1109,8 @@ pub fn roundtrip_k3( pub fn verify_k1_roundtrip( base_dt: &FlipTriangulation, simplex_key: SimplexKey, - context: &str, ) -> FlipWorkflowResult<()> { + let context = FlipWorkflowContext::roundtrip::(FlipMoveKind::K1); let before = snapshot_topology(base_dt)?; let mut trial = base_dt.clone(); roundtrip_k1(&mut trial, simplex_key)?; @@ -1081,8 +1131,8 @@ pub fn verify_k1_roundtrip( pub fn verify_k2_roundtrip( base_dt: &FlipTriangulation, facet: FacetHandle, - context: &str, ) -> FlipWorkflowResult<()> { + let context = FlipWorkflowContext::roundtrip::(FlipMoveKind::K2); let before = snapshot_topology(base_dt)?; let mut trial = base_dt.clone(); roundtrip_k2(&mut trial, facet)?; @@ -1110,8 +1160,8 @@ pub fn verify_k2_roundtrip( pub fn verify_k3_roundtrip( base_dt: &FlipTriangulation, ridge: RidgeHandle, - context: &str, ) -> FlipWorkflowResult<()> { + let context = FlipWorkflowContext::roundtrip::(FlipMoveKind::K3); let before = snapshot_topology(base_dt)?; let mut trial = base_dt.clone(); roundtrip_k3(&mut trial, ridge)?; @@ -1121,12 +1171,21 @@ pub fn verify_k3_roundtrip( fn validate_topology_and_delaunay( dt: &FlipTriangulation, - context: &str, + context: FlipWorkflowContext, ) -> FlipWorkflowResult<()> { dt.validate() - .map_err(|source| FlipWorkflowError::InvalidAfterRoundtrip { - context: context.to_string(), - source, + .map_err(|source| FlipWorkflowError::InvalidAfterRoundtrip { context, source }) +} + +fn validate_forward_topology( + dt: &FlipTriangulation, + context: FlipWorkflowContext, +) -> FlipWorkflowResult<()> { + dt.as_triangulation() + .validate() + .map_err(|source| FlipWorkflowError::InvalidAfterForward { + context, + source: Box::new(source), }) } @@ -1180,14 +1239,12 @@ fn simplex_centroid( simplex_key: SimplexKey, ) -> FlipWorkflowResult<[f64; D]> { let simplex = dt - .tds() .simplex(simplex_key) .ok_or(FlipWorkflowError::MissingSimplex { simplex_key })?; let mut coords = [0.0_f64; D]; for &vkey in simplex.vertices() { let vertex = dt - .tds() .vertex(vkey) .ok_or(FlipWorkflowError::MissingVertex { vertex_key: vkey })?; let vcoords = vertex.point().coords(); @@ -1215,7 +1272,6 @@ fn simplex_points( simplex_key: SimplexKey, ) -> FlipWorkflowResult>> { let simplex = dt - .tds() .simplex(simplex_key) .ok_or(FlipWorkflowError::MissingSimplex { simplex_key })?; vertex_points(dt, simplex.vertices()) @@ -1232,30 +1288,16 @@ fn facet_support_points( dt: &FlipTriangulation, facet: FacetHandle, ) -> FlipWorkflowResult>> { - let simplex = - dt.tds() - .simplex(facet.simplex_key()) - .ok_or(FlipWorkflowError::MissingSimplex { - simplex_key: facet.simplex_key(), - })?; - let vertex_count = simplex.number_of_vertices(); - let facet_index = usize::from(facet.facet_index()); - if facet_index >= vertex_count { - return Err(FlipWorkflowError::InvalidFacetSupportIndex { - facet, - facet_index: facet.facet_index(), - vertex_count, - simplex_key: facet.simplex_key(), - }); - } - let neighbor_key = simplex - .neighbors() - .and_then(|mut neighbors| neighbors.nth(facet_index).flatten()) + let neighbor_key = facet_neighbor_key(dt, facet)? .ok_or(FlipWorkflowError::FacetWithoutInteriorNeighbor { facet })?; + let simplex = dt + .simplex(facet.simplex_key()) + .ok_or(FlipWorkflowError::MissingSimplex { + simplex_key: facet.simplex_key(), + })?; let mut keys = simplex.vertices().to_vec(); let neighbor = dt - .tds() .simplex(neighbor_key) .ok_or(FlipWorkflowError::MissingSimplex { simplex_key: neighbor_key, @@ -1266,6 +1308,32 @@ fn facet_support_points( vertex_points(dt, &keys) } +/// Returns the live neighbor across a facet while preserving support-query errors. +/// +/// Boundary facets are not flippable k=2 candidates, but direct support +/// inspection still reports them as [`FlipWorkflowError::FacetWithoutInteriorNeighbor`]. +fn facet_neighbor_key( + dt: &FlipTriangulation, + facet: FacetHandle, +) -> FlipWorkflowResult> { + let simplex = dt + .simplex(facet.simplex_key()) + .ok_or(FlipWorkflowError::MissingSimplex { + simplex_key: facet.simplex_key(), + })?; + let vertex_count = simplex.number_of_vertices(); + let facet_index = usize::from(facet.facet_index()); + if facet_index >= vertex_count { + return Err(FlipWorkflowError::InvalidFacetSupportIndex { + facet, + facet_index: facet.facet_index(), + vertex_count, + simplex_key: facet.simplex_key(), + }); + } + Ok(simplex.neighbor_key(facet_index).flatten()) +} + /// Collects the union of simplex vertices across the full k=3 ridge star. /// /// # Errors @@ -1277,12 +1345,11 @@ fn ridge_support_points( dt: &FlipTriangulation, ridge: RidgeHandle, ) -> FlipWorkflowResult>> { - let simplex = - dt.tds() - .simplex(ridge.simplex_key()) - .ok_or(FlipWorkflowError::MissingSimplex { - simplex_key: ridge.simplex_key(), - })?; + let simplex = dt + .simplex(ridge.simplex_key()) + .ok_or(FlipWorkflowError::MissingSimplex { + simplex_key: ridge.simplex_key(), + })?; let vertex_count = simplex.number_of_vertices(); let omit_a = usize::from(ridge.omit_a()); let omit_b = usize::from(ridge.omit_b()); @@ -1314,37 +1381,42 @@ fn ridge_support_points( .map(|(_, vertex_key)| *vertex_key), ) .map_err(|source| FlipWorkflowError::InvalidRidgeCandidate { ridge, source })?; - let star_simplices = ridge_star_simplices(dt.tds(), &ridge_candidate) - .map_err(|source| ridge_star_error(ridge, source))?; - let mut keys = Vec::new(); - for simplex_key in star_simplices { - let star_simplex = dt - .tds() - .simplex(simplex_key) - .ok_or(FlipWorkflowError::MissingSimplex { simplex_key })?; - keys.extend(star_simplex.vertices()); + for (_, star_simplex) in dt.simplices() { + if ridge_candidate + .as_slice() + .iter() + .all(|vertex_key| star_simplex.vertices().contains(vertex_key)) + { + keys.extend(star_simplex.vertices()); + } } keys.sort_unstable(); keys.dedup(); vertex_points(dt, &keys) } -/// Preserves specific missing-simplex and missing-vertex support errors while -/// keeping other ridge-star failures attached to the inspected ridge. -fn ridge_star_error(ridge: RidgeHandle, source: ManifoldError) -> FlipWorkflowError { - match source { - ManifoldError::Tds(TdsError::SimplexNotFound { simplex_key, .. }) => { - FlipWorkflowError::MissingSimplex { simplex_key } - } - ManifoldError::Tds(TdsError::VertexNotFound { vertex_key, .. }) => { - FlipWorkflowError::MissingVertex { vertex_key } - } - source => FlipWorkflowError::RidgeStar { - ridge, - source: Box::new(source), - }, +/// Resolves two vertex keys to a live public edge key. +fn find_live_edge( + dt: &FlipTriangulation, + a: VertexKey, + b: VertexKey, +) -> Result { + if a == b { + return Err(EdgeKeyError::DuplicateEndpoint { endpoint: a }); + } + if !dt.contains_vertex_key(a) { + return Err(EdgeKeyError::MissingEndpoint { endpoint: a }); + } + if !dt.contains_vertex_key(b) { + return Err(EdgeKeyError::MissingEndpoint { endpoint: b }); } + dt.edges() + .find(|edge| { + let (first, second) = edge.endpoints(); + (first == a && second == b) || (first == b && second == a) + }) + .ok_or(EdgeKeyError::EdgeNotFound { v0: a, v1: b }) } /// Resolves vertex keys to points. @@ -1358,12 +1430,11 @@ fn vertex_points( ) -> FlipWorkflowResult>> { keys.iter() .map(|vertex_key| { - dt.tds() - .vertex(*vertex_key) - .map(|vertex| *vertex.point()) - .ok_or(FlipWorkflowError::MissingVertex { + dt.vertex(*vertex_key).map(|vertex| *vertex.point()).ok_or( + FlipWorkflowError::MissingVertex { vertex_key: *vertex_key, - }) + }, + ) }) .collect() } diff --git a/benches/delaunay_repair.rs b/benches/delaunay_repair.rs index 6e9adfa9..fde23ecd 100644 --- a/benches/delaunay_repair.rs +++ b/benches/delaunay_repair.rs @@ -13,6 +13,11 @@ //! seed, so the measured closure never times a repair failure chain. Case //! labels record whether the prepared fixture was `violating` or already //! `delaunay`, so baseline comparisons notice when a fixture changes meaning. +//! A separate `repair_transaction_pressure` case selects fixtures whose probe +//! run performs at least one direct flip without heuristic rebuild fallback, +//! isolating the public repair path most sensitive to per-flip rollback +//! snapshots. Compare that case with `tds_clone.rs` when evaluating journaled or +//! reusable rollback designs. //! Exactly-cospherical adversarial fixtures are deliberately excluded: strict //! flip repair can legitimately fail to converge on them, which is a //! correctness scenario rather than a stable performance contract. @@ -24,7 +29,10 @@ //! cargo bench --profile perf --bench delaunay_repair //! ``` -use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use criterion::{ + BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, + measurement::WallTime, +}; use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, Vertex, }; @@ -32,9 +40,10 @@ use delaunay::prelude::generators::generate_random_points_in_range_seeded; use delaunay::prelude::geometry::{ AdaptiveKernel, CoordinateRange, ExactPredicates, Kernel, Point, }; -use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; +use delaunay::prelude::repair::{DelaunayRepairHeuristicConfig, DelaunayRepairOutcome}; use delaunay::try_vertices_from_points; use std::hint::black_box; +use std::num::NonZeroUsize; use std::process; use std::time::Duration; @@ -48,13 +57,45 @@ const SEED_SEARCH_ATTEMPTS: usize = 16; const SAMPLE_SIZE: usize = 10; const WARM_UP_TIME: Duration = Duration::from_millis(500); const MEASUREMENT_TIME: Duration = Duration::from_secs(2); +const TRANSACTION_PRESSURE_MIN_FLIPS: NonZeroUsize = NonZeroUsize::MIN; +const TRANSACTION_PRESSURE_SEED_SALT: u64 = 0xA511_E9B3_6C4D_27F1; type BenchTriangulation = DelaunayTriangulation, (), (), D>; +#[derive(Clone, Copy)] +enum RepairFixtureRequirement { + AnyConvergent, + DirectFlipRepair { min_flips: NonZeroUsize }, +} + +impl RepairFixtureRequirement { + /// Returns whether a probe run has the repair behavior this benchmark case needs. + const fn accepts(self, outcome: &DelaunayRepairOutcome) -> bool { + match self { + Self::AnyConvergent => true, + Self::DirectFlipRepair { min_flips } => { + !outcome.used_heuristic() && outcome.stats.flips_performed >= min_flips.get() + } + } + } + + /// Describes the fixture predicate for benchmark setup failure messages. + fn description(self) -> String { + match self { + Self::AnyConvergent => String::from("repair-convergent"), + Self::DirectFlipRepair { min_flips } => format!( + "direct flip-repair fixture with at least {} performed flips", + min_flips.get() + ), + } + } +} + struct RepairSource { vertex_count: usize, simplex_count: usize, violating: bool, + probe_flips_performed: usize, triangulation: BenchTriangulation, } @@ -106,6 +147,26 @@ fn generate_vertices(requested_vertices: usize, seed: u64) -> Ve /// completes `repair_delaunay_with_flips_advanced`, so the measured closure /// never times a repair failure chain. fn build_source(requested_vertices: usize, seed_base: u64) -> RepairSource +where + AdaptiveKernel: ExactPredicates + Kernel, +{ + build_source_with_requirement( + requested_vertices, + seed_base, + RepairFixtureRequirement::AnyConvergent, + ) +} + +/// Build one repair fixture whose probe satisfies the requested repair behavior. +/// +/// The transaction-pressure case uses this to require successful direct +/// flip-based repair with performed flips, keeping heuristic rebuild setup out +/// of the benchmark case that is meant to expose per-flip snapshot cost. +fn build_source_with_requirement( + requested_vertices: usize, + seed_base: u64, + requirement: RepairFixtureRequirement, +) -> RepairSource where AdaptiveKernel: ExactPredicates + Kernel, { @@ -124,25 +185,29 @@ where }; let mut probe = triangulation.clone(); - if probe - .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) - .is_err() - { + let Ok(outcome) = + probe.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) + else { + continue; + }; + if !requirement.accepts(&outcome) { continue; } - let violating = triangulation.is_delaunay_via_flips().is_err(); + let violating = triangulation.verify_via_flip_predicates().is_err(); return RepairSource { vertex_count: triangulation.number_of_vertices(), simplex_count: triangulation.number_of_simplices(), violating, + probe_flips_performed: outcome.stats.flips_performed, triangulation, }; } abort_benchmark(format!( - "no repair-convergent {D}D fixture built for {requested_vertices} vertices \ - after {SEED_SEARCH_ATTEMPTS} seeds" + "no {} {D}D fixture built for {requested_vertices} vertices \ + after {SEED_SEARCH_ATTEMPTS} seeds", + requirement.description() )) } @@ -158,6 +223,7 @@ fn bench_repair_dimension( dim_label: &str, counts: &[usize], seed_base: u64, + transaction_pressure_vertices: usize, ) where AdaptiveKernel: ExactPredicates + Kernel, { @@ -202,27 +268,77 @@ fn bench_repair_dimension( ); } + bench_transaction_pressure_case( + &mut group, + transaction_pressure_vertices, + seed_base ^ TRANSACTION_PRESSURE_SEED_SALT, + ); + group.finish(); } +/// Register the public repair case selected to exercise transactional rollback snapshots. +fn bench_transaction_pressure_case( + group: &mut BenchmarkGroup<'_, WallTime>, + requested_vertices: usize, + seed_base: u64, +) where + AdaptiveKernel: ExactPredicates + Kernel, +{ + let source = build_source_with_requirement::( + requested_vertices, + seed_base, + RepairFixtureRequirement::DirectFlipRepair { + min_flips: TRANSACTION_PRESSURE_MIN_FLIPS, + }, + ); + group.throughput(Throughput::Elements(triangulation_element_count(&source))); + + group.bench_with_input( + BenchmarkId::new( + "repair_transaction_pressure", + format!( + "flip_repair_vertices_{}_simplices_{}_probe_flips_{}", + source.vertex_count, source.simplex_count, source.probe_flips_performed + ), + ), + &source, + |b, source| { + b.iter_batched( + || source.triangulation.clone(), + |mut triangulation| { + black_box( + triangulation + .repair_delaunay_with_flips_advanced( + DelaunayRepairHeuristicConfig::default(), + ) + .or_abort(), + ); + }, + BatchSize::SmallInput, + ); + }, + ); +} + /// Benchmark 2D flip-based Delaunay repair. fn bench_delaunay_repair_2d(c: &mut Criterion) { - bench_repair_dimension::<2>(c, "2d", &[500, 2_000], 0x2EFA_0000_0000_0002); + bench_repair_dimension::<2>(c, "2d", &[500, 2_000], 0x2EFA_0000_0000_0002, 2_000); } /// Benchmark 3D flip-based Delaunay repair. fn bench_delaunay_repair_3d(c: &mut Criterion) { - bench_repair_dimension::<3>(c, "3d", &[150, 500], 0x2EFA_0000_0000_0003); + bench_repair_dimension::<3>(c, "3d", &[150, 500], 0x2EFA_0000_0000_0003, 500); } /// Benchmark 4D flip-based Delaunay repair. fn bench_delaunay_repair_4d(c: &mut Criterion) { - bench_repair_dimension::<4>(c, "4d", &[50, 100], 0x2EFA_0000_0000_0004); + bench_repair_dimension::<4>(c, "4d", &[50, 100], 0x2EFA_0000_0000_0004, 100); } /// Benchmark 5D flip-based Delaunay repair. fn bench_delaunay_repair_5d(c: &mut Criterion) { - bench_repair_dimension::<5>(c, "5d", &[25, 40], 0x2EFA_0000_0000_0005); + bench_repair_dimension::<5>(c, "5d", &[25, 40], 0x2EFA_0000_0000_0005, 40); } criterion_group!( diff --git a/benches/locate.rs b/benches/locate.rs index 46a03a02..ec562103 100644 --- a/benches/locate.rs +++ b/benches/locate.rs @@ -21,7 +21,7 @@ //! ``` use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use delaunay::prelude::algorithms::{LocateResult, locate}; +use delaunay::prelude::algorithms::LocateResult; use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationBuilder, Vertex, }; @@ -74,8 +74,6 @@ fn query_bounds() -> CoordinateRange { /// Build one deterministic triangulation plus an inside-the-hull query batch. fn build_source(requested_vertices: usize, seed_base: u64) -> LocateSource { - let kernel = AdaptiveKernel::::new(); - for attempt in 0..SEED_SEARCH_ATTEMPTS { let attempt_seed = u64::try_from(attempt).or_abort(); let seed = seed_for_case::(requested_vertices, seed_base) @@ -100,7 +98,7 @@ fn build_source(requested_vertices: usize, seed_base: u64) -> Lo if hinted_queries.len() == QUERY_COUNT { break; } - let located = locate(triangulation.tds(), &kernel, &query, None).or_abort(); + let located = triangulation.locate(&query, None).or_abort(); if let LocateResult::InsideSimplex(simplex_key) = located { hinted_queries.push((query, simplex_key)); } @@ -130,7 +128,6 @@ fn bench_locate_dimension( counts: &[usize], seed_base: u64, ) { - let kernel = AdaptiveKernel::::new(); let sources: Vec> = counts .iter() .map(|&requested_vertices| build_source::(requested_vertices, seed_base)) @@ -156,9 +153,7 @@ fn bench_locate_dimension( |b, source| { b.iter(|| { for (query, _) in &source.hinted_queries { - black_box( - locate(source.triangulation.tds(), &kernel, query, None).or_abort(), - ); + black_box(source.triangulation.locate(query, None).or_abort()); } }); }, @@ -186,10 +181,7 @@ fn bench_locate_dimension( |b, source| { b.iter(|| { for (query, hint) in &source.hinted_queries { - black_box( - locate(source.triangulation.tds(), &kernel, query, Some(*hint)) - .or_abort(), - ); + black_box(source.triangulation.locate(query, Some(*hint)).or_abort()); } }); }, diff --git a/benches/pachner_stress.rs b/benches/pachner_stress.rs index 4cf5796f..0e155316 100644 --- a/benches/pachner_stress.rs +++ b/benches/pachner_stress.rs @@ -6,22 +6,42 @@ //! PL-manifold fixtures. It complements `ci_performance_suite` by focusing on //! the unified dispatch facade rather than the individual flip primitives. -use std::{hint::black_box, num::TryFromIntError}; +use std::{ + env, + fmt::{Display, Write as _}, + hint::black_box, + num::{NonZeroUsize, TryFromIntError}, + sync::LazyLock, + time::{Duration, Instant}, +}; use criterion::{ BatchSize, BenchmarkGroup, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, }; -use delaunay::prelude::construction::{Vertex, vertex}; +use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulationBuilder, RetryPolicy, TopologyGuarantee, Vertex, + vertex, +}; +use delaunay::prelude::generators::generate_random_points_in_range_seeded; +use delaunay::prelude::geometry::{CoordinateRange, RobustKernel}; use delaunay::prelude::pachner::{ - EdgeKey, FacetHandle, PachnerMove, PachnerMoveResult, PachnerMoves, RidgeHandle, SimplexKey, - TriangleHandle, VertexKey, + EdgeKey, FacetHandle, FlipError, PachnerMove, PachnerMoveResult, PachnerMoves, PachnerProposal, + RidgeHandle, SimplexKey, TriangleHandle, VertexKey, }; +use delaunay::prelude::triangulation::Triangulation; +use delaunay::try_vertices_from_points; +use markov_chain_monte_carlo::prelude::delayed::{ + Chain, ChainId, DelayedProposal, DelayedStep, DelayedStepError, Target, Trace, TraceRecorder, + TraceStepOutcome, +}; +use rand::{Rng, RngExt, SeedableRng, rngs::StdRng}; +use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, get_current_pid}; /// Shared benchmark setup error helpers. #[path = "common/bench_utils.rs"] pub mod bench_utils; -use bench_utils::{OrAbort, OrAbortWithContext}; +use bench_utils::{OrAbort, OrAbortWithContext, abort_benchmark}; #[path = "common/flip_fixtures.rs"] #[expect( @@ -39,7 +59,28 @@ use flip_fixtures::STABLE_POINTS_4D; mod flip_workflows; use flip_workflows::{CandidateFilter, FlipTriangulation}; -const MOVES_PER_SAMPLE: usize = 256; +type MonteCarloTriangulation = Triangulation, (), (), D>; + +static MOVES_PER_SAMPLE: LazyLock = + LazyLock::new(|| nonzero_usize("MOVES_PER_SAMPLE", 256)); +static MONTE_CARLO_ATTEMPTS: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_ATTEMPTS", 100_000)); +const MONTE_CARLO_3D_VERTICES: usize = 10_000; +const MONTE_CARLO_4D_VERTICES: usize = 1_000; +static MONTE_CARLO_KEY_REFRESH_EVERY: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_KEY_REFRESH_EVERY", 256)); +static MONTE_CARLO_RETRY_ATTEMPTS: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_RETRY_ATTEMPTS", 24)); +static MONTE_CARLO_SAMPLE_SIZE: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_SAMPLE_SIZE", 10)); +const MONTE_CARLO_TRACE_TAIL: usize = 32; +static MONTE_CARLO_VALIDATE_EVERY: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_VALIDATE_EVERY", 1_000)); +static MONTE_CARLO_VERTEX_GROWTH_DIVISOR: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_VERTEX_GROWTH_DIVISOR", 10)); +static MONTE_CARLO_VERTEX_SHRINK_DIVISOR: LazyLock = + LazyLock::new(|| nonzero_usize("MONTE_CARLO_VERTEX_SHRINK_DIVISOR", 20)); +const MONTE_CARLO_REPORT_ENV: &str = "DELAUNAY_PACHNER_STRESS_REPORT"; struct PachnerStressSetup { base_dt: FlipTriangulation<4>, @@ -55,6 +96,867 @@ struct PachnerStressSetup { k3_inverse_triangle: TriangleHandle, } +#[derive(Clone, Copy)] +struct MonteCarloConfig { + label: &'static str, + vertex_count: usize, + move_attempts: NonZeroUsize, + validate_every: NonZeroUsize, + key_refresh_every: NonZeroUsize, + min_vertex_count: usize, + max_vertex_count: usize, + seed: u64, +} + +impl MonteCarloConfig { + const fn move_attempts(self) -> usize { + self.move_attempts.get() + } + + const fn validate_every(self) -> usize { + self.validate_every.get() + } + + const fn key_refresh_every(self) -> usize { + self.key_refresh_every.get() + } +} + +#[derive(Clone, Copy, Debug)] +struct MonteCarloReport { + attempts: usize, + accepted: usize, + rejected: usize, + candidate_misses: usize, + proposal_rejections: usize, + validations: usize, + validation_nanos: u128, + elapsed_nanos: u128, + attempts_per_second: u128, + final_vertices: usize, + final_simplices: usize, + start_rss_kib: u64, + max_rss_kib: u64, + final_rss_kib: u64, +} + +struct MoveSampler { + simplex_keys: Vec, + vertex_keys: Vec, + facet_handles: Vec, + edge_keys: Vec, + ridge_handles: Vec, +} + +impl MoveSampler { + /// Captures the current live key frontier used for randomized move proposals. + fn from_triangulation(dt: &MonteCarloTriangulation) -> Self { + let mut sampler = Self { + simplex_keys: Vec::new(), + vertex_keys: Vec::new(), + facet_handles: Vec::new(), + edge_keys: Vec::new(), + ridge_handles: Vec::new(), + }; + sampler.refresh(dt); + sampler + } + + /// Refreshes cached keys after enough accepted moves may have stale candidates. + fn refresh(&mut self, dt: &MonteCarloTriangulation) { + self.simplex_keys.clear(); + self.simplex_keys + .extend(dt.simplices().map(|(simplex_key, _)| simplex_key)); + + self.vertex_keys.clear(); + self.vertex_keys + .extend(dt.vertices().map(|(vertex_key, _)| vertex_key)); + + self.facet_handles.clear(); + self.facet_handles + .extend(dt.facets().map(|facet| facet.or_abort().handle())); + + self.edge_keys.clear(); + self.edge_keys.extend(dt.edges()); + + self.ridge_handles.clear(); + self.ridge_handles + .extend(dt.ridge_handles().map(OrAbort::or_abort)); + } + + /// Selects a cached simplex key uniformly from the last refresh. + fn random_simplex_key(&self, rng: &mut R) -> Option { + random_cached(&self.simplex_keys, rng) + } + + /// Selects a cached vertex key uniformly from the last refresh. + fn random_vertex_key(&self, rng: &mut R) -> Option { + random_cached(&self.vertex_keys, rng) + } + + /// Selects a cached facet handle uniformly from the last refresh. + fn random_facet(&self, rng: &mut R) -> Option { + random_cached(&self.facet_handles, rng) + } + + /// Selects a cached edge key uniformly from the last refresh. + fn random_edge(&self, rng: &mut R) -> Option { + random_cached(&self.edge_keys, rng) + } + + /// Selects a cached ridge handle uniformly from the last refresh. + fn random_ridge(&self, rng: &mut R) -> Option { + random_cached(&self.ridge_handles, rng) + } +} + +/// Selects a cached proposal item uniformly while preserving empty-cache misses. +fn random_cached(values: &[T], rng: &mut R) -> Option { + (!values.is_empty()).then(|| { + let index = rng.random_range(0..values.len()); + values[index] + }) +} + +/// Flat diagnostic target: successful planned Pachner moves accept with probability one. +struct FlatPachnerTarget; + +impl Target> for FlatPachnerTarget { + fn log_prob(&self, _state: &MonteCarloTriangulation) -> f64 { + 0.0 + } +} + +#[derive(Clone, Debug)] +struct PachnerChainPlan { + request: PachnerMove<(), D>, + proposal: PachnerProposal<(), D>, +} + +#[derive(Clone, Debug)] +enum PachnerStepInfo { + Proposed { + request: PachnerMove<(), D>, + }, + CandidateMiss, + ProposalRejected { + request: PachnerMove<(), D>, + rejection: FlipError, + }, +} + +struct PachnerProposalKernel { + config: MonteCarloConfig, + sampler: MoveSampler, + proposed_steps: usize, + candidate_misses: usize, + proposal_rejections: usize, + last_request: Option>, + last_result: Option>, + last_no_plan_info: Option>, +} + +impl PachnerProposalKernel { + fn new(dt: &MonteCarloTriangulation, config: MonteCarloConfig) -> Self { + Self { + config, + sampler: MoveSampler::from_triangulation(dt), + proposed_steps: 0, + candidate_misses: 0, + proposal_rejections: 0, + last_request: None, + last_result: None, + last_no_plan_info: None, + } + } + + fn maybe_refresh(&mut self, dt: &MonteCarloTriangulation) { + if self + .proposed_steps + .is_multiple_of(self.config.key_refresh_every()) + { + self.sampler.refresh(dt); + } + } +} + +impl DelayedProposal> for PachnerProposalKernel { + type Plan = PachnerChainPlan; + type Info = PachnerStepInfo; + type Error = FlipError; + + fn propose_plan( + &mut self, + state: &MonteCarloTriangulation, + rng: &mut R, + ) -> Result, Self::Error> { + self.proposed_steps = self.proposed_steps.saturating_add(1); + self.maybe_refresh(state); + self.last_result = None; + + let Some(request) = random_pachner_move(state, &self.sampler, rng, self.config) else { + self.candidate_misses = self.candidate_misses.saturating_add(1); + self.last_request = None; + self.last_no_plan_info = Some(PachnerStepInfo::CandidateMiss); + return Ok(None); + }; + + self.last_request = Some(request); + match state.propose_pachner(request) { + Ok(proposal) => { + self.last_no_plan_info = None; + Ok(Some(PachnerChainPlan { request, proposal })) + } + Err(error) => { + self.proposal_rejections = self.proposal_rejections.saturating_add(1); + self.last_no_plan_info = Some(PachnerStepInfo::ProposalRejected { + request, + rejection: error, + }); + Ok(None) + } + } + } + + fn no_plan_info(&mut self) -> Option { + self.last_no_plan_info.take() + } + + fn proposed_log_prob>>( + &self, + state: &MonteCarloTriangulation, + _plan: &Self::Plan, + target: &T, + ) -> Result { + Ok(target.log_prob(state)) + } + + fn info(&self, plan: &Self::Plan) -> Self::Info { + PachnerStepInfo::Proposed { + request: plan.request, + } + } + + fn commit( + &mut self, + state: &mut MonteCarloTriangulation, + plan: Self::Plan, + _rng: &mut R, + ) -> Result<(), Self::Error> { + let result = plan.proposal.attempt_on(state)?; + self.last_result = Some(result); + Ok(()) + } +} + +/// Reads a positive `usize` override, falling back to `default`. +fn configured_usize(name: &str, default: usize) -> usize { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(default) +} + +/// Reads a positive `usize` override, preserving the non-zero proof. +fn configured_nonzero_usize(name: &str, default: NonZeroUsize) -> NonZeroUsize { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .and_then(NonZeroUsize::new) + .unwrap_or(default) +} + +/// Parses a benchmark-owned non-zero default. +fn nonzero_usize(name: &str, value: usize) -> NonZeroUsize { + NonZeroUsize::new(value).or_abort(format_args!("{name} must be non-zero")) +} + +/// Reads a `u64` override, falling back to `default`. +fn configured_u64(name: &str, default: u64) -> u64 { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(default) +} + +/// Reads a case-specific override before the shared Monte Carlo override. +fn configured_case_usize(label: &str, field: &str, default: usize) -> usize { + let case_name = format!( + "DELAUNAY_PACHNER_STRESS_{field}_{}", + label.to_ascii_uppercase() + ); + let shared_name = format!("DELAUNAY_PACHNER_STRESS_{field}"); + configured_usize(&case_name, configured_usize(&shared_name, default)) +} + +/// Reads a case-specific positive override as a proof-bearing nonzero count. +fn configured_case_nonzero_usize(label: &str, field: &str, default: NonZeroUsize) -> NonZeroUsize { + let case_name = format!( + "DELAUNAY_PACHNER_STRESS_{field}_{}", + label.to_ascii_uppercase() + ); + let shared_name = format!("DELAUNAY_PACHNER_STRESS_{field}"); + configured_nonzero_usize(&case_name, configured_nonzero_usize(&shared_name, default)) +} + +/// Reads a case-specific seed override before the shared Monte Carlo seed. +fn configured_case_seed(label: &str, default: u64) -> u64 { + let case_name = format!( + "DELAUNAY_PACHNER_STRESS_SEED_{}", + label.to_ascii_uppercase() + ); + configured_u64( + &case_name, + configured_u64("DELAUNAY_PACHNER_STRESS_SEED", default), + ) +} + +/// Returns whether Monte Carlo source and metric lines should be printed. +fn monte_carlo_report_enabled() -> bool { + env::var_os(MONTE_CARLO_REPORT_ENV).is_some() +} + +/// Builds the dimension-specific Monte Carlo stress configuration. +fn monte_carlo_config( + label: &'static str, + default_vertices: usize, + default_seed: u64, +) -> MonteCarloConfig { + let vertex_count = + configured_case_usize(label, "VERTICES", default_vertices).max(D.saturating_add(1)); + let move_attempts = configured_case_nonzero_usize(label, "ATTEMPTS", *MONTE_CARLO_ATTEMPTS); + let validate_every = + configured_case_nonzero_usize(label, "VALIDATE_EVERY", *MONTE_CARLO_VALIDATE_EVERY); + let validate_every = NonZeroUsize::new(validate_every.get().min(move_attempts.get())) + .or_abort(format_args!("clamped validation interval must be non-zero")); + let key_refresh_every = + configured_case_nonzero_usize(label, "KEY_REFRESH_EVERY", *MONTE_CARLO_KEY_REFRESH_EVERY); + let growth_slack = (vertex_count / (*MONTE_CARLO_VERTEX_GROWTH_DIVISOR).get()).max(D + 1); + let shrink_slack = vertex_count / (*MONTE_CARLO_VERTEX_SHRINK_DIVISOR).get(); + + MonteCarloConfig { + label, + vertex_count, + move_attempts, + validate_every, + key_refresh_every, + min_vertex_count: vertex_count.saturating_sub(shrink_slack).max(D + 1), + max_vertex_count: vertex_count.saturating_add(growth_slack), + seed: configured_case_seed(label, default_seed), + } +} + +/// Returns the coordinate range used for Monte Carlo point clouds. +fn monte_carlo_bounds() -> CoordinateRange { + CoordinateRange::try_new(0.0_f64, 1.0).or_abort() +} + +/// Builds the initial randomized triangulation for one Monte Carlo stress case. +fn build_monte_carlo_dt( + config: MonteCarloConfig, + emit_report: bool, +) -> MonteCarloTriangulation { + let points = generate_random_points_in_range_seeded::( + config.vertex_count, + monte_carlo_bounds(), + config.seed, + ) + .or_abort(); + let vertices = try_vertices_from_points(&points).or_abort(); + let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts: *MONTE_CARLO_RETRY_ATTEMPTS, + base_seed: Some(config.seed ^ 0xC0DE_0253_C0DE_0253), + }); + + let dt = DelaunayTriangulationBuilder::new(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .construction_options(options) + .build_with_kernel(&RobustKernel::new()) + .or_abort(); + let tri = dt.into_triangulation(); + validate_monte_carlo_state( + &tri, + format_args!( + "initial Monte Carlo state dimension={D} label={} seed={}", + config.label, config.seed + ), + ); + if emit_report { + println!( + "pachner_stress_source dimension={D} label={} vertices={} simplices={} seed={}", + config.label, + tri.number_of_vertices(), + tri.number_of_simplices(), + config.seed + ); + } + tri +} + +/// Validates the invariants Pachner moves are expected to preserve. +fn validate_monte_carlo_state( + dt: &MonteCarloTriangulation, + context: impl Display, +) { + if let Err(error) = dt.validate() { + abort_benchmark(format_args!( + "{context}: topology validation failed: {error}" + )); + } + if let Err(error) = dt.is_valid_embedding() { + abort_benchmark(format_args!( + "{context}: embedding validation failed: {error}" + )); + } +} + +/// Return current process memory usage in KiB. +fn memory_usage_kib() -> u64 { + let pid = get_current_pid().or_abort(); + let mut system = System::new_with_specifics( + RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing().with_memory()), + ); + system.refresh_processes_specifics( + ProcessesToUpdate::Some(&[pid]), + true, + ProcessRefreshKind::nothing().with_memory(), + ); + system + .process(pid) + .map_or(0, |process| process.memory() / 1024) +} + +/// Converts bounded diagnostic counters into trace-observable values. +fn trace_value(value: usize) -> f64 { + f64::from(u32::try_from(value).or_abort()) +} + +/// Numeric observables recorded for each completed MCMC step. +fn monte_carlo_observables( + dt: &MonteCarloTriangulation, + proposal: &PachnerProposalKernel, +) -> [f64; 4] { + [ + trace_value(dt.number_of_vertices()), + trace_value(dt.number_of_simplices()), + trace_value(proposal.candidate_misses), + trace_value(proposal.proposal_rejections), + ] +} + +/// Records a completed MCMC step in the shared trace format. +fn record_monte_carlo_step( + recorder: &mut TraceRecorder, + chain: &Chain>, + proposal: &PachnerProposalKernel, + step: &DelayedStep>, +) { + recorder + .record( + chain, + TraceStepOutcome::from(step), + monte_carlo_observables(chain.state(), proposal), + ) + .or_abort(); +} + +/// Formats the short proposal metadata attached to the last delayed step. +fn describe_step_info(info: &PachnerStepInfo) -> String { + match info { + PachnerStepInfo::Proposed { request } => format!("proposed request={request:?}"), + PachnerStepInfo::CandidateMiss => String::from("candidate_miss"), + PachnerStepInfo::ProposalRejected { request, rejection } => { + format!("proposal_rejected request={request:?} rejection={rejection}") + } + } +} + +/// Formats the tail of the MCMC trace for invariant-failure diagnostics. +fn trace_tail(trace: &Trace) -> String { + let records = trace.records(); + let start = records.len().saturating_sub(MONTE_CARLO_TRACE_TAIL); + let mut output = String::new(); + for record in &records[start..] { + let values = record.observable_values(); + let vertices = values.first().copied().unwrap_or_default(); + let simplices = values.get(1).copied().unwrap_or_default(); + let candidate_misses = values.get(2).copied().unwrap_or_default(); + let proposal_rejections = values.get(3).copied().unwrap_or_default(); + let outcome = record.outcome(); + let _ = write!( + &mut output, + "step={} accepted={} proposed={} vertices={} simplices={} \ + candidate_misses={} proposal_rejections={}; ", + record.step(), + outcome.is_accepted(), + outcome.had_proposal(), + vertices, + simplices, + candidate_misses, + proposal_rejections + ); + } + output +} + +/// Builds a diagnostic validation context from chain and trace state. +fn monte_carlo_validation_context( + config: MonteCarloConfig, + step: usize, + chain: &Chain>, + proposal: &PachnerProposalKernel, + last_step: Option<&DelayedStep>>, + trace: &Trace, +) -> String { + let chain_id = ChainId::new(0); + let mut context = format!( + "Monte Carlo validation dimension={D} label={} step={} attempts={} accepted={} \ + rejected={} candidate_misses={} proposal_rejections={} acceptance_rate={:.6} \ + last_request={:?} last_result={:?}", + config.label, + step, + config.move_attempts(), + chain.accepted(), + chain.rejected(), + proposal.candidate_misses, + proposal.proposal_rejections, + trace.acceptance_rate(chain_id), + proposal.last_request, + proposal.last_result + ); + if let Some(step) = last_step { + let info = step + .info + .as_ref() + .map_or_else(|| String::from("none"), describe_step_info); + let _ = write!( + &mut context, + " last_step_outcome={:?} last_step_info={} last_log_alpha={:?}", + step.outcome, info, step.log_alpha + ); + } + let _ = write!(&mut context, " trace_tail=[{}]", trace_tail(trace)); + context +} + +/// Aborts with chain context when a delayed Pachner step fails exceptionally. +fn abort_monte_carlo_step_error( + config: MonteCarloConfig, + step: usize, + chain: &Chain>, + proposal: &PachnerProposalKernel, + trace: &Trace, + error: &DelayedStepError, +) -> ! { + let context = monte_carlo_validation_context(config, step, chain, proposal, None, trace); + abort_benchmark(format_args!("{context}: MCMC Pachner step failed: {error}")); +} + +/// Chooses one raw Pachner request from the current cached topology frontier. +fn random_pachner_move( + dt: &MonteCarloTriangulation, + sampler: &MoveSampler, + rng: &mut R, + config: MonteCarloConfig, +) -> Option> { + let move_kind_count = if D >= 4 { 6 } else { 5 }; + let mut move_kind = rng.random_range(0..move_kind_count); + let vertex_count = dt.number_of_vertices(); + if vertex_count >= config.max_vertex_count && move_kind == 0 { + move_kind = 1; + } else if vertex_count <= config.min_vertex_count && move_kind == 1 { + move_kind = 0; + } + + match move_kind { + 0 => random_k1_insert(dt, sampler, rng), + 1 => sampler + .random_vertex_key(rng) + .map(|vertex_key| PachnerMove::K1Remove { vertex_key }), + 2 => random_k2(sampler, rng), + 3 => random_k2_inverse(sampler, rng), + 4 => random_k3(sampler, rng), + 5 => random_k3_inverse(dt, sampler, rng), + _ => None, + } +} + +/// Chooses a random simplex and inserts a vertex at its centroid. +fn random_k1_insert( + dt: &MonteCarloTriangulation, + sampler: &MoveSampler, + rng: &mut (impl Rng + ?Sized), +) -> Option> { + let simplex_key = sampler.random_simplex_key(rng)?; + let coords = random_simplex_centroid(dt, simplex_key)?.or_abort(); + let vertex: Vertex<(), D> = vertex!(coords).or_abort(); + Some(PachnerMove::K1Insert { + simplex_key, + vertex, + }) +} + +/// Chooses a random simplex facet for a k=2 move. +fn random_k2( + sampler: &MoveSampler, + rng: &mut (impl Rng + ?Sized), +) -> Option> { + let facet = sampler.random_facet(rng)?; + Some(PachnerMove::K2 { facet }) +} + +/// Chooses two vertices from a random simplex as an inverse k=2 edge candidate. +fn random_k2_inverse( + sampler: &MoveSampler, + rng: &mut (impl Rng + ?Sized), +) -> Option> { + let edge = sampler.random_edge(rng)?; + Some(PachnerMove::K2Inverse { edge }) +} + +/// Chooses a random ridge from a random simplex for a k=3 move. +fn random_k3( + sampler: &MoveSampler, + rng: &mut (impl Rng + ?Sized), +) -> Option> { + let ridge = sampler.random_ridge(rng)?; + Some(PachnerMove::K3 { ridge }) +} + +/// Chooses three vertices from a random simplex as an inverse k=3 triangle candidate. +fn random_k3_inverse( + dt: &MonteCarloTriangulation, + sampler: &MoveSampler, + rng: &mut (impl Rng + ?Sized), +) -> Option> { + let simplex_key = sampler.random_simplex_key(rng)?; + let vertices = dt.simplex_vertices(simplex_key).ok()?; + let [a, b, c] = three_distinct_indices(rng, vertices.len())?; + let triangle = TriangleHandle::try_new(vertices[a], vertices[b], vertices[c]).or_abort(); + Some(PachnerMove::K3Inverse { triangle }) +} + +/// Computes a live simplex centroid when the cached key still exists. +fn random_simplex_centroid( + dt: &MonteCarloTriangulation, + simplex_key: SimplexKey, +) -> Option> { + let vertices = dt.simplex_vertices(simplex_key).ok()?; + let mut coords = [0.0; D]; + for &vertex_key in vertices { + let vertex_coords = dt.vertex_coords(vertex_key)?; + for (coord, value) in coords.iter_mut().zip(vertex_coords) { + *coord += *value; + } + } + + let vertex_count = match u32::try_from(vertices.len()) { + Ok(value) => f64::from(value), + Err(error) => return Some(Err(error)), + }; + for coord in &mut coords { + *coord /= vertex_count; + } + Some(Ok(coords)) +} + +/// Chooses three distinct indices from a collection length. +fn three_distinct_indices(rng: &mut (impl Rng + ?Sized), len: usize) -> Option<[usize; 3]> { + if len < 3 { + return None; + } + let first = rng.random_range(0..len); + let mut second = rng.random_range(0..len); + while second == first { + second = rng.random_range(0..len); + } + let mut third = rng.random_range(0..len); + while third == first || third == second { + third = rng.random_range(0..len); + } + Some([first, second, third]) +} + +/// Executes one long randomized Pachner sequence and validates periodically. +fn run_monte_carlo_sequence( + dt: MonteCarloTriangulation, + config: MonteCarloConfig, +) -> MonteCarloReport { + let mut rng = StdRng::seed_from_u64(config.seed ^ 0x0253_0253_0253_0253); + let target = FlatPachnerTarget; + let mut chain = Chain::new(dt, &target).or_abort(); + let mut proposal = PachnerProposalKernel::new(chain.state(), config); + let mut recorder = TraceRecorder::new( + ChainId::new(0), + [ + "vertices", + "simplices", + "candidate_misses", + "proposal_rejections", + ], + ) + .or_abort(); + let start_rss_kib = memory_usage_kib(); + let mut max_rss_kib = start_rss_kib; + let mut validations = 0; + let mut validation_nanos = 0; + let mut last_step = None; + + for step in 1..=config.move_attempts() { + let mcmc_step = chain + .step_delayed(&target, &mut proposal, &mut rng) + .unwrap_or_else(|error| { + abort_monte_carlo_step_error( + config, + step, + &chain, + &proposal, + recorder.trace(), + &error, + ); + }); + record_monte_carlo_step(&mut recorder, &chain, &proposal, &mcmc_step); + last_step = Some(mcmc_step); + + if step.is_multiple_of(config.validate_every()) { + let validation_start = Instant::now(); + let context = monte_carlo_validation_context( + config, + step, + &chain, + &proposal, + last_step.as_ref(), + recorder.trace(), + ); + validate_monte_carlo_state(chain.state(), context); + validation_nanos += validation_start.elapsed().as_nanos(); + validations += 1; + max_rss_kib = max_rss_kib.max(memory_usage_kib()); + proposal.sampler.refresh(chain.state()); + } + } + + if !config + .move_attempts() + .is_multiple_of(config.validate_every()) + { + let validation_start = Instant::now(); + let context = monte_carlo_validation_context( + config, + config.move_attempts(), + &chain, + &proposal, + last_step.as_ref(), + recorder.trace(), + ); + validate_monte_carlo_state(chain.state(), context); + validation_nanos += validation_start.elapsed().as_nanos(); + validations += 1; + } + + let final_rss_kib = memory_usage_kib(); + max_rss_kib = max_rss_kib.max(final_rss_kib); + MonteCarloReport { + attempts: config.move_attempts(), + accepted: chain.accepted(), + rejected: chain.rejected(), + candidate_misses: proposal.candidate_misses, + proposal_rejections: proposal.proposal_rejections, + validations, + validation_nanos, + elapsed_nanos: 0, + attempts_per_second: 0, + final_vertices: chain.state().number_of_vertices(), + final_simplices: chain.state().number_of_simplices(), + start_rss_kib, + max_rss_kib, + final_rss_kib, + } +} + +/// Records the Monte Carlo sequence counters in a parseable one-line format. +fn emit_monte_carlo_report(config: MonteCarloConfig, report: MonteCarloReport) { + println!( + "pachner_stress_metric dimension={D} label={} attempts={} accepted={} rejected={} \ + candidate_misses={} proposal_rejections={} validations={} validation_nanos={} \ + elapsed_nanos={} attempts_per_second={} final_vertices={} final_simplices={} \ + start_rss_kib={} max_rss_kib={} final_rss_kib={}", + config.label, + report.attempts, + report.accepted, + report.rejected, + report.candidate_misses, + report.proposal_rejections, + report.validations, + report.validation_nanos, + report.elapsed_nanos, + report.attempts_per_second, + report.final_vertices, + report.final_simplices, + report.start_rss_kib, + report.max_rss_kib, + report.final_rss_kib + ); +} + +/// Runs one Monte Carlo sequence per Criterion iteration and excludes reporting overhead. +fn bench_monte_carlo_case( + c: &mut Criterion, + label: &'static str, + default_vertices: usize, + default_seed: u64, +) { + let config = monte_carlo_config::(label, default_vertices, default_seed); + let mut group = c.benchmark_group(format!("pachner_stress/monte_carlo/{label}")); + group.sample_size((*MONTE_CARLO_SAMPLE_SIZE).get()); + group.warm_up_time(Duration::from_secs(1)); + group.measurement_time(Duration::from_secs(30)); + group.throughput(Throughput::Elements( + u64::try_from(config.move_attempts()).or_abort(), + )); + + let bench_name = format!( + "{}v_{}attempts_validate{}", + config.vertex_count, + config.move_attempts(), + config.validate_every() + ); + let emit_reports = monte_carlo_report_enabled(); + group.bench_function(bench_name, |b| { + let mut source: Option> = None; + b.iter_custom(|iters| { + let mut total = Duration::new(0, 0); + for _ in 0..iters { + let dt = source + .get_or_insert_with(|| build_monte_carlo_dt::(config, emit_reports)) + .clone(); + let start = Instant::now(); + let mut report = run_monte_carlo_sequence(dt, config); + let elapsed = start.elapsed(); + total += elapsed; + report.elapsed_nanos = elapsed.as_nanos(); + let attempts = u128::try_from(report.attempts).or_abort(); + report.attempts_per_second = + attempts.saturating_mul(1_000_000_000) / report.elapsed_nanos.max(1); + if emit_reports { + emit_monte_carlo_report::(config, report); + } + black_box(report); + } + total + }); + }); + + group.finish(); +} + +/// Registers the dimension-scaled Monte Carlo Pachner stress cases. +fn pachner_monte_carlo_stress(c: &mut Criterion) { + bench_monte_carlo_case::<3>(c, "3d", MONTE_CARLO_3D_VERTICES, 0x0253_0000_0000_0003); + bench_monte_carlo_case::<4>(c, "4d", MONTE_CARLO_4D_VERTICES, 0x0253_0000_0000_0004); +} + /// Builds one stable 4D fixture and selects deterministic accepted move supports. fn stress_setup() -> PachnerStressSetup { let base_dt = flip_workflows::build_flip_dt(STABLE_POINTS_4D).or_abort(); @@ -89,13 +991,11 @@ fn simplex_centroid( simplex_key: SimplexKey, ) -> Result<[f64; 4], TryFromIntError> { let simplex = dt - .tds() .simplex(simplex_key) .or_abort(format_args!("missing selected simplex {simplex_key:?}")); let mut coords = [0.0; 4]; for &vertex_key in simplex.vertices() { let vertex = dt - .tds() .vertex(vertex_key) .or_abort(format_args!("missing simplex vertex {vertex_key:?}")); for (coord, value) in coords.iter_mut().zip(vertex.point().coords()) { @@ -126,8 +1026,8 @@ fn k1_remove_fixture( }, ); let vertex_key = dt - .tds() - .vertex_key_from_uuid(&vertex_uuid) + .vertices() + .find_map(|(vertex_key, vertex)| (vertex.uuid() == vertex_uuid).then_some(vertex_key)) .or_abort(format_args!("missing inserted k=1 vertex {vertex_uuid}")); assert_eq!(inserted.inserted_face_vertices.as_slice(), &[vertex_key]); assert!(!inserted.new_simplices.is_empty()); @@ -178,7 +1078,12 @@ fn inserted_edge(dt: &FlipTriangulation<4>, vertices: &[VertexKey]) -> EdgeKey { vertices.len() )); }; - EdgeKey::try_new(dt.tds(), *a, *b).or_abort() + dt.edges() + .find(|edge| { + let (first, second) = edge.endpoints(); + (first == *a && second == *b) || (first == *b && second == *a) + }) + .or_abort(format_args!("inserted k=2 edge {a:?}-{b:?} is missing")) } /// Converts a reported inserted face into an inverse k=3 triangle handle. @@ -194,7 +1099,7 @@ fn inserted_triangle(vertices: &[VertexKey]) -> TriangleHandle { /// Creates one batch of independent triangulation clones for repeated move attempts. fn clone_batch(base_dt: &FlipTriangulation<4>) -> Vec> { - vec![base_dt.clone(); MOVES_PER_SAMPLE] + vec![base_dt.clone(); (*MOVES_PER_SAMPLE).get()] } /// Registers one stress case that repeats the same raw Pachner request. @@ -228,7 +1133,7 @@ fn pachner_stress(c: &mut Criterion) { let setup = stress_setup(); let mut group = c.benchmark_group("pachner_stress"); group.throughput(Throughput::Elements( - u64::try_from(MOVES_PER_SAMPLE).or_abort(), + u64::try_from((*MOVES_PER_SAMPLE).get()).or_abort(), )); bench_pachner_move( @@ -280,5 +1185,5 @@ fn pachner_stress(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, pachner_stress); +criterion_group!(benches, pachner_stress, pachner_monte_carlo_stress); criterion_main!(benches); diff --git a/benches/pl_manifold_repair.rs b/benches/pl_manifold_repair.rs index c420a38b..19a938d5 100644 --- a/benches/pl_manifold_repair.rs +++ b/benches/pl_manifold_repair.rs @@ -45,7 +45,7 @@ fn bench_overshared_facets_orphan_cleanup(c: &mut Criterion) { &fixture, |b, fixture| { b.iter_batched( - || fixture.tds().clone(), + || fixture.repair_input_storage().clone(), |mut tds| { let stats = repair_overshared_facet_orphan_cleanup_3d(&mut tds).or_abort(); let _ = black_box(stats); @@ -100,7 +100,7 @@ fn bench_targeted_fixture( fixture, |b, fixture| { b.iter_batched( - || fixture.tds().clone(), + || fixture.repair_input_storage().clone(), |mut tds| { let stats = repair_targeted_pl_manifold_topology(&mut tds).or_abort(); let _ = black_box(stats); diff --git a/benches/profiling_suite.rs b/benches/profiling_suite.rs index d82ebb94..31f5466e 100644 --- a/benches/profiling_suite.rs +++ b/benches/profiling_suite.rs @@ -523,16 +523,16 @@ fn bench_neighbor_queries( let points = generated_points_in_range::(n_points, wide_bounds(), seed); let vertices = benchmark_vertices_from_generated_points(&points); let dt = construct_triangulation::(&vertices, seed); - let tds = dt.tds(); - let simplex_keys: Vec<_> = tds.simplex_keys().collect(); + let simplex_keys: Vec<_> = dt.simplices().map(|(simplex_key, _)| simplex_key).collect(); group.throughput(Throughput::Elements(simplex_keys.len() as u64)); group.bench_function("find_neighbors_all_simplices", |b| { b.iter(|| { for &simplex_key in &simplex_keys { - let neighbors = tds.find_neighbors_by_key(simplex_key); - black_box(neighbors); + for neighbor in dt.simplex_neighbors(simplex_key) { + black_box(neighbor); + } } }); }); @@ -560,12 +560,11 @@ fn bench_vertex_iteration( let points = generated_points_in_range::(n_points, wide_bounds(), seed); let vertices = benchmark_vertices_from_generated_points(&points); let dt = construct_triangulation::(&vertices, seed); - let tds = dt.tds(); group.bench_function("iterate_all_vertices", |b| { b.iter(|| { let mut count = 0; - for (_, vertex) in tds.vertices() { + for (_, vertex) in dt.vertices() { black_box(vertex); count += 1; } @@ -595,14 +594,13 @@ fn bench_simplex_iteration( let points = generated_points_in_range::(n_points, wide_bounds(), seed); let vertices = benchmark_vertices_from_generated_points(&points); let dt = construct_triangulation::(&vertices, seed); - let tds = dt.tds(); - group.throughput(Throughput::Elements(tds.number_of_simplices() as u64)); + group.throughput(Throughput::Elements(dt.number_of_simplices() as u64)); group.bench_function("iterate_all_simplices", |b| { b.iter(|| { let mut count = 0; - for simplex_key in tds.simplex_keys() { + for (simplex_key, _) in dt.simplices() { black_box(simplex_key); count += 1; } @@ -932,7 +930,6 @@ fn benchmark_query_latency(c: &mut Criterion) { b.iter(|| {}); return; }; - let tds = dt.tds(); // Generate query points let query_points = gen_points::<3>(100, PointDistribution::Random, QUERY_SEED); @@ -942,7 +939,7 @@ fn benchmark_query_latency(c: &mut Criterion) { SmallBuffer, SIMPLEX_VERTICES_BUFFER_SIZE>, > = Vec::with_capacity(MAX_PRECOMPUTED_SIMPLICES); let mut sampled_count = 0; - for simplex in tds.simplices() { + for simplex in dt.simplices() { if sampled_count >= MAX_PRECOMPUTED_SIMPLICES { break; } @@ -954,7 +951,7 @@ fn benchmark_query_latency(c: &mut Criterion) { let mut vertex_points: SmallBuffer, SIMPLEX_VERTICES_BUFFER_SIZE> = SmallBuffer::new(); for vkey in vertex_keys { - if let Some(vertex) = tds.vertex(*vkey) { + if let Some(vertex) = dt.vertex(*vkey) { vertex_points.push(*vertex.point()); } } @@ -1055,11 +1052,11 @@ macro_rules! benchmark_validation_components_dimension { group.measurement_time(bench_time(15)); group.throughput(Throughput::Elements($count as u64)); - group.bench_function("tds_is_valid", |b| { + group.bench_function("is_valid_structure", |b| { b.iter(|| { - if let Err(error) = black_box(dt.tds().is_valid()) { + if let Err(error) = black_box(dt.is_valid_structure()) { abort_benchmark(format_args!( - "TDS validation should pass for benchmark triangulation: {error}" + "structure validation should pass for benchmark triangulation: {error}" )); } }); @@ -1149,15 +1146,24 @@ fn bench_bottlenecks(c: &mut Criterion) { }, |dt| { if let Some(dt) = dt { - let boundary_facets = match dt.tds().one_sided_facets() { - Ok(value) => value, + let boundary_facet_count = match dt.boundary_facets() { + Ok(mut value) => match value + .try_fold(0_usize, |count, facet| facet.map(|_| count + 1)) + { + Ok(count) => count, + Err(error) => { + abort_benchmark(format_args!( + "boundary_facets failed: {error}" + )); + } + }, Err(error) => { abort_benchmark(format_args!( "boundary_facets failed: {error}" )); } }; - black_box(boundary_facets.len()); + black_box(boundary_facet_count); } }, BatchSize::LargeInput, diff --git a/benches/tds_clone.rs b/benches/tds_clone.rs index 1aa5e77d..82dd7f5d 100644 --- a/benches/tds_clone.rs +++ b/benches/tds_clone.rs @@ -1,11 +1,12 @@ #![forbid(unsafe_code)] -//! Benchmark: `Tds::clone` snapshot cost vs triangulation size (2D-5D) +//! Benchmark: public triangulation clone snapshot cost vs triangulation size (2D-5D) //! -//! This benchmark measures the full topology/data snapshot cost that currently -//! dominates transactional rollback designs based on whole-`Tds` cloning. It is -//! intended as a baseline for comparing future journaled or localized rollback -//! designs. +//! This benchmark measures the full owner snapshot cost that currently +//! dominates transactional rollback designs based on whole-topology cloning. It +//! is intended as a baseline for comparing future journaled or localized +//! rollback designs without exposing the raw topology container through the +//! public API. //! //! Intended for **manual** runs (not part of the CI performance suite). //! @@ -18,7 +19,6 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; use delaunay::prelude::generators::generate_random_points_in_range_seeded; use delaunay::prelude::geometry::{AdaptiveKernel, CoordinateRange}; -use delaunay::prelude::tds::Tds; use delaunay::try_vertices_from_points; use std::hint::black_box; use std::time::Duration; @@ -42,7 +42,7 @@ fn benchmark_bounds() -> CoordinateRange { struct CloneSource { vertex_count: usize, simplex_count: usize, - tds: Tds<(), (), D>, + triangulation: BenchTriangulation, } /// Derive a deterministic, dimension-specific seed for one benchmark case. @@ -66,12 +66,13 @@ fn build_clone_source(requested_vertices: usize, seed_base: u64) let vertices = generate_vertices::(requested_vertices, seed); let triangulation: BenchTriangulation = DelaunayTriangulation::builder(&vertices).build().or_abort(); - let tds = triangulation.tds().clone(); + let vertex_count = triangulation.number_of_vertices(); + let simplex_count = triangulation.number_of_simplices(); CloneSource { - vertex_count: tds.number_of_vertices(), - simplex_count: tds.number_of_simplices(), - tds, + vertex_count, + simplex_count, + triangulation, } } @@ -88,7 +89,7 @@ fn bench_dimension( counts: &[usize], seed_base: u64, ) { - let mut group = c.benchmark_group(format!("tds_clone/{dim_label}")); + let mut group = c.benchmark_group(format!("triangulation_clone/{dim_label}")); group.sample_size(SAMPLE_SIZE); group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); @@ -99,7 +100,7 @@ fn bench_dimension( group.bench_with_input( BenchmarkId::new( - "tds_clone", + "triangulation_clone", format!( "vertices_{}_simplices_{}", source.vertex_count, source.simplex_count @@ -107,7 +108,7 @@ fn bench_dimension( ), &source, |b, source| { - b.iter(|| black_box(source.tds.clone())); + b.iter(|| black_box(source.triangulation.clone())); }, ); } diff --git a/docs/api_design.md b/docs/api_design.md index 746102e6..834153c9 100644 --- a/docs/api_design.md +++ b/docs/api_design.md @@ -190,24 +190,13 @@ preserving owner/generation evidence between stages. ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + DelaunayResult, DelaunayTriangulationBuilder, vertex, }; -use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::pachner::{ - EdgeKey, FacetHandle, FlipError, PachnerMove, PachnerMoves, TriangleHandle, + EdgeKey, FacetHandle, PachnerMove, PachnerMoves, TriangleHandle, }; -#[derive(Debug, thiserror::Error)] -enum ExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - Flip(#[from] FlipError), - #[error(transparent)] - Coordinate(#[from] CoordinateConversionError), -} - -fn main() -> Result<(), ExampleError> { +fn main() -> DelaunayResult<()> { // Start with a valid triangulation let vertices = vec![ vertex![0.0, 0.0, 0.0]?, @@ -367,25 +356,11 @@ You can mix both APIs in the same workflow: ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + DelaunayResult, DelaunayTriangulationBuilder, vertex, }; -use delaunay::prelude::geometry::CoordinateConversionError; -use delaunay::prelude::insertion::InsertionError; -use delaunay::prelude::pachner::{FacetHandle, FlipError, PachnerMove, PachnerMoves}; - -#[derive(Debug, thiserror::Error)] -enum ExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - Insertion(#[from] InsertionError), - #[error(transparent)] - Flip(#[from] FlipError), - #[error(transparent)] - Coordinate(#[from] CoordinateConversionError), -} +use delaunay::prelude::pachner::{FacetHandle, PachnerMove, PachnerMoves}; -fn main() -> Result<(), ExampleError> { +fn main() -> DelaunayResult<()> { // 1. Build initial triangulation (Builder API) let vertices = vec![ vertex![0.0, 0.0, 0.0]?, @@ -436,7 +411,7 @@ Use the appropriate validation level for your needs: ```rust // Level 2: Structural only (fast) -assert!(dt.tds().is_valid().is_ok()); +assert!(dt.is_valid_structure().is_ok()); // Level 3: + Manifold topology assert!(dt.as_triangulation().is_valid_topology().is_ok()); diff --git a/docs/architecture/prelude_reference.md b/docs/architecture/prelude_reference.md index 9c0bf964..34b9f4fd 100644 --- a/docs/architecture/prelude_reference.md +++ b/docs/architecture/prelude_reference.md @@ -11,18 +11,18 @@ they exercise. | Construct/configure a Delaunay triangulation | `use delaunay::prelude::construction::*` | | Construction telemetry diagnostics | `use delaunay::prelude::diagnostics::*` | | Export stable simplicial-complex primitives | `use delaunay::prelude::export::*` | -| Validation policies, errors, reports, and Level 5 diagnostics | `use delaunay::prelude::validation::*` | +| Validation policies, errors, reports, PL-manifold link errors, and Level 5 diagnostics | `use delaunay::prelude::validation::*` | | Delaunay repair diagnostics and policies | `use delaunay::prelude::repair::*` | | Delaunayize workflow | `use delaunay::prelude::delaunayize::*` | | Hilbert ordering and quantization utilities | `use delaunay::prelude::ordering::*` | -| Low-level incremental insertion building blocks | `use delaunay::prelude::insertion::*` | +| Incremental insertion diagnostics and result types | `use delaunay::prelude::insertion::*` | | Post-construction vertex deletion errors and keys | `use delaunay::prelude::deletion::*` | | Low-level TDS simplices, facets, keys, and validation reports | `use delaunay::prelude::tds::*` | | Points, simplex embeddings, coordinate ranges, kernels, predicates, and geometric measures | `use delaunay::prelude::geometry::*` | | Random points or triangulations for examples, tests, and benchmarks | `use delaunay::prelude::generators::*` | -| Read-only traversal, adjacency, simplex barycenters, convex hulls, and comparison helpers | `use delaunay::prelude::query::*` | +| Read-only traversal, adjacency, ridge views, simplex barycenters, convex hulls, and comparison helpers | `use delaunay::prelude::query::*` | | Topological spaces, topology traits, and lifted toroidal IDs | `use delaunay::prelude::topology::spaces::*` | -| Topology validation, Euler characteristic helpers, and ridge queries | `use delaunay::prelude::topology::validation::*` | +| Low-level topology validation, Euler characteristic helpers, manifold validators, and ridge queries | `use delaunay::prelude::topology::validation::*` | ## Policy diff --git a/docs/dev/testing.md b/docs/dev/testing.md index 215978b2..ae5a71b2 100644 --- a/docs/dev/testing.md +++ b/docs/dev/testing.md @@ -525,6 +525,24 @@ All `use` imports for a test module must go at the **top** of the module, not inside individual test functions. This keeps dependencies visible in one place and avoids duplicated or scattered imports. +Local test-only helpers, shims, forced-failure hooks, and fixture state belong +inside the owning file's `#[cfg(test)] mod tests { ... }` block. Do not put +local test-only modules or imports in the production module preamble. Production +code that must branch for a unit test should reference helpers under +`tests::...` only from code guarded by `#[cfg(test)]`. Shared cross-module test +support that must live beside private storage internals must be named +`test_support`, placed near the owning tests rather than in the preamble, and +given the narrowest visibility that still lets the tests compile. + +Thread-local fault-injection flags are a last-resort unit-test seam for rare +rollback, repair, and validation branches that cannot be reached +deterministically through public APIs or narrower test fixtures. Keep them +inside the owning `mod tests`, use an RAII guard that restores the previous +value, and document why thread-local state is needed for parallel-test +isolation. Prefer explicit inputs, typed fixtures, or harness APIs whenever they +can cover the branch, and remove the thread-local hook once a cleaner trigger +exists. + Keeping helpers and types **above** macros and tests makes them easy to find and avoids forward-reference confusion. New helpers should be added to this section rather than inlined next to the tests that use them. diff --git a/docs/diagnostics.md b/docs/diagnostics.md index f63c19f3..2ce1eec9 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -133,22 +133,10 @@ empty-circumsphere violations: ```rust use delaunay::prelude::diagnostics::delaunay_violation_report; use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + DelaunayResult, DelaunayTriangulationBuilder, vertex, }; -use delaunay::prelude::geometry::CoordinateConversionError; -use delaunay::prelude::DelaunayValidationError; - -#[derive(Debug, thiserror::Error)] -enum DiagnosticsExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - Coordinate(#[from] CoordinateConversionError), - #[error(transparent)] - Validation(#[from] DelaunayValidationError), -} -fn main() -> Result<(), DiagnosticsExampleError> { +fn main() -> DelaunayResult<()> { let vertices = vec![ vertex![0.0, 0.0, 0.0]?, vertex![1.0, 0.0, 0.0]?, @@ -157,7 +145,7 @@ fn main() -> Result<(), DiagnosticsExampleError> { ]; let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - let report = delaunay_violation_report(dt.tds(), None)?; + let report = dt.delaunay_violation_report(None)?; assert!(report.is_valid()); Ok(()) } @@ -165,7 +153,9 @@ fn main() -> Result<(), DiagnosticsExampleError> { Reports store `SimplexKey` and `VertexKey` values rather than copying every coordinate. This keeps diagnostics compact and lets callers recover coordinates, -UUIDs, or attached data from the original `Tds`. +UUIDs, or attached data from the original triangulation through key-based +queries such as `dt.simplex(key)`, `dt.vertex(key)`, and +`dt.simplex_vertices(key)`. Useful fields: @@ -182,9 +172,7 @@ Useful fields: The logging helper is useful when a failure is easier to inspect as a trace: ```rust -use delaunay::prelude::diagnostics::debug_print_first_delaunay_violation; - -debug_print_first_delaunay_violation(dt.tds(), None); +dt.debug_print_first_delaunay_violation(None); ``` Install a `tracing` subscriber in tests or applications to see output. In tests, diff --git a/docs/invariants.md b/docs/invariants.md index e588dc59..0079a475 100644 --- a/docs/invariants.md +++ b/docs/invariants.md @@ -304,6 +304,12 @@ construction completion by default. When stronger guarantees are required, enables vertex-link validation after every insertion, trading performance for earlier detection and improved diagnosability. +The owner-level public API exposes this certification as +`Triangulation::validate_vertex_links()` and +`DelaunayTriangulation::validate_vertex_links()`. These methods use the +triangulation's declared topology metadata when deciding whether a one-sided +facet is a true boundary or an admissible periodic identification. + ### Ridge links A **ridge** is a codimension‑2 simplex (e.g. an edge in 3D, a triangle in 4D). @@ -326,6 +332,12 @@ PL‑manifoldness. Certain global or vertex‑local pathologies are only detecta via vertex‑link validation, which is why vertex‑link checks are deferred until construction completion by default. +The public owner methods are `Triangulation::validate_ridge_links()`, +`Triangulation::validate_ridge_links_for_simplices()`, and matching +`DelaunayTriangulation` forwarding methods. The localized form is intended for +post-insertion and post-flip diagnostics where the touched simplex frontier is +known. + --- ## Topological domains diff --git a/docs/topology.md b/docs/topology.md index 05430eb8..0c763357 100644 --- a/docs/topology.md +++ b/docs/topology.md @@ -69,7 +69,7 @@ Level 3 always checks: incidence APIs parse this into the owner-bound `FacetToSimplicesIndex` via `Tds::build_facet_to_simplices_index`; Level 3 validation builds one raw `FacetToSimplicesMap`, parses it into `ValidatedFacetDegreeMap`, and reuses - that proof-bearing map so boundary, vertex-link, and Euler checks do not + that proof-bearing map so boundary, ridge-link, vertex-link, and Euler checks do not rebuild or revalidate the same facet-degree evidence. Boundary classification additionally excludes admissible periodic self-identifications, which are closed quotient topology rather than boundary. @@ -94,7 +94,13 @@ Implementation pointers: - Level 3 entry points and validation vocabulary: `src/core/validation.rs` (`Triangulation::is_valid_topology`, `Triangulation::validate`) -- Public manifold validators: `src/topology/manifold.rs` +- Owner-level topology validators: `src/core/validation.rs` and + `src/delaunay/query.rs` + (`Triangulation::validate_ridge_links`, + `Triangulation::validate_ridge_links_for_simplices`, + `Triangulation::validate_vertex_links`, and the matching + `DelaunayTriangulation` forwarding methods) +- Storage-level manifold validators: `src/topology/manifold.rs` (`validate_closed_boundary`, `validate_vertex_links`, `validate_ridge_links`) - Internal raw-map reuse helpers: `src/topology/manifold.rs` (`ValidatedFacetDegreeMap::try_from_facet_map`, @@ -108,8 +114,10 @@ Facet incidence by itself does **not** prove that a facet is a manifold boundary It only describes how many D-simplices share a canonical facet key in the TDS. The current API keeps this distinction explicit: -- `Tds::one_sided_facets()` and `Tds::number_of_one_sided_facets()` report raw - one-sided facet incidence. This is a Level 1–2/TDS fact. +- `Triangulation::facet_incidence_index()` and + `DelaunayTriangulation::facet_incidence_index()` report raw facet incidence; + `FacetIncidenceView::is_one_sided()` identifies one-sided incidences. This is + a Level 1–2 incidence fact, not a topology-aware boundary classification. - `Triangulation::boundary_facets()` and `DelaunayTriangulation::boundary_facets()` report true boundary facets after interpreting the incidence under the triangulation's `GlobalTopology`. @@ -178,8 +186,24 @@ simplicial complex). This is currently not part of Level 3 validation. ## PL-manifold validators (`topology::manifold`) -`src/topology/manifold.rs` contains combinatorial validators for manifold and -PL-manifold invariants (no geometric predicates): +The public owner-level entry points for PL-manifold link checks are: + +- `Triangulation::validate_ridge_links()` and + `DelaunayTriangulation::validate_ridge_links()` for the global codimension-2 + ridge-link screen. +- `Triangulation::validate_ridge_links_for_simplices()` and + `DelaunayTriangulation::validate_ridge_links_for_simplices()` for localized + post-edit diagnostics over a touched simplex frontier. +- `Triangulation::validate_vertex_links()` and + `DelaunayTriangulation::validate_vertex_links()` for the canonical + vertex-link PL-manifold certification. + +These owner methods are the preferred API for papers, examples, tests, and +application code because they carry the triangulation's topology metadata and +avoid exposing raw TDS storage. + +`src/topology/manifold.rs` also contains storage-level combinatorial validators +for manifold and PL-manifold invariants (no geometric predicates): - `validate_closed_boundary` - `validate_ridge_links` diff --git a/docs/validation.md b/docs/validation.md index a43fdb3f..f42f109e 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -361,7 +361,7 @@ fn main() -> DelaunayResult<()> { let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; // Quick structural check (Level 2) - assert!(dt.tds().is_valid().is_ok()); + assert!(dt.is_valid_structure().is_ok()); // Detailed report showing all violations across Levels 1–5 (on failure) match dt.validation_report() { @@ -378,7 +378,7 @@ fn main() -> DelaunayResult<()> { ### Diagnostics -For most users, start with `dt.tds().is_valid()` (fast-fail) or `dt.validation_report()` (full diagnostics across Levels 1–5). +For most users, start with `dt.is_valid_structure()` (fast-fail) or `dt.validation_report()` (full diagnostics across Levels 1–5). --- @@ -393,6 +393,12 @@ Validates that the triangulation forms a valid topological manifold. - `Triangulation::is_valid_topology()` - Level 3 topology fast-fail validation only. - `Triangulation::topology_diagnostic()` - First actionable Level 3 diagnostic. - `Triangulation::topology_report()` - All checkable Level 3 topology failures. +- `Triangulation::validate_ridge_links()` - Explicit global ridge-link + PL-manifold diagnostic. +- `Triangulation::validate_ridge_links_for_simplices()` - Explicit localized + ridge-link diagnostic for a touched simplex frontier. +- `Triangulation::validate_vertex_links()` - Explicit vertex-link + PL-manifold certification. - `Triangulation::validate()` - Levels 1–3 (elements + structure + topology). ### What It Checks @@ -404,18 +410,29 @@ Validates that the triangulation forms a valid topological manifold. 2. **Codimension-2 boundary manifoldness (closed boundary)**: Each (d−2)-ridge on the boundary must be incident to exactly 2 boundary facets - This is the "no boundary of boundary" condition - Interior ridges can have higher degree; only boundary ridges are constrained -3. **PL-manifold vertex-link condition** (when `TopologyGuarantee::PLManifold`): +3. **PL-manifold ridge-link condition** (when `TopologyGuarantee::PLManifold` or + `TopologyGuarantee::PLManifoldStrict`): + The link of every checked ridge is a connected path or cycle. Use + `dt.validate_ridge_links()` for a global explicit check, or + `dt.validate_ridge_links_for_simplices(touched)` after a local edit. +4. **PL-manifold vertex-link condition** (when `TopologyGuarantee::PLManifold` certifies + construction completion, or when `TopologyGuarantee::PLManifoldStrict` checks every insertion): For every vertex `v`, the link `Lk(v)` must be a (D−1)-sphere (interior vertex) or (D−1)-ball (boundary vertex). -4. **Connectedness**: All simplices form a single connected component in the simplex neighbor graph + Use `dt.validate_vertex_links()` for an explicit owner-level check. +5. **Connectedness**: All simplices form a single connected component in the simplex neighbor graph - Detected via a graph traversal over neighbor pointers (O(N·D)) -5. **No isolated vertices**: Every vertex must be incident to at least one simplex -6. **Euler Characteristic**: χ matches expected topology (when an expectation is defined) +6. **No isolated vertices**: Every vertex must be incident to at least one simplex +7. **Euler Characteristic**: χ matches expected topology (when an expectation is defined) - Empty: χ = 0 - Single simplex / Ball(D): χ = 1 - Closed sphere S^D: χ = 1 + (-1)^D - Unknown: χ is computed but not enforced `Triangulation::validate()` (Levels 1–3) additionally runs `Tds::validate()` first. +The `DelaunayTriangulation` wrapper forwards the explicit ridge-link and +vertex-link validators to its owned `Triangulation`, so Delaunay workflows can +call `dt.validate_ridge_links()`, `dt.validate_ridge_links_for_simplices(...)`, +and `dt.validate_vertex_links()` directly. ### Complexity @@ -621,14 +638,14 @@ Start: Do you need to validate? │ ├─ Production hot path? → Usually skip (but validate during integration testing / when debugging) │ └─ Need certainty? → Validate (Level 2 or 3; add Level 4 if embedding matters, Level 5 if Delaunay matters) │ - ├─ After manual TDS mutation? → Level 2 (`dt.tds().is_valid()`) + ├─ After manual topology mutation? → Level 2 (`dt.is_valid_structure()`) │ ├─ Debugging embedded-geometry issues? → Level 4 (`dt.as_triangulation().validate_embedding()`) │ ├─ Debugging Delaunay issues? → Level 5 (`dt.is_valid_delaunay()`) │ ├─ Production validation? - │ ├─ Performance critical? → Level 2 (`dt.tds().is_valid()`) + │ ├─ Performance critical? → Level 2 (`dt.is_valid_structure()`) │ ├─ Topological correctness critical? → Level 3 (`dt.as_triangulation().is_valid_topology()`) │ ├─ Embedded correctness critical? → Level 4 (`dt.as_triangulation().validate_embedding()`) │ └─ Delaunay correctness critical? → Level 5 (`dt.is_valid_delaunay()`) @@ -680,7 +697,7 @@ fn test_my_triangulation_operation() { my_operation(&mut dt); // Validate at appropriate level - assert!(dt.tds().is_valid().is_ok()); // Level 2: Structural + assert!(dt.is_valid_structure().is_ok()); // Level 2: Structural assert!(dt.as_triangulation().is_valid_topology().is_ok()); // Level 3: Topology assert!(dt.as_triangulation().validate_embedding().is_ok()); // Level 4: Faithful embedding assert!(dt.is_valid_delaunay().is_ok()); // Level 5: Delaunay property @@ -709,7 +726,7 @@ pub fn my_algorithm( #[cfg(debug_assertions)] { - dt.tds().is_valid()?; + dt.validate_structure()?; dt.as_triangulation().is_valid_topology()?; } @@ -745,7 +762,7 @@ pub fn validate_with_level( level: u8, ) -> Result<(), ValidationLevelError> { match level { - 2 => dt.tds().is_valid().map_err(ValidationLevelError::from), + 2 => dt.validate_structure().map_err(ValidationLevelError::from), 3 => dt .as_triangulation() .is_valid_topology() diff --git a/docs/workflows.md b/docs/workflows.md index d8f1517c..bbbdb38d 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -371,7 +371,7 @@ fn main() -> DelaunayResult<()> { return Ok(()); }; dt.set_simplex_data(simplex_key, Some(42))?; - assert_eq!(dt.tds().simplex(simplex_key).map(|s| s.data()), Some(Some(&42))); + assert_eq!(dt.simplex(simplex_key).map(|s| s.data()), Some(Some(&42))); Ok(()) } ``` @@ -443,22 +443,10 @@ orientation canonicalization fails, the operation rolls back to the pre-deletion ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + DelaunayResult, DelaunayTriangulationBuilder, vertex, }; -use delaunay::prelude::deletion::DeleteVertexError; -use delaunay::prelude::geometry::CoordinateConversionError; -#[derive(Debug, thiserror::Error)] -enum DeletionExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - DeleteVertex(#[from] DeleteVertexError), - #[error(transparent)] - Coordinate(#[from] CoordinateConversionError), -} - -fn main() -> Result<(), DeletionExampleError> { +fn main() -> DelaunayResult<()> { let vertices = vec![ vertex![0.0, 0.0, 0.0]?, vertex![1.0, 0.0, 0.0]?, @@ -508,22 +496,11 @@ See [`api_design.md`](api_design.md) for the full construction vs local move API ```rust use delaunay::prelude::construction::{ - DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + DelaunayResult, DelaunayTriangulationBuilder, vertex, }; -use delaunay::prelude::geometry::CoordinateConversionError; -use delaunay::prelude::pachner::{FlipError, PachnerMove, PachnerMoves}; - -#[derive(Debug, thiserror::Error)] -enum FlipExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - Flip(#[from] FlipError), - #[error(transparent)] - Coordinate(#[from] CoordinateConversionError), -} +use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; -fn main() -> Result<(), FlipExampleError> { +fn main() -> DelaunayResult<()> { let vertices = vec![ vertex![0.0, 0.0, 0.0]?, vertex![1.0, 0.0, 0.0]?, diff --git a/examples/delaunayize_repair.rs b/examples/delaunayize_repair.rs index 9c103de2..dc9e06fe 100644 --- a/examples/delaunayize_repair.rs +++ b/examples/delaunayize_repair.rs @@ -25,7 +25,7 @@ use delaunay::prelude::construction::{ }; use delaunay::prelude::delaunayize::*; use delaunay::prelude::geometry::CoordinateConversionError; -use delaunay::prelude::pachner::{FacetError, FacetHandle, FlipError, PachnerMove, PachnerMoves}; +use delaunay::prelude::pachner::{FacetError, FlipError, PachnerMove, PachnerMoves}; use delaunay::prelude::validation::DelaunayTriangulationValidationError; // For the generic print_outcome helper. @@ -173,7 +173,7 @@ fn flip_then_repair_2d() -> Result<(), DelaunayizeRepairExampleError> { if let Some(neighbors) = simplex.neighbors() { for (i, n) in neighbors.enumerate() { if let (Some(_), Ok(idx)) = (n, u8::try_from(i)) { - facets.push(FacetHandle::try_new(dt.tds(), ck, idx)?); + facets.push(dt.facet_handle(ck, idx)?); } } } diff --git a/examples/diagnostics.rs b/examples/diagnostics.rs index 0f0f5cf1..79632b8b 100644 --- a/examples/diagnostics.rs +++ b/examples/diagnostics.rs @@ -8,36 +8,16 @@ //! cargo run --features diagnostics --example diagnostics //! ``` -#[cfg(feature = "diagnostics")] -use delaunay::prelude::DelaunayValidationError; #[cfg(feature = "diagnostics")] use delaunay::prelude::construction::{ - ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, + ConstructionOptions, DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, }; #[cfg(feature = "diagnostics")] -use delaunay::prelude::diagnostics::{ - debug_print_first_delaunay_violation, delaunay_violation_report, -}; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::geometry::{AdaptiveKernel, CoordinateConversionError}; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::tds::InvariantError; -#[cfg(feature = "diagnostics")] -#[derive(Debug, thiserror::Error)] -enum DiagnosticsExampleError { - #[error(transparent)] - Construction(#[from] DelaunayTriangulationConstructionError), - #[error(transparent)] - DelaunayValidation(#[from] DelaunayValidationError), - #[error(transparent)] - CoordinateConversion(#[from] CoordinateConversionError), - #[error(transparent)] - Invariant(#[from] InvariantError), -} +use delaunay::prelude::geometry::AdaptiveKernel; #[cfg(feature = "diagnostics")] -fn main() -> Result<(), DiagnosticsExampleError> { +fn main() -> DelaunayResult<()> { init_tracing(); println!("Diagnostics feature example"); @@ -67,7 +47,7 @@ fn init_tracing() { /// Shows the shape of an empty diagnostics report for a valid triangulation. #[cfg(feature = "diagnostics")] -fn report_valid_triangulation() -> Result<(), DiagnosticsExampleError> { +fn report_valid_triangulation() -> DelaunayResult<()> { let vertices = vec![ vertex![0.0, 0.0, 0.0]?, vertex![1.0, 0.0, 0.0]?, @@ -77,7 +57,7 @@ fn report_valid_triangulation() -> Result<(), DiagnosticsExampleError> { let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulationBuilder::new(&vertices).build()?; - let report = delaunay_violation_report(dt.tds(), None)?; + let report = dt.delaunay_violation_report(None)?; println!("Valid 3D triangulation:"); println!(" vertices: {}", report.number_of_vertices); @@ -90,9 +70,9 @@ fn report_valid_triangulation() -> Result<(), DiagnosticsExampleError> { /// Imports valid explicit connectivity that is not Delaunay, then reports the violation. #[cfg(feature = "diagnostics")] -fn report_non_delaunay_triangulation() -> Result<(), DiagnosticsExampleError> { +fn report_non_delaunay_triangulation() -> DelaunayResult<()> { let dt = build_non_delaunay_triangulation_2d()?; - let report = delaunay_violation_report(dt.tds(), None)?; + let report = dt.delaunay_violation_report(None)?; println!("Explicit non-Delaunay 2D triangulation:"); println!(" vertices: {}", report.number_of_vertices); @@ -109,14 +89,14 @@ fn report_non_delaunay_triangulation() -> Result<(), DiagnosticsExampleError> { println!(" offending external vertex: {:?}", detail.offending_vertex); } - debug_print_first_delaunay_violation(dt.tds(), None); + dt.debug_print_first_delaunay_violation(None); Ok(()) } /// Builds a valid Levels 1-4 triangulation whose prescribed diagonal violates Delaunayness. #[cfg(feature = "diagnostics")] fn build_non_delaunay_triangulation_2d() --> Result, (), (), 2>, DiagnosticsExampleError> { +-> DelaunayResult, (), (), 2>> { let vertices = vec![ vertex![0.0, 0.0]?, vertex![4.0, 0.0]?, diff --git a/examples/topology_editing.rs b/examples/topology_editing.rs index 38545766..e5d65c96 100644 --- a/examples/topology_editing.rs +++ b/examples/topology_editing.rs @@ -23,6 +23,7 @@ reason = "example preserves the crate's typed insertion and flip errors instead of erasing them" )] +use delaunay::InvariantError; use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, @@ -36,7 +37,7 @@ use delaunay::prelude::pachner::{ EdgeKey, EdgeKeyError, FacetError, FacetHandle, FlipError, PachnerMove, PachnerMoves, RidgeHandle, TriangleHandle, TriangleHandleError, Vertex, VertexKey, }; -use delaunay::prelude::tds::{InvariantError, TdsError}; +use delaunay::prelude::tds::TdsError; use delaunay::prelude::validation::DelaunayTriangulationValidationError; type ExampleResult = Result; @@ -49,6 +50,8 @@ enum TopologyEditingExampleError { #[error(transparent)] Validation(#[from] DelaunayTriangulationValidationError), #[error(transparent)] + Topology(#[from] InvariantError), + #[error(transparent)] Insertion(#[from] InsertionError), #[error(transparent)] Flip(#[from] FlipError), @@ -61,8 +64,6 @@ enum TopologyEditingExampleError { #[error(transparent)] Tds(#[from] TdsError), #[error(transparent)] - Invariant(#[from] InvariantError), - #[error(transparent)] Circumcenter(#[from] CircumcenterError), #[error(transparent)] CoordinateConversion(#[from] CoordinateConversionError), @@ -179,17 +180,16 @@ fn pachner_2d_k1() -> ExampleResult { .ok_or(TopologyEditingExampleError::EmptyTriangulation { demo: "2D k=1 demo", })?; - let simplex = - dt.tds() - .simplex(simplex_key) - .ok_or(TopologyEditingExampleError::MissingSimplex { - demo: "2D k=1 demo", - })?; + let simplex = dt + .simplex(simplex_key) + .ok_or(TopologyEditingExampleError::MissingSimplex { + demo: "2D k=1 demo", + })?; let vertex_points: Vec> = simplex .vertices() .iter() .map(|vkey| { - dt.tds().vertex(*vkey).map(|vertex| *vertex.point()).ok_or( + dt.vertex(*vkey).map(|vertex| *vertex.point()).ok_or( TopologyEditingExampleError::MissingVertex { demo: "2D k=1 demo", vertex_key: *vkey, @@ -231,7 +231,7 @@ fn pachner_2d_k1() -> ExampleResult { println!(" New vertex: {:?}", flip_info.inserted_face_vertices); // Verify structural validity (always maintained) - dt.tds().is_valid()?; + dt.validate_structure()?; println!(" ✓ Structural invariants preserved"); // Apply inverse k=1 flip (remove vertex) @@ -392,17 +392,16 @@ fn pachner_3d_k1() -> ExampleResult { .ok_or(TopologyEditingExampleError::EmptyTriangulation { demo: "3D k=1 demo", })?; - let simplex = - dt.tds() - .simplex(simplex_key) - .ok_or(TopologyEditingExampleError::MissingSimplex { - demo: "3D k=1 demo", - })?; + let simplex = dt + .simplex(simplex_key) + .ok_or(TopologyEditingExampleError::MissingSimplex { + demo: "3D k=1 demo", + })?; let vertex_points: Vec> = simplex .vertices() .iter() .map(|vkey| { - dt.tds().vertex(*vkey).map(|vertex| *vertex.point()).ok_or( + dt.vertex(*vkey).map(|vertex| *vertex.point()).ok_or( TopologyEditingExampleError::MissingVertex { demo: "3D k=1 demo", vertex_key: *vkey, @@ -592,11 +591,7 @@ fn find_interior_facet( let Ok(facet_idx) = u8::try_from(facet_idx) else { continue; }; - return Ok(Some(FacetHandle::try_new( - dt.tds(), - simplex_key, - facet_idx, - )?)); + return Ok(Some(dt.facet_handle(simplex_key, facet_idx)?)); } } } @@ -616,7 +611,7 @@ fn find_roundtrip_k2_facet_3d(dt: &Dt3) -> ExampleResult> { let Ok(facet_idx) = u8::try_from(facet_idx) else { continue; }; - let facet = FacetHandle::try_new(dt.tds(), simplex_key, facet_idx)?; + let facet = dt.facet_handle(simplex_key, facet_idx)?; if dt.propose_pachner(PachnerMove::K2 { facet }).is_err() { continue; } @@ -654,7 +649,7 @@ fn find_flippable_ridge_3d(dt: &Dt3) -> ExampleResult> { let Ok(omit_b) = u8::try_from(j) else { continue; }; - let ridge = RidgeHandle::try_new(dt.tds(), simplex_key, omit_a, omit_b)?; + let ridge = dt.ridge_handle(simplex_key, omit_a, omit_b)?; let mut trial = dt.clone(); let Ok(proposal) = trial.propose_pachner(PachnerMove::K3 { ridge }) else { continue; @@ -685,7 +680,7 @@ fn inserted_edge_3d( }, ); }; - Ok(EdgeKey::try_new(dt.tds(), *a, *b)?) + Ok(dt.edge_key(*a, *b)?) } /// Parses the inserted face of a k=3 move into its inverse triangle candidate. diff --git a/semgrep.yaml b/semgrep.yaml index fed018a1..de639772 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -315,6 +315,46 @@ rules: ... } + - id: delaunay.rust.no-module-scope-cfg-test-use + languages: + - generic + severity: WARNING + message: "Move test-only imports into the test module instead of gating module-scope use items with #[cfg(test)]." + metadata: + category: maintainability + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + Module-scope `#[cfg(test)] use` items make production imports depend on + test-only compilation. Unit-test dependencies should live inside the + `#[cfg(test)] mod tests` block that uses them. + paths: + include: + - "/src/**/*.rs" + - "/tests/**/*.rs" + - "/benches/**/*.rs" + - "/examples/**/*.rs" + pattern-regex: '(?m)^[ \t]*#\[cfg\(test\)\][ \t]*\n[ \t]*use[ \t]+[^;]+;' + + - id: delaunay.rust.no-public-api-cfg-test-shim + languages: + - generic + severity: WARNING + message: "Do not expose public APIs through cfg(test); use a real feature gate or keep helpers inside mod tests." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + Public or owner-level APIs should not exist only to satisfy unit tests. + If downstream users may call the item, gate it on the real feature that + owns the workflow; otherwise keep test helpers inside `mod tests`. + paths: + include: + - "/src/**/*.rs" + - "/tests/**/*.rs" + - "/benches/**/*.rs" + - "/examples/**/*.rs" + pattern-regex: '(?ms)^[ \t]*#\[cfg\(any\([^\]\n]*\btest\b[^\]\n]*\)\)\][^\n]*\n(?:(?!^[ \t]*pub[ \t]+(?:use|(?:const[ \t]+)?fn)\b).*\n)*^[ \t]*pub[ \t]+(?:use|(?:const[ \t]+)?fn)\b' # yamllint disable-line rule:line-length + - id: delaunay.rust.no-deep-crate-paths-in-functions languages: - rust @@ -1094,6 +1134,135 @@ rules: - pattern-regex: '^\s*pub\s+mod\s+tds_snapshot\b' - pattern-regex: '^\s*pub\s+use\s+.*\btds_snapshot\b' + - id: delaunay.rust.no-public-tds-accessor-methods + languages: + - generic + severity: WARNING + message: "Keep tds() accessors crate-private; expose owner-scoped query methods instead." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + Triangulation and DelaunayTriangulation should not expose their storage + layer through public tds() accessors. Public callers should use + owner-scoped queries such as facet_incidence_index(), ridges(), + simplex_vertices(), uuid lookup methods, and validation/report methods. + paths: + include: + - "/src/**/*.rs" + - "/tests/semgrep/src/project_rules/**/*.rs" + pattern-regex: '^\s*pub\s+(?:const\s+)?fn\s+tds\s*(?:<[^>{}]*>)?\s*\(' + + - id: delaunay.rust.no-tds-accessor-in-doctests + languages: + - generic + severity: WARNING + message: "Doctests should use public owner queries instead of .tds()." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + Public Rust documentation should not teach callers to reach through a + triangulation owner into TDS storage. Use owner-scoped query and + validation APIs instead. + paths: + include: + - "/src/**/*.rs" + - "/tests/semgrep/src/project_rules/**/*.rs" + pattern-regex: '^\s*//[!/].*\.tds\s*\(' + + - id: delaunay.rust.no-tds-accessor-in-markdown-examples + languages: + - generic + severity: WARNING + message: "Markdown examples should use public owner queries instead of .tds()." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + README and docs examples form part of the public API contract. They + should model owner-scoped queries and validators rather than exposing + TDS storage details. + paths: + include: + - "/README.md" + - "/docs/**/*.md" + - "/tests/semgrep/docs/**/*.md" + exclude: + - "/docs/archive/**" + pattern-regex: '\.tds\s*\(' + + - id: delaunay.rust.no-as-triangulation-storage-reach-through + languages: + - generic + severity: WARNING + message: "Use owner-scoped query methods or crate-private owner helpers instead of as_triangulation().tds/kernel reach-through." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + `as_triangulation()` is a public owner view, not a storage escape hatch. + Code and examples should use public owner queries when available, or a + named crate-private `tds()`/`kernel()` helper for intentionally + low-level internals. + paths: + include: + - "/src/**/*.rs" + - "/tests/**/*.rs" + - "/benches/**/*.rs" + - "/examples/**/*.rs" + - "/README.md" + - "/docs/**/*.md" + exclude: + - "/docs/archive/**" + pattern-regex: 'as_triangulation\s*\(\s*\)\s*(?:\n\s*)?\.\s*(?:tds|kernel)\b' + + - id: delaunay.rust.no-as-triangulation-clone + languages: + - generic + severity: WARNING + message: "Use into_triangulation() when consuming a DelaunayTriangulation instead of cloning its borrowed as_triangulation() view." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + `as_triangulation()` is the borrowed view API. When a caller is done + with a DelaunayTriangulation and needs an owned Triangulation, moving + through `into_triangulation()` preserves the ownership intent and avoids + a large topology clone. + paths: + include: + - "/src/**/*.rs" + - "/tests/**/*.rs" + - "/benches/**/*.rs" + - "/examples/**/*.rs" + - "/README.md" + - "/docs/**/*.md" + exclude: + - "/docs/archive/**" + pattern-regex: 'as_triangulation\s*\(\s*\)\s*(?:\n\s*)?\.\s*clone\s*\(' + + - id: delaunay.rust.no-raw-tds-flip-predicate-verifier-outside-core-flips + languages: + - generic + severity: WARNING + message: "Use DelaunayTriangulation::verify_via_flip_predicates outside the low-level flip core." + metadata: + category: api + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + The raw TDS flip-predicate verifier pairs storage and kernels manually. + Ordinary validation should go through DelaunayTriangulation so topology, + kernel, and storage stay owned by one API boundary. + paths: + include: + - "/src/**/*.rs" + - "/benches/**/*.rs" + - "/tests/**/*.rs" + exclude: + - "/src/core/algorithms/flips.rs" + pattern-regex: '\bverify_tds_via_flip_predicates\s*\(' + - id: delaunay.rust.tds-serialize-must-use-snapshot languages: - generic @@ -1563,6 +1732,26 @@ rules: pattern-regex: >- (?ms)^\s*pub\s+fn\s+(?!validated_)[A-Za-z0-9_]+\s*\([^{};]*?\)\s*->\s*Result\s*<\s*[A-Za-z0-9_:<> ,]*Fixture[A-Za-z0-9_:<> ,]*\s*, + - id: delaunay.rust.benchmark-k2-facet-selection-requires-interior-neighbor-guard + languages: + - generic + severity: WARNING + message: "Check the k=2 candidate has an interior neighbor before collecting facet support points." + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/delaunay/issues/253" + rationale: >- + Boundary facets can share the same local support shape as interior k=2 + candidates. The benchmark candidate selector must skip one-sided facets + before collecting support points so it diagnoses the real public flip + precondition instead of selecting an invalid boundary candidate. + paths: + include: + - "/benches/common/flip_workflows.rs" + - "/tests/semgrep/src/project_rules/rust_style.rs" + pattern-regex: >- + (?ms)pub\s+fn\s+flippable_k2_facet[A-Za-z0-9_]*\b[^{]*\{(?:(?!facet_neighbor_key\s*\(\s*dt\s*,\s*facet\s*\)\?\s*\.is_none\s*\(\)).)*facet_support_points\s*\(\s*dt\s*,\s*facet\s*\)\? + - id: delaunay.rust.no-box-dyn-error-in-examples-benches languages: - rust diff --git a/src/bench_fixtures.rs b/src/bench_fixtures.rs index 9c48ff2f..62d14fa1 100644 --- a/src/bench_fixtures.rs +++ b/src/bench_fixtures.rs @@ -38,9 +38,9 @@ pub mod pl_manifold { } impl OversharedFacetOrphanCleanupFixture3d { - /// Returns the structurally coherent benchmark TDS. + /// Returns the structurally coherent TDS used as repair benchmark input. #[must_use] - pub const fn tds(&self) -> &Tds<(), (), 3> { + pub const fn repair_input_storage(&self) -> &Tds<(), (), 3> { &self.tds } @@ -64,9 +64,9 @@ pub mod pl_manifold { } impl TargetedTopologyRepairFixture { - /// Returns the structurally coherent benchmark TDS. + /// Returns the structurally coherent TDS used as repair benchmark input. #[must_use] - pub const fn tds(&self) -> &Tds<(), (), D> { + pub const fn repair_input_storage(&self) -> &Tds<(), (), D> { &self.tds } diff --git a/src/core/adjacency.rs b/src/core/adjacency.rs index ae5000c6..61797b14 100644 --- a/src/core/adjacency.rs +++ b/src/core/adjacency.rs @@ -17,7 +17,6 @@ use crate::core::collections::{FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, SmallB use crate::core::edge::EdgeKey; use crate::core::tds::incidence::VertexIncidenceIndex; use crate::core::tds::{SimplexKey, TdsError, VertexKey}; -use std::marker::PhantomData; use thiserror::Error; /// Errors that can occur while building optional topology indexes. @@ -98,8 +97,8 @@ pub struct EdgeIndex<'tds> { /// Number of unique edges in the triangulation snapshot. pub(in crate::core) edge_count: usize, - /// Ties this derived index to the borrowed source TDS snapshot. - pub(in crate::core) _tds: PhantomData<&'tds VertexIncidenceIndex>, + /// Borrowed canonical incidence relation that ties this index to the source TDS snapshot. + pub(in crate::core) _source_incidence: &'tds VertexIncidenceIndex, } /// Derived simplex→neighbor index for one triangulation snapshot. @@ -118,8 +117,8 @@ pub struct SimplexNeighborIndex<'tds> { pub(in crate::core) simplex_to_neighbors: FastHashMap>, - /// Ties this derived index to the borrowed source TDS snapshot. - pub(in crate::core) _tds: PhantomData<&'tds VertexIncidenceIndex>, + /// Borrowed canonical incidence relation that ties this index to the source TDS snapshot. + pub(in crate::core) _source_incidence: &'tds VertexIncidenceIndex, } /// Borrowed adjacency view for one triangulation snapshot. diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 1cd052a5..59f15f3f 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -60,7 +60,7 @@ use crate::geometry::traits::coordinate::{ CoordinateConversionError, CoordinateValidationError, CoordinateValues, }; use crate::topology::traits::global_topology_model::{ - GlobalTopologyModel, GlobalTopologyModelAdapter, + GlobalTopologyModel, GlobalTopologyModelAdapter, GlobalTopologyModelError, }; use crate::topology::traits::topological_space::GlobalTopology; use crate::validation::DelaunayTriangulationValidationError; @@ -156,8 +156,6 @@ where } let mut touched_simplices = SimplexKeyBuffer::new(); let mut touched_simplex_set = FastHashSet::::default(); - let mut flip_workspace = Tds::empty(); - let mut prefer_secondary = false; macro_rules! timed_step { @@ -181,7 +179,6 @@ where record_attempt_ridge, run_next_ridge_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -198,7 +195,6 @@ where record_attempt_edge, run_next_edge_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -216,7 +212,6 @@ where record_attempt_triangle, run_next_triangle_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -238,7 +233,6 @@ where record_attempt_facet, run_next_facet_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -258,7 +252,6 @@ where record_attempt_ridge, run_next_ridge_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -275,7 +268,6 @@ where record_attempt_edge, run_next_edge_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -293,7 +285,6 @@ where record_attempt_triangle, run_next_triangle_repair_step( tds, - &mut flip_workspace, kernel, &mut queues, &mut stats, @@ -391,20 +382,14 @@ where direction, orientation_policy, validation_scope, - None, ) } -/// Applies a bistellar flip with caller-owned rollback scratch storage. -/// -/// This preserves the same failure-atomic public flip contract as -/// [`apply_bistellar_flip_with_k`] while letting local repair loops reuse one -/// trial TDS allocation across many candidate flips. #[expect( clippy::too_many_arguments, - reason = "Flip mutation needs explicit move, cavity, policy, validation inputs, and scratch storage" + reason = "Raw flip mutation needs explicit move, cavity, policy, and validation inputs" )] -fn apply_bistellar_flip_with_k_in_workspace( +fn apply_bistellar_flip_with_k_raw( tds: &mut Tds, k_move: usize, removed_face_vertices: &[VertexKey], @@ -413,13 +398,22 @@ fn apply_bistellar_flip_with_k_in_workspace( direction: FlipDirection, orientation_policy: ReplacementOrientationPolicy, validation_scope: FlipValidationScope, - trial_workspace: &mut Tds, ) -> Result, FlipError> where U: DataType, V: DataType, { - apply_bistellar_flip_with_k_inner( + let PreparedFlip { + kind, + direction, + removed_simplices, + removed_face_vertices, + inserted_face_vertices, + new_simplex_vertices, + new_simplex_offsets, + external_facets, + removed_simplex_vertices, + } = prepare_bistellar_flip( tds, k_move, removed_face_vertices, @@ -427,9 +421,81 @@ where removed_simplices, direction, orientation_policy, + )?; + + let new_simplices = apply_prepared_flip_mutation( + tds, + new_simplex_vertices, + new_simplex_offsets, + &external_facets, + &removed_simplices, + k_move, + direction, validation_scope, - Some(trial_workspace), - ) + )?; + + Ok(AppliedFlip { + info: FlipInfo { + kind, + direction, + removed_simplices, + new_simplices, + removed_face_vertices, + inserted_face_vertices, + }, + removed_simplex_vertices, + }) +} + +/// Applies a generic k-move without rollback. +/// +/// The caller owns transaction rollback if this returns an error or if later +/// postconditions fail. +pub(crate) fn apply_bistellar_flip_raw( + tds: &mut Tds, + context: &FlipContext, +) -> Result, FlipError> +where + U: DataType, + V: DataType, +{ + Ok(apply_bistellar_flip_with_k_raw( + tds, + K_MOVE, + &context.removed_face_vertices, + &context.inserted_face_vertices, + &context.removed_simplices, + context.direction, + ReplacementOrientationPolicy::AllowSigned, + FlipValidationScope::FullTds, + )? + .info) +} + +/// Applies a runtime-k generic move without rollback. +/// +/// The caller owns transaction rollback if this returns an error or if later +/// postconditions fail. +pub(crate) fn apply_bistellar_flip_dynamic_raw( + tds: &mut Tds, + k_move: usize, + context: &FlipContextDyn, +) -> Result, FlipError> +where + U: DataType, + V: DataType, +{ + Ok(apply_bistellar_flip_with_k_raw( + tds, + k_move, + &context.removed_face_vertices, + &context.inserted_face_vertices, + &context.removed_simplices, + context.direction, + ReplacementOrientationPolicy::AllowSigned, + FlipValidationScope::FullTds, + )? + .info) } /// Builds and validates the replacement side of a bistellar flip without mutating storage. @@ -652,13 +718,12 @@ where /// Shared implementation for failure-atomic bistellar mutation. /// -/// The original TDS is mutated only after the trial TDS has been fully rewired -/// and locally validated. Passing `trial_workspace` lets hot repair paths reuse -/// rollback storage; leaving it `None` keeps the standalone API's independent -/// trial allocation behavior. +/// The original TDS is mutated inside the shared rollback transaction and is +/// committed only after the replacement cavity has been fully rewired and +/// locally validated. #[expect( clippy::too_many_arguments, - reason = "Flip mutation needs explicit move, cavity, policy, validation inputs, and optional scratch storage" + reason = "Flip mutation needs explicit move, cavity, policy, and validation inputs" )] fn apply_bistellar_flip_with_k_inner( tds: &mut Tds, @@ -669,7 +734,6 @@ fn apply_bistellar_flip_with_k_inner( direction: FlipDirection, orientation_policy: ReplacementOrientationPolicy, validation_scope: FlipValidationScope, - trial_workspace: Option<&mut Tds>, ) -> Result, FlipError> where U: DataType, @@ -695,83 +759,18 @@ where orientation_policy, )?; - let apply_to_trial = |trial: &mut Tds| -> Result { - let mut new_simplices = SimplexKeyBuffer::new(); - - for (vertices, periodic_offsets) in - new_simplex_vertices.into_iter().zip(new_simplex_offsets) - { - let mut simplex = Simplex::try_new(vertices)?; - if let Some(offsets) = periodic_offsets { - simplex.set_periodic_vertex_offsets(offsets)?; - } - let simplex_key = trial - .insert_simplex_with_mapping_prechecked_topology(simplex) - .map_err(|source| FlipMutationError::SimplexInsertion { - source: source.into(), - })?; - new_simplices.push(simplex_key); - } - - wire_cavity_neighbors( - trial, - &new_simplices, - external_facets.iter().copied(), - Some(&removed_simplices), - ) - .map_err(FlipNeighborWiringError::from)?; - - trial - .remove_simplices_by_keys(&removed_simplices) - .map_err(|source| FlipError::from(FlipMutationError::SimplexRemoval { source }))?; - - let validation_result = match validation_scope { - FlipValidationScope::FullTds => trial.is_valid().map_err(TdsValidationFailure::from), - FlipValidationScope::LocalCavity => validate_flip_trial_cavity( - trial, - &new_simplices, - &external_facets, - &removed_simplices, - ), - }; - validation_result.map_err(|source| { - FlipError::from(FlipMutationError::TrialValidation { - k_move, - direction, - source, - }) - })?; - - #[cfg(debug_assertions)] - { - // This is intentionally debug/test-only for the same reason as the - // pre-flip scan above: production validation already checks coherent - // orientation at explicit validation boundaries. - if !trial.is_coherently_oriented() { - return Err(FlipError::from( - FlipMutationError::CoherentOrientationViolation { - stage: FlipOrientationCheckStage::AfterTrialMutation, - k_move, - direction, - }, - )); - } - } - - Ok(new_simplices) - }; - - let new_simplices = if let Some(trial) = trial_workspace { - trial.clone_from_for_rollback(tds); - let new_simplices = apply_to_trial(trial)?; - std::mem::swap(tds, trial); - new_simplices - } else { - let mut trial = tds.clone_for_rollback(); - let new_simplices = apply_to_trial(&mut trial)?; - *tds = trial; - new_simplices - }; + let mut transaction = TdsRollbackTransaction::begin(tds); + let new_simplices = apply_prepared_flip_mutation( + transaction.tds_mut(), + new_simplex_vertices, + new_simplex_offsets, + &external_facets, + &removed_simplices, + k_move, + direction, + validation_scope, + )?; + transaction.commit(); Ok(AppliedFlip { info: FlipInfo { @@ -786,6 +785,88 @@ where }) } +/// Mutates an already-prepared flip cavity inside the caller's rollback window. +/// +/// This helper exists so public, transaction-backed flips and raw Pachner +/// primitives share the same insertion, neighbor wiring, removal, and +/// post-mutation validation sequence without each owning its own snapshot. +#[expect( + clippy::too_many_arguments, + reason = "Prepared flip mutation needs explicit replacement storage and validation policy" +)] +fn apply_prepared_flip_mutation( + tds: &mut Tds, + new_simplex_vertices: ReplacementSimplexVertices, + new_simplex_offsets: ReplacementPeriodicOffsets, + external_facets: &[FacetHandle], + removed_simplices: &SimplexKeyBuffer, + k_move: usize, + direction: FlipDirection, + validation_scope: FlipValidationScope, +) -> Result +where + U: DataType, + V: DataType, +{ + let mut new_simplices = SimplexKeyBuffer::new(); + + for (vertices, periodic_offsets) in new_simplex_vertices.into_iter().zip(new_simplex_offsets) { + let mut simplex = Simplex::try_new(vertices)?; + if let Some(offsets) = periodic_offsets { + simplex.set_periodic_vertex_offsets(offsets)?; + } + let simplex_key = tds + .insert_simplex_with_mapping_prechecked_topology(simplex) + .map_err(|source| FlipMutationError::SimplexInsertion { + source: source.into(), + })?; + new_simplices.push(simplex_key); + } + + wire_cavity_neighbors( + tds, + &new_simplices, + external_facets.iter().copied(), + Some(removed_simplices), + ) + .map_err(FlipNeighborWiringError::from)?; + + tds.remove_simplices_by_keys(removed_simplices) + .map_err(|source| FlipError::from(FlipMutationError::SimplexRemoval { source }))?; + + let validation_result = match validation_scope { + FlipValidationScope::FullTds => tds.is_valid().map_err(TdsValidationFailure::from), + FlipValidationScope::LocalCavity => { + validate_flip_trial_cavity(tds, &new_simplices, external_facets, removed_simplices) + } + }; + validation_result.map_err(|source| { + FlipError::from(FlipMutationError::TrialValidation { + k_move, + direction, + source, + }) + })?; + + #[cfg(debug_assertions)] + { + // This is intentionally debug/test-only for the same reason as the + // pre-flip scan above: production validation already checks coherent + // orientation at explicit validation boundaries. + if !tds.is_coherently_oriented() { + return Err(FlipError::from( + FlipMutationError::CoherentOrientationViolation { + stage: FlipOrientationCheckStage::AfterTrialMutation, + k_move, + direction, + }, + )); + } + } + + Ok(new_simplices) +} + /// Selects whether a flip is only topological or must preserve Delaunay geometry. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ReplacementOrientationPolicy { @@ -2322,63 +2403,6 @@ where ) } -/// Apply a generic k-move (no Delaunay check). -/// -/// # Errors -/// -/// Returns a [`FlipError`] if the flip would be degenerate, duplicate an existing simplex, -/// create non-manifold topology, if the incidence index references a missing simplex, -/// if predicate evaluation fails, or if underlying TDS mutations fail. -pub(crate) fn apply_bistellar_flip( - tds: &mut Tds, - context: &FlipContext, -) -> Result, FlipError> -where - U: DataType, - V: DataType, -{ - Ok(apply_bistellar_flip_with_k( - tds, - K_MOVE, - &context.removed_face_vertices, - &context.inserted_face_vertices, - &context.removed_simplices, - context.direction, - ReplacementOrientationPolicy::AllowSigned, - FlipValidationScope::FullTds, - )? - .info) -} - -/// Apply a generic k-move with runtime k (no Delaunay check). -/// -/// # Errors -/// -/// Returns a [`FlipError`] if the flip would be degenerate, duplicate an existing simplex, -/// create non-manifold topology, if the incidence index references a missing simplex, -/// if predicate evaluation fails, or if underlying TDS mutations fail. -pub(crate) fn apply_bistellar_flip_dynamic( - tds: &mut Tds, - k_move: usize, - context: &FlipContextDyn, -) -> Result, FlipError> -where - U: DataType, - V: DataType, -{ - Ok(apply_bistellar_flip_with_k( - tds, - k_move, - &context.removed_face_vertices, - &context.inserted_face_vertices, - &context.removed_simplices, - context.direction, - ReplacementOrientationPolicy::AllowSigned, - FlipValidationScope::FullTds, - )? - .info) -} - /// Validate a generic k-move without mutating the TDS. /// /// # Errors @@ -2453,47 +2477,19 @@ where ) } -/// Apply a k=2 Delaunay-repair move with reusable rollback storage. +/// Apply a k=3 Delaunay-repair move with positive replacement geometry. /// -/// This is the local-repair hot-path variant of [`apply_delaunay_flip_k2`]; it -/// preserves positive replacement geometry and failure atomicity while avoiding -/// a fresh whole-TDS allocation for each attempted flip. -fn apply_delaunay_flip_k2_in_workspace( - tds: &mut Tds, - context: &FlipContext, - trial_workspace: &mut Tds, -) -> Result, FlipError> -where - U: DataType, - V: DataType, -{ - apply_bistellar_flip_with_k_in_workspace( - tds, - 2, - &context.removed_face_vertices, - &context.inserted_face_vertices, - &context.removed_simplices, - context.direction, - ReplacementOrientationPolicy::RequirePositive, - FlipValidationScope::LocalCavity, - trial_workspace, - ) -} - -/// Apply a k=3 Delaunay-repair move with reusable rollback storage. -/// -/// This preserves positive replacement geometry and failure atomicity while -/// avoiding a fresh whole-TDS allocation for each attempted local-repair flip. -fn apply_delaunay_flip_k3_in_workspace( +/// This preserves positive replacement geometry and failure atomicity through +/// the shared TDS rollback transaction. +fn apply_delaunay_flip_k3( tds: &mut Tds, context: &FlipContext, - trial_workspace: &mut Tds, ) -> Result, FlipError> where U: DataType, V: DataType, { - apply_bistellar_flip_with_k_in_workspace( + apply_bistellar_flip_with_k( tds, 3, &context.removed_face_vertices, @@ -2502,26 +2498,24 @@ where context.direction, ReplacementOrientationPolicy::RequirePositive, FlipValidationScope::LocalCavity, - trial_workspace, ) } -/// Apply a dynamic-size Delaunay-repair move with reusable rollback storage. +/// Apply a dynamic-size Delaunay-repair move. /// /// This variant is used when the repair search cannot statically name `k`; it /// still routes through the same validated, failure-atomic bistellar mutation /// path as the dimension-specific helpers. -fn apply_delaunay_flip_dynamic_in_workspace( +fn apply_delaunay_flip_dynamic( tds: &mut Tds, k_move: usize, context: &FlipContextDyn, - trial_workspace: &mut Tds, ) -> Result, FlipError> where U: DataType, V: DataType, { - apply_bistellar_flip_with_k_in_workspace( + apply_bistellar_flip_with_k( tds, k_move, &context.removed_face_vertices, @@ -2530,7 +2524,6 @@ where context.direction, ReplacementOrientationPolicy::RequirePositive, FlipValidationScope::LocalCavity, - trial_workspace, ) } @@ -2570,9 +2563,9 @@ impl FlipDirection { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum FlipOrientationCheckStage { - /// Before applying the flip to the trial TDS. + /// Before applying the flip inside the rollback transaction. BeforeMutation, - /// After applying the flip to the trial TDS and before committing it. + /// After applying the flip inside the rollback transaction and before committing it. AfterTrialMutation, } /// Detect repeated flip signatures and abort on cycles. @@ -2850,13 +2843,22 @@ pub enum FlipPredicateError { source: CoordinateConversionError, }, /// A topology model failed to lift a periodic vertex for predicate evaluation. - #[error("failed to lift vertex {vertex_key:?} for periodic predicate: {details}")] + #[error("failed to lift vertex {vertex_key:?} for periodic predicate: {source}")] PeriodicVertexLift { /// Vertex being lifted. vertex_key: VertexKey, - /// Underlying topology-model error, captured in display form because it - /// may contain floating-point values and therefore is not `Eq`. - details: String, + /// Underlying topology-model error. + #[source] + source: GlobalTopologyModelError, + }, + /// A lifted periodic vertex produced invalid point coordinates. + #[error("lifted periodic vertex {vertex_key:?} produced invalid coordinates: {source}")] + PeriodicLiftedPointValidation { + /// Vertex whose lifted coordinates were invalid. + vertex_key: VertexKey, + /// Coordinate validation failure for the lifted point. + #[source] + source: CoordinateValidationError, }, } @@ -3194,6 +3196,12 @@ pub enum FlipFailureKind { /// Simplex creation failed. #[error("simplex creation")] SimplexCreation, + /// Flip transaction could not repair post-mutation orientation invariants. + #[error("postcondition orientation repair")] + PostconditionRepair, + /// Flip transaction failed embedded-geometry validation after mutation. + #[error("embedding validation")] + EmbeddingValidation, /// Neighbor wiring failed. #[error("neighbor wiring")] NeighborWiring, @@ -3714,9 +3722,9 @@ pub enum FlipMutationError { #[source] source: TdsMutationError, }, - /// Trial TDS validation failed before committing a flip. + /// Transactional TDS validation failed before committing a flip. #[error( - "trial TDS validation failed after bistellar flip (k={k_move}, direction={direction:?}): {source}" + "transactional TDS validation failed after bistellar flip (k={k_move}, direction={direction:?}): {source}" )] TrialValidation { /// k for the attempted move. @@ -3727,13 +3735,13 @@ pub enum FlipMutationError { #[source] source: TdsValidationFailure, }, - /// Trial TDS coherent-orientation validation failed before committing a flip. + /// Transactional TDS coherent-orientation validation failed before committing a flip. /// /// This diagnostic is debug/test-only in the flip hot path because it scans /// global TDS orientation state. Release-mode callers should use explicit /// validation boundaries when they need this invariant checked. #[error( - "trial TDS coherent orientation invariant violated during {stage:?} (k={k_move}, direction={direction:?})" + "transactional TDS coherent orientation invariant violated during {stage:?} (k={k_move}, direction={direction:?})" )] CoherentOrientationViolation { /// Stage where the invariant was checked. @@ -4121,6 +4129,20 @@ pub enum FlipError { /// Simplex creation failed. #[error(transparent)] SimplexCreation(#[from] Box), + /// Flip transaction could not repair post-mutation orientation invariants. + #[error("Flip postcondition orientation repair failed: {source}")] + PostconditionRepair { + /// Structured orientation-repair failure. + #[source] + source: Box, + }, + /// Flip transaction failed embedded-geometry validation after mutation. + #[error("Flip postcondition embedding validation failed: {source}")] + EmbeddingValidation { + /// Structured Level 4 embedding validation error. + #[source] + source: Box, + }, /// Neighbor wiring failed during flip application. #[error("Neighbor wiring failed: {reason}")] NeighborWiring { @@ -4239,6 +4261,8 @@ impl From<&FlipError> for FlipFailureKind { FlipError::InsertedSimplexAlreadyExists { .. } => Self::InsertedSimplexAlreadyExists, FlipError::FacetIteration { .. } => Self::FacetIteration, FlipError::SimplexCreation(_) => Self::SimplexCreation, + FlipError::PostconditionRepair { .. } => Self::PostconditionRepair, + FlipError::EmbeddingValidation { .. } => Self::EmbeddingValidation, FlipError::NeighborWiring { reason } => match reason.as_ref() { FlipNeighborWiringError::TopologyValidation { .. } | FlipNeighborWiringError::DelaunayValidation { .. } @@ -4551,7 +4575,7 @@ impl TryFrom<[VertexKey; 3]> for TriangleHandle { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; -/// let handle = RidgeHandle::try_new(dt.tds(), simplex_key, 2, 0)?; +/// let handle: RidgeHandle = dt.ridge_handle(simplex_key, 2, 0)?; /// assert_eq!(handle.omit_a(), 0); /// assert_eq!(handle.omit_b(), 2); /// # Ok(()) @@ -6201,24 +6225,6 @@ where ) } -/// Apply a k=2 bistellar flip (no Delaunay check). -/// -/// # Errors -/// -/// Returns a [`FlipError`] if the flip would be degenerate, duplicate an existing simplex, -/// create non-manifold topology, if predicate evaluation fails, or if underlying TDS -/// mutations fail. -pub(crate) fn apply_bistellar_flip_k2( - tds: &mut Tds, - context: &FlipContext, -) -> Result, FlipError> -where - U: DataType, - V: DataType, -{ - apply_bistellar_flip::(tds, context) -} - /// Validate a k=2 bistellar flip without mutating the TDS. /// /// # Errors @@ -6586,24 +6592,6 @@ where Ok(false) } -/// Apply a k=3 bistellar flip (no Delaunay check). -/// -/// # Errors -/// -/// Returns a [`FlipError`] if the flip would be degenerate, duplicate an existing simplex, -/// create non-manifold topology, if predicate evaluation fails, or if underlying TDS -/// mutations fail. -pub(crate) fn apply_bistellar_flip_k3( - tds: &mut Tds, - context: &FlipContext, -) -> Result, FlipError> -where - U: DataType, - V: DataType, -{ - apply_bistellar_flip::(tds, context) -} - /// Validate a k=3 bistellar flip without mutating the TDS. /// /// # Errors @@ -6687,13 +6675,11 @@ where }) } -/// Apply a forward k=1 move (simplex split) by inserting a new vertex. -/// -/// # Errors +/// Apply a forward k=1 move without rollback. /// -/// Returns a [`FlipError`] if the simplex is missing, the vertex cannot be inserted, -/// or the flip would be degenerate. -pub(crate) fn apply_bistellar_flip_k1( +/// The caller owns transaction rollback if this returns an error or if later +/// postconditions fail. +pub(crate) fn apply_bistellar_flip_k1_raw( tds: &mut Tds, simplex_key: SimplexKey, vertex: Vertex, @@ -6711,32 +6697,15 @@ where source: source.into(), } })?; - - let context = match build_k1_forward_context_from_simplex(tds, simplex_key, vertex_key) { - Ok(ctx) => ctx, - Err(e) => { - // Remove the just-inserted vertex to avoid leaving an orphan. - let _ = tds.remove_vertex(vertex_key); - return Err(e); - } - }; - - let result = apply_bistellar_flip::(tds, &context); - - if result.is_err() { - let _ = tds.remove_vertex(vertex_key); - } - - result + let context = build_k1_forward_context_from_simplex(tds, simplex_key, vertex_key)?; + apply_bistellar_flip_raw::(tds, &context) } -/// Apply an inverse k=1 move (vertex collapse) by removing a vertex whose star -/// is a simplex. +/// Apply an inverse k=1 move without rollback. /// -/// # Errors -/// -/// Returns a [`FlipError`] if the vertex star is invalid or the flip would be degenerate. -pub(crate) fn apply_bistellar_flip_k1_inverse( +/// The caller owns transaction rollback if this returns an error or if later +/// postconditions fail. +pub(crate) fn apply_bistellar_flip_k1_inverse_raw( tds: &mut Tds, vertex_key: VertexKey, ) -> Result, FlipError> @@ -6749,8 +6718,7 @@ where } let context = build_k1_inverse_context(tds, vertex_key)?; - let info = apply_bistellar_flip_dynamic(tds, D + 1, &context)?; - + let info = apply_bistellar_flip_dynamic_raw(tds, D + 1, &context)?; let _ = tds.remove_vertex(vertex_key); Ok(info) @@ -7390,59 +7358,24 @@ where } } -/// Verify the Delaunay property via local flip predicates (fast O(simplices) validation). +/// Crate-internal TDS verifier for the Delaunay property via local flip predicates. /// -/// This function checks whether the triangulation satisfies the Delaunay property by testing -/// all possible flip configurations (k=2 facets, k=3 ridges, and their inverses). If no -/// violations are detected via these local checks, the triangulation is Delaunay. +/// Public callers should use +/// [`DelaunayTriangulation::verify_via_flip_predicates`](crate::DelaunayTriangulation::verify_via_flip_predicates) +/// so the kernel and global topology come from the owning triangulation rather +/// than being paired manually with a raw TDS. /// -/// This is **much faster** than the naive O(simplices × vertices) empty-circumsphere check, -/// while being equally correct due to the completeness of bistellar flip predicates. -/// -/// # Performance -/// -/// - **Complexity**: O(simplices) — tests only local flip predicates -/// - **Speedup**: ~40-100x faster than brute-force for typical triangulations -/// - **Use case**: Ideal for property-based testing with many iterations +/// This helper remains available inside the crate for low-level repair fixtures +/// that intentionally construct temporary TDS states before wrapping them in an +/// owner. /// /// # Errors /// /// Returns [`DelaunayRepairError::PostconditionFailed`] if any flip predicate detects /// a Delaunay violation, or [`DelaunayRepairError::VerificationFailed`] if a /// local predicate cannot be evaluated. -/// -/// # Examples -/// -/// ``` -/// use delaunay::prelude::*; -/// use delaunay::prelude::repair::verify_delaunay_via_flip_predicates; -/// use delaunay::prelude::geometry::AdaptiveKernel; -/// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Source(#[from] DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { -/// let vertices = vec![ -/// delaunay::vertex![0.0, 0.0, 0.0]?, -/// delaunay::vertex![1.0, 0.0, 0.0]?, -/// delaunay::vertex![0.0, 1.0, 0.0]?, -/// delaunay::vertex![0.0, 0.0, 1.0]?, -/// ]; -/// -/// let dt: DelaunayTriangulation<_, (), (), 3> = -/// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let kernel = AdaptiveKernel::::new(); -/// -/// // Fast O(N) verification -/// assert!(verify_delaunay_via_flip_predicates(dt.tds(), &kernel).is_ok()); -/// # Ok(()) -/// # } -/// ``` -pub fn verify_delaunay_via_flip_predicates( +#[cfg(test)] +pub(crate) fn verify_tds_via_flip_predicates( tds: &Tds, kernel: &K, ) -> Result<(), DelaunayRepairError> @@ -7454,49 +7387,19 @@ where verify_delaunay_with_topology(tds, kernel, GlobalTopology::DEFAULT) } -/// Verify the Delaunay property via local flip predicates for a full triangulation. +/// Crate-internal triangulation verifier for the Delaunay property via local flip predicates. /// -/// This is the preferred Level 5 validation entry point because it carries the -/// triangulation's global topology alongside the TDS. For periodic topologies -/// (e.g. toroidal), insphere predicates are evaluated in lifted coordinates so -/// that facets spanning periodic boundaries are not reported as false violations. +/// Public Delaunay owners expose this as +/// [`DelaunayTriangulation::verify_via_flip_predicates`](crate::DelaunayTriangulation::verify_via_flip_predicates). +/// Keeping this helper crate-private prevents callers from pairing arbitrary +/// triangulations with a Delaunay-only API boundary. /// /// # Errors /// /// Returns [`DelaunayRepairError::PostconditionFailed`] if any flip predicate detects /// a Delaunay violation, or [`DelaunayRepairError::VerificationFailed`] if /// verification cannot evaluate the local predicates. -/// -/// # Examples -/// -/// ``` -/// use delaunay::prelude::*; -/// use delaunay::prelude::repair::verify_delaunay_for_triangulation; -/// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Source(#[from] DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { -/// let vertices = vec![ -/// delaunay::vertex![0.0, 0.0, 0.0]?, -/// delaunay::vertex![1.0, 0.0, 0.0]?, -/// delaunay::vertex![0.0, 1.0, 0.0]?, -/// delaunay::vertex![0.0, 0.0, 1.0]?, -/// ]; -/// -/// let dt: DelaunayTriangulation<_, (), (), 3> = -/// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// -/// // Topology-aware O(N) verification -/// assert!(verify_delaunay_for_triangulation(dt.as_triangulation()).is_ok()); -/// # Ok(()) -/// # } -/// ``` -pub fn verify_delaunay_for_triangulation( +pub(crate) fn verify_triangulation_via_flip_predicates( triangulation: &Triangulation, ) -> Result<(), DelaunayRepairError> where @@ -8956,7 +8859,6 @@ where )] fn run_next_ridge_repair_step( tds: &mut Tds, - trial_workspace: &mut Tds, kernel: &K, queues: &mut RepairQueues, stats: &mut DelaunayRepairStats, @@ -9099,7 +9001,7 @@ where ); } }; - let applied = match apply_delaunay_flip_k3_in_workspace(tds, &context, trial_workspace) { + let applied = match apply_delaunay_flip_k3(tds, &context) { Ok(applied) => applied, Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { @@ -9156,7 +9058,6 @@ where )] fn run_next_edge_repair_step( tds: &mut Tds, - trial_workspace: &mut Tds, kernel: &K, queues: &mut RepairQueues, stats: &mut DelaunayRepairStats, @@ -9293,30 +9194,29 @@ where ); } }; - let applied = - match apply_delaunay_flip_dynamic_in_workspace(tds, kind.k(), &context, trial_workspace) { - Ok(applied) => applied, - Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { - diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { - location: RepairSkipLocation::Edge(edge), - removed_face: vertex_key_list(&context.removed_face_vertices), - inserted_face: vertex_key_list(&context.inserted_face_vertices), - }); - log_apply_skip(&err); - return Ok(true); - } - Err( - err @ (FlipError::DegenerateSimplex - | FlipError::NegativeOrientation { .. } - | FlipError::DuplicateSimplex - | FlipError::NonManifoldFacet - | FlipError::SimplexCreation(_)), - ) => { - log_apply_skip(&err); - return Ok(true); - } - Err(e) => return Err(e.into()), - }; + let applied = match apply_delaunay_flip_dynamic(tds, kind.k(), &context) { + Ok(applied) => applied, + Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { + diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { + location: RepairSkipLocation::Edge(edge), + removed_face: vertex_key_list(&context.removed_face_vertices), + inserted_face: vertex_key_list(&context.inserted_face_vertices), + }); + log_apply_skip(&err); + return Ok(true); + } + Err( + err @ (FlipError::DegenerateSimplex + | FlipError::NegativeOrientation { .. } + | FlipError::DuplicateSimplex + | FlipError::NonManifoldFacet + | FlipError::SimplexCreation(_)), + ) => { + log_apply_skip(&err); + return Ok(true); + } + Err(e) => return Err(e.into()), + }; *last_applied_flip = Some(LastAppliedFlip::from_applied_flip(&applied)); let info = applied.info; if repair_trace_enabled() { @@ -9351,7 +9251,6 @@ where )] fn run_next_triangle_repair_step( tds: &mut Tds, - trial_workspace: &mut Tds, kernel: &K, queues: &mut RepairQueues, stats: &mut DelaunayRepairStats, @@ -9479,30 +9378,29 @@ where ); } }; - let applied = - match apply_delaunay_flip_dynamic_in_workspace(tds, kind.k(), &context, trial_workspace) { - Ok(applied) => applied, - Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { - diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { - location: RepairSkipLocation::Triangle(triangle), - removed_face: vertex_key_list(&context.removed_face_vertices), - inserted_face: vertex_key_list(&context.inserted_face_vertices), - }); - log_apply_skip(&err); - return Ok(true); - } - Err( - err @ (FlipError::DegenerateSimplex - | FlipError::NegativeOrientation { .. } - | FlipError::DuplicateSimplex - | FlipError::NonManifoldFacet - | FlipError::SimplexCreation(_)), - ) => { - log_apply_skip(&err); - return Ok(true); - } - Err(e) => return Err(e.into()), - }; + let applied = match apply_delaunay_flip_dynamic(tds, kind.k(), &context) { + Ok(applied) => applied, + Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { + diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { + location: RepairSkipLocation::Triangle(triangle), + removed_face: vertex_key_list(&context.removed_face_vertices), + inserted_face: vertex_key_list(&context.inserted_face_vertices), + }); + log_apply_skip(&err); + return Ok(true); + } + Err( + err @ (FlipError::DegenerateSimplex + | FlipError::NegativeOrientation { .. } + | FlipError::DuplicateSimplex + | FlipError::NonManifoldFacet + | FlipError::SimplexCreation(_)), + ) => { + log_apply_skip(&err); + return Ok(true); + } + Err(e) => return Err(e.into()), + }; *last_applied_flip = Some(LastAppliedFlip::from_applied_flip(&applied)); let info = applied.info; if repair_trace_enabled() { @@ -9537,7 +9435,6 @@ where )] fn run_next_facet_repair_step( tds: &mut Tds, - trial_workspace: &mut Tds, kernel: &K, queues: &mut RepairQueues, stats: &mut DelaunayRepairStats, @@ -9670,7 +9567,7 @@ where ); } }; - let applied = match apply_delaunay_flip_k2_in_workspace(tds, &context, trial_workspace) { + let applied = match apply_delaunay_flip_k2(tds, &context) { Ok(applied) => applied, Err(err) if let FlipError::InsertedSimplexAlreadyExists { .. } = &err => { diagnostics.record_inserted_simplex_skip(InsertedSimplexSkipSample { @@ -10105,16 +10002,9 @@ where .ok_or(FlipError::MissingVertex { vertex_key })?; let lifted_coords = topology_model .lift_for_orientation(*vertex.point().coords(), periodic_offset) - .map_err(|source| FlipPredicateError::PeriodicVertexLift { - vertex_key, - details: source.to_string(), - })?; + .map_err(|source| FlipPredicateError::PeriodicVertexLift { vertex_key, source })?; Point::try_new(lifted_coords).map_err(|source| { - FlipPredicateError::PeriodicVertexLift { - vertex_key, - details: source.to_string(), - } - .into() + FlipPredicateError::PeriodicLiftedPointValidation { vertex_key, source }.into() }) } @@ -11594,7 +11484,7 @@ mod tests { ) .unwrap(); - let info = apply_bistellar_flip(&mut tds, &ctx).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &ctx).unwrap(); assert!(!tds.contains_simplex(simplex_cavity_left)); assert!(!tds.contains_simplex(simplex_cavity_right)); @@ -12270,7 +12160,7 @@ mod tests { .any(|simplex_key| simplex_key == simplex_around_edge_2) ); - let info = apply_bistellar_flip(&mut tds, &ctx).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &ctx).unwrap(); // Removed simplices should be gone. assert!(!tds.contains_simplex(simplex_around_edge_0)); @@ -12519,107 +12409,11 @@ mod tests { simplex_neighbors } - fn snapshot_incidence(tds: &Tds<(), (), D>) -> Vec<(Uuid, Option)> { - let mut incident_simplices: Vec<(Uuid, Option)> = tds - .vertices() - .map(|(_, vertex)| { - ( - vertex.uuid(), - vertex - .incident_simplex() - .and_then(|simplex_key| tds.simplex(simplex_key).map(Simplex::uuid)), - ) - }) - .collect(); - incident_simplices.sort(); - incident_simplices - } - fn assert_same_vertex_simplex_topology(actual: &TopologySnapshot, expected: &TopologySnapshot) { assert_eq!(actual.vertices, expected.vertices); assert_eq!(actual.simplex_vertices, expected.simplex_vertices); } - fn insert_translated_simplex( - tds: &mut Tds<(), (), D>, - offset: f64, - ) -> (Vec, SimplexKey) { - let mut vertices = Vec::with_capacity(D + 1); - vertices.push( - tds.insert_vertex_with_mapping(vertex!([offset; D]).unwrap()) - .unwrap(), - ); - - for axis in 0..D { - let mut coords = [offset; D]; - coords[axis] += 1.0; - vertices.push( - tds.insert_vertex_with_mapping(vertex!(coords).unwrap()) - .unwrap(), - ); - } - - let simplex_key = tds - .insert_simplex_with_mapping( - Simplex::try_new_with_data(vertices.clone(), None).unwrap(), - ) - .unwrap(); - (vertices, simplex_key) - } - - fn test_flip_trial_validation_rollback_for_dim() { - let mut tds: Tds<(), (), D> = Tds::empty(); - let (_first_vertices, first_simplex) = insert_translated_simplex(&mut tds, 0.0); - let (_second_vertices, second_simplex) = insert_translated_simplex(&mut tds, 10.0); - repair_neighbor_pointers(&mut tds).unwrap(); - tds.assign_incident_simplices().unwrap(); - - let isolated_vertex = tds - .insert_vertex_with_mapping(vertex!([20.0; D]).unwrap()) - .unwrap(); - tds.vertex_mut(isolated_vertex) - .unwrap() - .set_incident_simplex(Some(second_simplex)); - - let before = snapshot_topology(&tds); - let before_incidence = snapshot_incidence(&tds); - let denominator = f64::from(u32::try_from(D + 2).unwrap()); - let new_vertex = vertex!([1.0 / denominator; D]).unwrap(); - - let result = apply_bistellar_flip_k1(&mut tds, first_simplex, new_vertex); - match result { - Err(FlipError::TdsMutation { reason }) - if matches!(reason.as_ref(), FlipMutationError::TrialValidation { .. }) => {} - other => panic!("expected FlipMutationError::TrialValidation, got {other:?}"), - } - - assert_eq!( - snapshot_topology(&tds), - before, - "trial.is_valid() failure must leave the original TDS unchanged" - ); - assert_eq!( - snapshot_incidence(&tds), - before_incidence, - "trial.is_valid() failure must leave incident_simplex pointers unchanged" - ); - } - - macro_rules! gen_trial_validation_rollback_tests { - ($($dim:literal),+ $(,)?) => { - pastey::paste! { - $( - #[test] - fn []() { - test_flip_trial_validation_rollback_for_dim::<$dim>(); - } - )+ - } - }; - } - - gen_trial_validation_rollback_tests!(2, 3, 4, 5); - #[test] fn test_flip_trial_validation_rejects_unassigned_neighbor_slot() { let mut tds: Tds<(), (), 2> = Tds::empty(); @@ -12717,7 +12511,7 @@ mod tests { ); let context = build_k2_flip_context(&tds, facet) .map_err(|err| TestCaseError::fail(format!("k=2 context build failed: {err:?}")))?; - let info = apply_bistellar_flip_k2(&mut tds, &context) + let info = apply_bistellar_flip_raw(&mut tds, &context) .map_err(|err| TestCaseError::fail(format!("k=2 flip failed: {err:?}")))?; tds.is_valid() .map_err(|err| TestCaseError::fail(format!("post k=2 TDS invalid: {err:?}")))?; @@ -12751,7 +12545,7 @@ mod tests { let context_back = build_k2_flip_context(&tds, facet).map_err(|err| { TestCaseError::fail(format!("inverse k=2 context build failed: {err:?}")) })?; - apply_bistellar_flip_k2(&mut tds, &context_back) + apply_bistellar_flip_raw(&mut tds, &context_back) .map_err(|err| TestCaseError::fail(format!("inverse k=2 flip failed: {err:?}")))?; } else { let inserted = inserted_face_vertices(&info, 2)?; @@ -12766,7 +12560,7 @@ mod tests { let context_back = build_k2_flip_context_from_edge(&tds, edge).map_err(|err| { TestCaseError::fail(format!("inverse k=2 context build failed: {err:?}")) })?; - apply_bistellar_flip_dynamic(&mut tds, D, &context_back) + apply_bistellar_flip_dynamic_raw(&mut tds, D, &context_back) .map_err(|err| TestCaseError::fail(format!("inverse k=2 flip failed: {err:?}")))?; } @@ -12867,7 +12661,7 @@ mod tests { ); let context = build_k3_flip_context(&tds, ridge) .map_err(|err| TestCaseError::fail(format!("k=3 context build failed: {err:?}")))?; - let info = apply_bistellar_flip_k3(&mut tds, &context) + let info = apply_bistellar_flip_raw(&mut tds, &context) .map_err(|err| TestCaseError::fail(format!("k=3 flip failed: {err:?}")))?; tds.is_valid() .map_err(|err| TestCaseError::fail(format!("post k=3 TDS invalid: {err:?}")))?; @@ -12904,7 +12698,7 @@ mod tests { let context_back = build_k2_flip_context(&tds, facet).map_err(|err| { TestCaseError::fail(format!("inverse k=3 context build failed: {err:?}")) })?; - apply_bistellar_flip_k2(&mut tds, &context_back) + apply_bistellar_flip_raw(&mut tds, &context_back) .map_err(|err| TestCaseError::fail(format!("inverse k=3 flip failed: {err:?}")))?; } else { let inserted = inserted_face_vertices(&info, 3)?; @@ -12922,7 +12716,7 @@ mod tests { build_k3_flip_context_from_triangle(&tds, triangle).map_err(|err| { TestCaseError::fail(format!("inverse k=3 context build failed: {err:?}")) })?; - apply_bistellar_flip_dynamic(&mut tds, ridge_vertex_count, &context_back) + apply_bistellar_flip_dynamic_raw(&mut tds, ridge_vertex_count, &context_back) .map_err(|err| TestCaseError::fail(format!("inverse k=3 flip failed: {err:?}")))?; } @@ -13027,13 +12821,13 @@ mod tests { let new_vertex = vertex!([0.1; $dim]).unwrap(); let new_uuid = new_vertex.uuid(); - let _info = apply_bistellar_flip_k1(&mut tds, simplex_key, new_vertex) + let _info = apply_bistellar_flip_k1_raw(&mut tds, simplex_key, new_vertex) .unwrap(); assert!(tds.is_valid().is_ok()); let new_key = tds.vertex_key_from_uuid(&new_uuid).unwrap(); let _info_back = - apply_bistellar_flip_k1_inverse(&mut tds, new_key).unwrap(); + apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert!(tds.is_valid().is_ok()); assert_eq!(snapshot_topology(&tds), before); @@ -13080,7 +12874,7 @@ mod tests { let facet = FacetHandle::from_validated(simplex_a, u8::try_from($dim).unwrap()); let context = build_k2_flip_context(&tds, facet).unwrap(); - let info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert!(tds.is_valid().is_ok()); if $dim == 2 { @@ -13104,12 +12898,12 @@ mod tests { let facet = inverse_facet.expect("inverse k=2 facet not found"); let context_back = build_k2_flip_context(&tds, facet).unwrap(); let _info_back = - apply_bistellar_flip_k2(&mut tds, &context_back).unwrap(); + apply_bistellar_flip_raw(&mut tds, &context_back).unwrap(); } else { let edge = EdgeKey::from_validated_endpoints(opposite_a, opposite_b); let context_back = build_k2_flip_context_from_edge(&tds, edge).unwrap(); let _info_back = - apply_bistellar_flip_dynamic(&mut tds, $dim, &context_back) + apply_bistellar_flip_dynamic_raw(&mut tds, $dim, &context_back) .unwrap(); } @@ -13175,7 +12969,7 @@ mod tests { u8::try_from($dim).unwrap(), ); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert!(tds.is_valid().is_ok()); if $dim == 3 { @@ -13202,12 +12996,12 @@ mod tests { let facet = inverse_facet.expect("inverse k=3 facet not found"); let context_back = build_k2_flip_context(&tds, facet).unwrap(); let _info_back = - apply_bistellar_flip_k2(&mut tds, &context_back).unwrap(); + apply_bistellar_flip_raw(&mut tds, &context_back).unwrap(); } else { let triangle = TriangleHandle::try_new(a, b, c).unwrap(); let context_back = build_k3_flip_context_from_triangle(&tds, triangle).unwrap(); - let _info_back = apply_bistellar_flip_dynamic( + let _info_back = apply_bistellar_flip_dynamic_raw( &mut tds, $dim - 1, &context_back, @@ -13288,7 +13082,7 @@ mod tests { }; assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, 0, &valid_shape), + apply_bistellar_flip_dynamic_raw(&mut tds, 0, &valid_shape), Err(FlipError::InvalidFlipContext { reason }) if matches!( reason.as_ref(), @@ -13299,7 +13093,7 @@ mod tests { ) ); assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, D + 2, &valid_shape), + apply_bistellar_flip_dynamic_raw(&mut tds, D + 2, &valid_shape), Err(FlipError::InvalidFlipContext { reason }) if matches!( reason.as_ref(), @@ -13315,7 +13109,7 @@ mod tests { ..valid_shape.clone() }; assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, 2, &wrong_removed_face), + apply_bistellar_flip_dynamic_raw(&mut tds, 2, &wrong_removed_face), Err(FlipError::InvalidFlipContext { reason }) if matches!( reason.as_ref(), @@ -13331,7 +13125,7 @@ mod tests { ..valid_shape.clone() }; assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, 2, &wrong_inserted_face), + apply_bistellar_flip_dynamic_raw(&mut tds, 2, &wrong_inserted_face), Err(FlipError::InvalidFlipContext { reason }) if matches!( reason.as_ref(), @@ -13348,7 +13142,7 @@ mod tests { ..valid_shape.clone() }; assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, 2, &wrong_removed_simplices), + apply_bistellar_flip_dynamic_raw(&mut tds, 2, &wrong_removed_simplices), Err(FlipError::InvalidFlipContext { reason }) if matches!( reason.as_ref(), @@ -13364,7 +13158,7 @@ mod tests { ..valid_shape }; assert_matches!( - apply_bistellar_flip_dynamic(&mut tds, 2, &overlapping_faces), + apply_bistellar_flip_dynamic_raw(&mut tds, 2, &overlapping_faces), Err(FlipError::InvalidFlipContext { reason }) if matches!(reason.as_ref(), FlipContextError::OverlappingFaces) ); @@ -13427,7 +13221,7 @@ mod tests { .map(SmallBuffer::len), Some(2) ); - let info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.removed_simplices.len(), 2); assert_eq!(info.new_simplices.len(), 2); @@ -13480,7 +13274,7 @@ mod tests { let context = build_k2_flip_context(&tds, facet).unwrap(); let feasibility = validate_bistellar_flip_k2(&tds, &context); assert_matches!(feasibility, Err(FlipError::DuplicateSimplex)); - let result = apply_bistellar_flip_k2(&mut tds, &context); + let result = apply_bistellar_flip_raw(&mut tds, &context); assert_matches!(result, Err(FlipError::DuplicateSimplex)); assert!(tds.is_valid().is_ok()); @@ -13550,7 +13344,7 @@ mod tests { feasibility, Err(FlipError::InsertedSimplexAlreadyExists { .. }) ); - let result = apply_bistellar_flip_k2(&mut tds, &ctx); + let result = apply_bistellar_flip_raw(&mut tds, &ctx); assert_matches!(result, Err(FlipError::InsertedSimplexAlreadyExists { .. })); assert!(tds.is_valid().is_ok()); @@ -13600,7 +13394,7 @@ mod tests { let context = build_k2_flip_context(&tds, facet).unwrap(); let feasibility = validate_bistellar_flip_k2(&tds, &context); assert_matches!(feasibility, Err(FlipError::NonManifoldFacet)); - let result = apply_bistellar_flip_k2(&mut tds, &context); + let result = apply_bistellar_flip_raw(&mut tds, &context); assert_matches!(result, Err(FlipError::NonManifoldFacet)); assert!(tds.is_valid().is_ok()); @@ -13641,7 +13435,7 @@ mod tests { let facet = FacetHandle::from_validated(c1, 3); // facet opposite vertex d (ABC) let context = build_k2_flip_context(&tds, facet).unwrap(); - let info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.new_simplices.len(), 3); assert!(tds.is_valid().is_ok()); @@ -13687,7 +13481,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 2, 3); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.kind, BistellarFlipKind::k3(3)); assert_eq!(info.removed_simplices.len(), 3); @@ -13738,7 +13532,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 3, 4); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.kind, BistellarFlipKind::k3(4)); assert_eq!(info.removed_simplices.len(), 3); @@ -13792,7 +13586,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 4, 5); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.kind, BistellarFlipKind::k3(5)); assert_eq!(info.removed_simplices.len(), 3); @@ -13981,32 +13775,6 @@ mod tests { ); } - #[test] - fn test_flip_k1_degenerate_insert_rejected() { - init_tracing(); - let mut tds: Tds<(), (), 2> = Tds::empty(); - let a = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0]).unwrap()) - .unwrap(); - let b = tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0]).unwrap()) - .unwrap(); - let c = tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0]).unwrap()) - .unwrap(); - let simplex_key = tds - .insert_simplex_with_mapping(Simplex::try_new_with_data(vec![a, b, c], None).unwrap()) - .unwrap(); - - let before = snapshot_topology(&tds); - let err = apply_bistellar_flip_k1(&mut tds, simplex_key, vertex!([0.5, 0.0]).unwrap()) - .unwrap_err(); - - assert_matches!(err, FlipError::DegenerateSimplex); - assert_eq!(snapshot_topology(&tds), before); - assert!(tds.is_valid().is_ok()); - } - #[test] fn test_dynamic_k2_forward_4d() { init_tracing(); @@ -14047,7 +13815,7 @@ mod tests { let facet = FacetHandle::from_validated(simplex_a, 4); let context = build_k2_flip_context(&tds, facet).unwrap(); let context_dyn = to_dynamic(context); - let info = apply_bistellar_flip_dynamic(&mut tds, 2, &context_dyn).unwrap(); + let info = apply_bistellar_flip_dynamic_raw(&mut tds, 2, &context_dyn).unwrap(); assert_eq!(info.kind, BistellarFlipKind::k2(4)); assert_eq!(info.removed_simplices.len(), 2); @@ -14102,7 +13870,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 4, 5); let context = build_k3_flip_context(&tds, ridge).unwrap(); let context_dyn = to_dynamic(context); - let info = apply_bistellar_flip_dynamic(&mut tds, 3, &context_dyn).unwrap(); + let info = apply_bistellar_flip_dynamic_raw(&mut tds, 3, &context_dyn).unwrap(); assert_eq!(info.kind, BistellarFlipKind::k3(5)); assert_eq!(info.removed_simplices.len(), 3); @@ -14157,7 +13925,7 @@ mod tests { let before = snapshot_topology(&tds); let facet = FacetHandle::from_validated(c1, 3); let context = build_k2_flip_context(&tds, facet).unwrap(); - let info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert!(tds.is_valid().is_ok()); let edge = EdgeKey::from_validated_endpoints( @@ -14165,7 +13933,7 @@ mod tests { info.inserted_face_vertices[1], ); let context_back = build_k2_flip_context_from_edge(&tds, edge).unwrap(); - let _info_back = apply_bistellar_flip_dynamic(&mut tds, 3, &context_back).unwrap(); + let _info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 3, &context_back).unwrap(); assert!(tds.is_valid().is_ok()); let after = snapshot_topology(&tds); @@ -14211,7 +13979,7 @@ mod tests { repair_neighbor_pointers(&mut candidate).unwrap(); - if verify_delaunay_via_flip_predicates(&candidate, &kernel).is_err() { + if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() { tds = Some(candidate); break; } @@ -14229,7 +13997,7 @@ mod tests { .unwrap(); assert!(stats.flips_performed > 0); - assert!(verify_delaunay_via_flip_predicates(&tds, &kernel).is_ok()); + assert!(verify_tds_via_flip_predicates(&tds, &kernel).is_ok()); assert!(tds.is_valid().is_ok()); } @@ -14270,7 +14038,7 @@ mod tests { repair_neighbor_pointers(&mut candidate).unwrap(); - if verify_delaunay_via_flip_predicates(&candidate, &kernel).is_err() { + if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() { tds = Some(candidate); break; } @@ -14351,7 +14119,7 @@ mod tests { // The fixture must be non-Delaunay for this test to be meaningful. assert!( - verify_delaunay_via_flip_predicates(&tds, &kernel).is_err(), + verify_tds_via_flip_predicates(&tds, &kernel).is_err(), "3D fixture must be non-Delaunay (e inside circumsphere of {{a,b,c,d}})" ); @@ -14377,7 +14145,7 @@ mod tests { } #[test] - fn test_verify_delaunay_via_flip_predicates_reports_non_delaunay_2d() { + fn test_verify_tds_via_flip_predicates_reports_non_delaunay_2d() { init_tracing(); let kernel = FastKernel::::new(); let a_coords = [0.0, 0.0]; @@ -14414,14 +14182,14 @@ mod tests { repair_neighbor_pointers(&mut candidate).unwrap(); - if verify_delaunay_via_flip_predicates(&candidate, &kernel).is_err() { + if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() { tds = Some(candidate); break; } } let tds = tds.expect("expected a non-Delaunay configuration from candidates"); - let result = verify_delaunay_via_flip_predicates(&tds, &kernel); + let result = verify_tds_via_flip_predicates(&tds, &kernel); assert_matches!(result, Err(DelaunayRepairError::PostconditionFailed { .. })); } @@ -14478,7 +14246,7 @@ mod tests { let facet = FacetHandle::from_validated(c1, 2); let context = build_k2_flip_context(&tds, facet).unwrap(); - let _info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let _info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert!(tds.is_valid().is_ok()); } @@ -14598,11 +14366,11 @@ mod tests { let facet = FacetHandle::from_validated(simplex_a, 4); let context = build_k2_flip_context(&tds, facet).unwrap(); - let _info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let _info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); let edge = EdgeKey::from_validated_endpoints(opposite_a, opposite_b); let context_back = build_k2_flip_context_from_edge(&tds, edge).unwrap(); - let info_back = apply_bistellar_flip_dynamic(&mut tds, 4, &context_back).unwrap(); + let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 4, &context_back).unwrap(); assert_eq!(info_back.kind.k, 4); assert_eq!(info_back.kind.d, 4); @@ -14633,13 +14401,13 @@ mod tests { let new_vertex = vertex!([0.1; 4]).unwrap(); let new_uuid = new_vertex.uuid(); - let info = apply_bistellar_flip_k1(&mut tds, simplex_key, new_vertex).unwrap(); + let info = apply_bistellar_flip_k1_raw(&mut tds, simplex_key, new_vertex).unwrap(); assert_eq!(info.kind.k, 1); assert_eq!(info.new_simplices.len(), 5); let new_key = tds.vertex_key_from_uuid(&new_uuid).unwrap(); - let info_back = apply_bistellar_flip_k1_inverse(&mut tds, new_key).unwrap(); + let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 5); assert_eq!(info_back.kind.d, 4); @@ -14694,7 +14462,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 4, 5); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); assert_eq!(info.kind.k, 3); assert_eq!(info.inserted_face_vertices.len(), 3); @@ -14706,7 +14474,7 @@ mod tests { ) .unwrap(); let context_back = build_k3_flip_context_from_triangle(&tds, triangle).unwrap(); - let info_back = apply_bistellar_flip_dynamic(&mut tds, 4, &context_back).unwrap(); + let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 4, &context_back).unwrap(); assert_eq!(info_back.kind.k, 4); assert_eq!(info_back.kind.d, 5); @@ -14754,11 +14522,11 @@ mod tests { let facet = FacetHandle::from_validated(simplex_a, 5); let context = build_k2_flip_context(&tds, facet).unwrap(); - let _info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let _info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); let edge = EdgeKey::from_validated_endpoints(opposite_a, opposite_b); let context_back = build_k2_flip_context_from_edge(&tds, edge).unwrap(); - let info_back = apply_bistellar_flip_dynamic(&mut tds, 5, &context_back).unwrap(); + let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 5, &context_back).unwrap(); assert_eq!(info_back.kind.k, 5); assert_eq!(info_back.kind.d, 5); @@ -14789,13 +14557,13 @@ mod tests { let new_vertex = vertex!([0.1; 5]).unwrap(); let new_uuid = new_vertex.uuid(); - let info = apply_bistellar_flip_k1(&mut tds, simplex_key, new_vertex).unwrap(); + let info = apply_bistellar_flip_k1_raw(&mut tds, simplex_key, new_vertex).unwrap(); assert_eq!(info.kind.k, 1); assert_eq!(info.new_simplices.len(), 6); let new_key = tds.vertex_key_from_uuid(&new_uuid).unwrap(); - let info_back = apply_bistellar_flip_k1_inverse(&mut tds, new_key).unwrap(); + let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 6); assert_eq!(info_back.kind.d, 5); @@ -14823,14 +14591,14 @@ mod tests { let new_vertex = vertex!([0.2, 0.2]).unwrap(); let new_uuid = new_vertex.uuid(); - let info = apply_bistellar_flip_k1(&mut tds, simplex, new_vertex).unwrap(); + let info = apply_bistellar_flip_k1_raw(&mut tds, simplex, new_vertex).unwrap(); assert_eq!(info.kind.k, 1); assert_eq!(info.kind.d, 2); assert_eq!(tds.number_of_simplices(), 3); let new_key = tds.vertex_key_from_uuid(&new_uuid).unwrap(); - let info_back = apply_bistellar_flip_k1_inverse(&mut tds, new_key).unwrap(); + let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 3); assert_eq!(info_back.kind.d, 2); @@ -14878,7 +14646,7 @@ mod tests { let facet = FacetHandle::from_validated(simplex_a, 4); let context = build_k2_flip_context(&tds, facet).unwrap(); - let info = apply_bistellar_flip_k2(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); let kernel = AdaptiveKernel::::new(); let seed_simplices: SimplexKeyBuffer = info.new_simplices.iter().copied().collect(); @@ -14940,7 +14708,7 @@ mod tests { let ridge = RidgeHandle::from_validated(c1, 4, 5); let context = build_k3_flip_context(&tds, ridge).unwrap(); - let info = apply_bistellar_flip_k3(&mut tds, &context).unwrap(); + let info = apply_bistellar_flip_raw(&mut tds, &context).unwrap(); let kernel = AdaptiveKernel::::new(); let seed_simplices: SimplexKeyBuffer = info.new_simplices.iter().copied().collect(); @@ -15314,6 +15082,24 @@ mod tests { FlipFailureKind::from(&FlipError::from(embedding_wiring)), FlipFailureKind::WiringValidation ); + + let transaction_embedding = FlipError::EmbeddingValidation { + source: Box::new(embedding_source.clone()), + }; + assert_eq!( + FlipFailureKind::from(&transaction_embedding), + FlipFailureKind::EmbeddingValidation + ); + + let transaction_repair = FlipError::PostconditionRepair { + source: Box::new(InsertionError::EmbeddingValidationFailed { + source: embedding_source, + }), + }; + assert_eq!( + FlipFailureKind::from(&transaction_repair), + FlipFailureKind::PostconditionRepair + ); } #[test] diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 550f9c30..43b9c0d0 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -2074,7 +2074,7 @@ impl InsertionError { } } -/// Fill cavity by creating new simplices connecting boundary facets to new vertex. +/// Fills a caller-validated replacement cavity with prechecked topology insertion. /// /// Each boundary facet becomes the base of a new (D+1)-simplex with the new vertex as apex. /// @@ -2119,59 +2119,6 @@ impl InsertionError { /// boundary facets and logs warnings if found. Duplicate facets will create /// overlapping simplices, which will be detected and repaired by subsequent topology /// validation passes (see `detect_local_facet_issues` / `repair_local_facet_issues`). -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::insertion::fill_cavity; -/// use delaunay::prelude::tds::FacetHandle; -/// use delaunay::prelude::*; -/// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Insertion(#[from] delaunay::prelude::insertion::InsertionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { -/// let vertices = vec![ -/// delaunay::vertex![0.0, 0.0, 0.0]?, -/// delaunay::vertex![1.0, 0.0, 0.0]?, -/// delaunay::vertex![0.0, 1.0, 0.0]?, -/// delaunay::vertex![0.0, 0.0, 1.0]?, -/// ]; -/// let dt: DelaunayTriangulation<_, (), (), 3> = -/// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let mut tds = dt.tds().clone(); -/// let Some(vkey) = tds.vertex_keys().next() else { return Ok(()); }; -/// let boundary_facets: Vec = Vec::new(); -/// -/// let new_simplices = fill_cavity(&mut tds, vkey, &boundary_facets)?; -/// assert!(new_simplices.is_empty()); -/// # Ok(()) -/// # } -/// ``` -pub fn fill_cavity( - tds: &mut Tds, - new_vertex_key: VertexKey, - boundary_facets: &[FacetHandle], -) -> Result -where - U: DataType, - V: DataType, -{ - fill_cavity_impl( - tds, - new_vertex_key, - boundary_facets, - CavityInsertionTopology::Checked, - ) -} - -/// Fills a replacement cavity using the transactional replacement insertion path. pub(crate) fn fill_cavity_replacing_simplices( tds: &mut Tds, new_vertex_key: VertexKey, @@ -2181,12 +2128,7 @@ where U: DataType, V: DataType, { - fill_cavity_impl( - tds, - new_vertex_key, - boundary_facets, - CavityInsertionTopology::Prechecked, - ) + fill_cavity_impl(tds, new_vertex_key, boundary_facets) } /// Fills a caller-validated cavity without per-simplex global insertion scans. @@ -2199,24 +2141,10 @@ where U: DataType, V: DataType, { - fill_cavity_impl( - tds, - new_vertex_key, - boundary_facets, - CavityInsertionTopology::Prechecked, - ) -} - -/// Topology-check mode used while filling new cavity simplices. -#[derive(Clone, Copy)] -enum CavityInsertionTopology { - /// Run the standard TDS insertion checks against every existing simplex. - Checked, - /// Skip global insertion scans because the caller validated the local boundary. - Prechecked, + fill_cavity_impl(tds, new_vertex_key, boundary_facets) } -/// Shared cavity-fill implementation for checked, replacement, and prechecked insertions. +/// Shared cavity-fill implementation for caller-validated replacement insertions. #[expect( clippy::too_many_lines, reason = "Cavity filling includes detailed debug instrumentation and error handling" @@ -2225,7 +2153,6 @@ fn fill_cavity_impl( tds: &mut Tds, new_vertex_key: VertexKey, boundary_facets: &[FacetHandle], - insertion_topology: CavityInsertionTopology, ) -> Result where U: DataType, @@ -2416,15 +2343,9 @@ where // Create and insert the new simplex let new_simplex = Simplex::try_new(new_simplex_vertices).map_err(CavityFillingError::from)?; - let simplex_key = match insertion_topology { - CavityInsertionTopology::Checked => { - tds.insert_simplex_with_mapping_trusted_vertices(new_simplex) - } - CavityInsertionTopology::Prechecked => { - tds.insert_simplex_with_mapping_prechecked_topology(new_simplex) - } - } - .map_err(CavityFillingError::from)?; + let simplex_key = tds + .insert_simplex_with_mapping_prechecked_topology(new_simplex) + .map_err(CavityFillingError::from)?; // Simplex creation provenance: log each newly created simplex with its // vertex ordering, geometric orientation, and source boundary facet. @@ -2516,26 +2437,11 @@ where /// # Errors /// Returns error if neighbor wiring fails or simplices cannot be found. /// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::insertion::{InsertionError, wire_cavity_neighbors}; -/// use delaunay::prelude::collections::SimplexKeyBuffer; -/// use delaunay::prelude::tds::Tds; -/// -/// # fn main() -> Result<(), InsertionError> { -/// let mut tds: Tds<(), (), 3> = Tds::empty(); -/// let new_simplices = SimplexKeyBuffer::new(); -/// -/// wire_cavity_neighbors(&mut tds, &new_simplices, [], None)?; -/// # Ok(()) -/// # } -/// ``` #[expect( clippy::too_many_lines, reason = "Neighbor wiring keeps cohesive logic and debug accounting together" )] -pub fn wire_cavity_neighbors( +pub(crate) fn wire_cavity_neighbors( tds: &mut Tds, new_simplices: &SimplexKeyBuffer, external_facets: I, @@ -3096,63 +3002,11 @@ where /// pointer. Returns [`InsertionError::NonManifoldTopology`] if local facet incidence has more /// than two incident simplices for one facet. /// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::{DelaunayTriangulation, DelaunayTriangulationBuilder}; -/// use delaunay::prelude::DelaunayTriangulationConstructionError; -/// use delaunay::prelude::insertion::{ -/// InsertionError, TdsMutationError, repair_neighbor_pointers_local, -/// }; -/// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Mutation(#[from] TdsMutationError), -/// # #[error(transparent)] -/// # Insertion(#[from] InsertionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { -/// let vertices = [ -/// delaunay::vertex![0.0, 0.0]?, -/// delaunay::vertex![1.0, 0.0]?, -/// delaunay::vertex![0.0, 1.0]?, -/// delaunay::vertex![1.0, 1.1]?, -/// ]; -/// let dt: DelaunayTriangulation<_, (), (), 2> = -/// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let mut tds = dt.tds().clone(); -/// -/// let Some((simplex_key, facet_idx, neighbor_key)) = tds -/// .simplices() -/// .find_map(|(simplex_key, simplex)| { -/// simplex.neighbors()?.enumerate().find_map(|(facet_idx, neighbor)| { -/// neighbor.map(|neighbor_key| (simplex_key, facet_idx, neighbor_key)) -/// }) -/// }) -/// else { -/// return Ok(()); -/// }; -/// -/// let repaired = repair_neighbor_pointers_local(&mut tds, &[simplex_key], Some(&[neighbor_key]))?; -/// assert_eq!(repaired, 0); -/// assert_eq!( -/// tds.simplex(simplex_key) -/// .and_then(|simplex| simplex.neighbor_key(facet_idx).flatten()), -/// Some(neighbor_key) -/// ); -/// # Ok(()) -/// # } -/// ``` #[expect( clippy::too_many_lines, reason = "Local neighbor repair keeps affected-set construction, facet indexing, and slot application together" )] -pub fn repair_neighbor_pointers_local( +pub(crate) fn repair_neighbor_pointers_local( tds: &mut Tds, seeds: &[SimplexKey], optional_external_simplices: Option<&[SimplexKey]>, @@ -3342,43 +3196,11 @@ where /// Returns [`InsertionError::NonManifoldTopology`] if any facet is shared by more than /// 2 simplices, since neighbor pointers are not well-defined in that case. /// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::construction::{ -/// DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, -/// }; -/// use delaunay::prelude::insertion::{InsertionError, repair_neighbor_pointers}; -/// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Insertion(#[from] InsertionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { -/// let vertices = vec![ -/// delaunay::vertex![0.0, 0.0]?, -/// delaunay::vertex![1.0, 0.0]?, -/// delaunay::vertex![0.0, 1.0]?, -/// ]; -/// let dt: DelaunayTriangulation<_, (), (), 2> = -/// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let mut tds = dt.tds().clone(); -/// -/// let _changed_slots = repair_neighbor_pointers(&mut tds)?; -/// assert!(tds.is_valid().is_ok()); -/// # Ok(()) -/// # } -/// ``` #[expect( clippy::too_many_lines, reason = "Neighbor rebuild keeps facet indexing + wiring + application cohesive; prefer correctness and debuggability" )] -pub fn repair_neighbor_pointers( +pub(crate) fn repair_neighbor_pointers( tds: &mut Tds, ) -> Result where @@ -3742,29 +3564,7 @@ where /// - Finding visible facets fails /// - Cavity filling or neighbor wiring fails /// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::insertion::extend_hull; -/// use delaunay::prelude::tds::Tds; -/// use delaunay::prelude::tds::VertexKey; -/// use delaunay::prelude::geometry::FastKernel; -/// use delaunay::prelude::geometry::Point; -/// use delaunay::prelude::geometry::Coordinate; -/// use slotmap::Key; -/// -/// # fn main() -> Result<(), delaunay::prelude::geometry::CoordinateConversionError> { -/// let mut tds: Tds<(), (), 3> = Tds::empty(); -/// let vkey = VertexKey::null(); -/// let kernel = FastKernel::::new(); -/// let point = Point::try_from([2.0, 2.0, 2.0])?; -/// -/// let result = extend_hull(&mut tds, &kernel, vkey, &point); -/// assert!(result.is_err()); -/// # Ok(()) -/// # } -/// ``` -pub fn extend_hull( +pub(crate) fn extend_hull( tds: &mut Tds, kernel: &K, new_vertex_key: VertexKey, @@ -4887,16 +4687,16 @@ mod tests { assert_matches!(insertion_source, Some(HullExtensionReason::Tds(_))); } - /// Macro to generate cavity filling tests for different dimensions - macro_rules! test_fill_cavity { + /// Macro to generate cavity replacement filling tests for different dimensions. + macro_rules! test_fill_cavity_replacing_simplices { ($dim:literal, $initial_vertices:expr, $new_vertex:expr, $expected_facets:literal) => { pastey::paste! { #[test] - fn []() { + fn []() { // Create initial simplex let vertices = $initial_vertices; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); // Insert new vertex let new_vertex = $new_vertex; @@ -4911,8 +4711,9 @@ mod tests { // Verify expected number of facets assert_eq!(boundary_facets.len(), $expected_facets); - // Fill cavity - let new_simplices = fill_cavity(tds, new_vkey, &boundary_facets).unwrap(); + // Fill the replacement cavity. + let new_simplices = + fill_cavity_replacing_simplices(tds, new_vkey, &boundary_facets).unwrap(); // Should create one simplex per boundary facet assert_eq!(new_simplices.len(), $expected_facets); @@ -4948,7 +4749,7 @@ mod tests { } // Generate tests for dimensions 2-5 - test_fill_cavity!( + test_fill_cavity_replacing_simplices!( 2, vec![ vertex!([0.0, 0.0]).unwrap(), @@ -4959,7 +4760,7 @@ mod tests { 3 // D+1 facets for a 2-simplex ); - test_fill_cavity!( + test_fill_cavity_replacing_simplices!( 3, vec![ vertex!([0.0, 0.0, 0.0]).unwrap(), @@ -4971,7 +4772,7 @@ mod tests { 4 // D+1 facets for a 3-simplex ); - test_fill_cavity!( + test_fill_cavity_replacing_simplices!( 4, vec![ vertex!([0.0, 0.0, 0.0, 0.0]).unwrap(), @@ -4984,7 +4785,7 @@ mod tests { 5 // D+1 facets for a 4-simplex ); - test_fill_cavity!( + test_fill_cavity_replacing_simplices!( 5, vec![ vertex!([0.0, 0.0, 0.0, 0.0, 0.0]).unwrap(), @@ -5001,14 +4802,14 @@ mod tests { // Error case tests #[test] - fn test_fill_cavity_with_invalid_vertex_key() { + fn test_fill_cavity_replacing_simplices_with_invalid_vertex_key() { let vertices = vec![ vertex!([0.0, 0.0]).unwrap(), vertex!([1.0, 0.0]).unwrap(), vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let invalid_vkey = VertexKey::from(KeyData::from_ffi(u64::MAX)); let simplex_key = tds.simplex_keys().next().unwrap(); @@ -5016,7 +4817,7 @@ mod tests { .map(|i| FacetHandle::from_validated(simplex_key, i)) .collect(); - let result = fill_cavity(tds, invalid_vkey, &boundary_facets); + let result = fill_cavity_replacing_simplices(tds, invalid_vkey, &boundary_facets); assert_matches!( result, Err(InsertionError::CavityFilling { @@ -5027,14 +4828,14 @@ mod tests { } #[test] - fn test_fill_cavity_with_invalid_facet_simplex() { + fn test_fill_cavity_replacing_simplices_with_invalid_facet_simplex() { let vertices = vec![ vertex!([0.0, 0.0]).unwrap(), vertex!([1.0, 0.0]).unwrap(), vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let new_vkey = tds .insert_vertex_with_mapping(vertex!([0.5, 0.5]).unwrap()) @@ -5044,7 +4845,7 @@ mod tests { .map(|i| FacetHandle::from_validated(invalid_simplex_key, i)) .collect(); - let result = fill_cavity(tds, new_vkey, &invalid_boundary_facets); + let result = fill_cavity_replacing_simplices(tds, new_vkey, &invalid_boundary_facets); assert!(result.is_err()); assert_matches!( result, @@ -5055,14 +4856,14 @@ mod tests { } #[test] - fn test_fill_cavity_with_invalid_facet_index() { + fn test_fill_cavity_replacing_simplices_with_invalid_facet_index() { let vertices = vec![ vertex!([0.0, 0.0]).unwrap(), vertex!([1.0, 0.0]).unwrap(), vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let new_vkey = tds .insert_vertex_with_mapping(vertex!([0.5, 0.5]).unwrap()) @@ -5071,7 +4872,7 @@ mod tests { let original_simplex_count = tds.number_of_simplices(); let invalid_boundary_facets = vec![FacetHandle::from_validated(simplex_key, 3)]; - let result = fill_cavity(tds, new_vkey, &invalid_boundary_facets); + let result = fill_cavity_replacing_simplices(tds, new_vkey, &invalid_boundary_facets); assert_matches!( result, @@ -5094,7 +4895,7 @@ mod tests { vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let mut invalid_simplices = SimplexKeyBuffer::new(); invalid_simplices.push(SimplexKey::from(KeyData::from_ffi(u64::MAX))); @@ -5347,34 +5148,34 @@ mod tests { } #[test] - fn test_fill_cavity_with_empty_boundary_facets() { + fn test_fill_cavity_replacing_simplices_with_empty_boundary_facets() { let vertices = vec![ vertex!([0.0, 0.0]).unwrap(), vertex!([1.0, 0.0]).unwrap(), vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let new_vkey = tds .insert_vertex_with_mapping(vertex!([0.5, 0.5]).unwrap()) .unwrap(); let empty_facets: Vec = vec![]; - let result = fill_cavity(tds, new_vkey, &empty_facets); + let result = fill_cavity_replacing_simplices(tds, new_vkey, &empty_facets); assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 0); } #[test] - fn test_fill_cavity_errors_on_boundary_simplex_wrong_vertex_count() { + fn test_fill_cavity_replacing_simplices_errors_on_boundary_simplex_wrong_vertex_count() { let vertices = vec![ vertex!([0.0, 0.0]).unwrap(), vertex!([1.0, 0.0]).unwrap(), vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); // Insert a new vertex (apex) let new_vkey = tds @@ -5389,7 +5190,7 @@ mod tests { .push_vertex_key(extra_vkey); let boundary_facets = vec![FacetHandle::from_validated(simplex_key, 0)]; - let err = fill_cavity(tds, new_vkey, &boundary_facets).unwrap_err(); + let err = fill_cavity_replacing_simplices(tds, new_vkey, &boundary_facets).unwrap_err(); assert_matches!( err, @@ -5460,7 +5261,7 @@ mod tests { vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let simplex_key = tds.simplex_keys().next().unwrap(); let vkey0 = tds.simplex(simplex_key).unwrap().vertices()[0]; @@ -6347,7 +6148,7 @@ mod tests { fn []() { let vertices = $initial_vertices; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); // Verify all neighbor pointers are initially valid for (_, simplex) in tds.simplices() { @@ -6435,7 +6236,7 @@ mod tests { vertex!([1.0, 1.1]).unwrap(), // break cocircular symmetry ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); // Remove all neighbor pointers. tds.clear_all_neighbors(); @@ -6472,7 +6273,7 @@ mod tests { fn []() { let vertices = $initial_vertices; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let (simplex_key, facet_idx, neighbor_key, _) = first_neighbor_pair(tds).expect("test triangulation should have adjacent simplices"); @@ -6620,7 +6421,7 @@ mod tests { vertex!([1.0, 1.1]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let (simplex_key, facet_idx, neighbor_key, _) = first_neighbor_pair(tds).expect("test triangulation should have adjacent simplices"); let stale_neighbor = SimplexKey::from(KeyData::from_ffi(u64::MAX - 7)); @@ -6708,7 +6509,7 @@ mod tests { vertex!([0.5, 0.35]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let (simplex_key, facet_idx, _neighbor_key, _) = first_neighbor_pair(tds).expect("test triangulation should have adjacent simplices"); @@ -6733,7 +6534,7 @@ mod tests { vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let kernel = FastKernel::::new(); let p = Point::try_new([2.0, 2.0]).expect("finite point coordinates"); @@ -6754,7 +6555,7 @@ mod tests { vertex!([0.0, 1.0]).unwrap(), ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = dt.tds_mut(); + let tds = dt.tds_mut_for_repair(); let kernel = FastKernel::::new(); let p = Point::try_new([0.25, 0.25]).expect("finite point coordinates"); // inside diff --git a/src/core/algorithms/locate.rs b/src/core/algorithms/locate.rs index f678cc7b..4eb78032 100644 --- a/src/core/algorithms/locate.rs +++ b/src/core/algorithms/locate.rs @@ -25,7 +25,6 @@ use crate::core::collections::{ }; use crate::core::facet::FacetHandle; use crate::core::tds::{SimplexKey, Tds, VertexKey}; -use crate::core::traits::data_type::DataType; use crate::core::util::canonical_points::{ CanonicalFacetPointError, CanonicalSimplexPointError, sorted_facet_points_with_extra, sorted_simplex_points, @@ -462,11 +461,10 @@ struct RidgeInfo { extra_facets: Vec, } -fn format_vertex_refs(tds: &Tds, vertex_keys: &[VertexKey]) -> String -where - U: DataType, - V: DataType, -{ +fn format_vertex_refs( + tds: &Tds, + vertex_keys: &[VertexKey], +) -> String { let mut refs = String::new(); for (idx, &vertex_key) in vertex_keys.iter().enumerate() { if idx != 0 { @@ -481,11 +479,7 @@ where refs } -fn format_facet_vertices(tds: &Tds, handle: FacetHandle) -> String -where - U: DataType, - V: DataType, -{ +fn format_facet_vertices(tds: &Tds, handle: FacetHandle) -> String { let Some(simplex) = tds.simplex(handle.simplex_key()) else { return String::from(""); }; @@ -503,11 +497,7 @@ where fn format_simplex_vertices( tds: &Tds, simplex_key: SimplexKey, -) -> String -where - U: DataType, - V: DataType, -{ +) -> String { let Some(simplex) = tds.simplex(simplex_key) else { return String::from(""); }; @@ -530,10 +520,7 @@ fn log_first_ridge_fan_dump( boundary_facets: &CavityBoundaryBuffer, info: &RidgeInfo, extra_simplices: &[SimplexKey], -) where - U: DataType, - V: DataType, -{ +) { if !ridge_fan_dump_enabled() || RIDGE_FAN_DUMP_EMITTED.swap(true, Ordering::Relaxed) { return; } @@ -781,15 +768,15 @@ pub(crate) struct LocateTrace { /// /// // Point inside the 4-simplex /// let inside_point = Point::try_from([0.2, 0.2, 0.2, 0.2])?; -/// let inside = locate(dt.tds(), &kernel, &inside_point, None)?; +/// let inside = dt.locate(&inside_point, None)?; /// std::assert_matches!( /// inside, -/// LocateResult::InsideSimplex(simplex_key) if dt.tds().contains_simplex(simplex_key) +/// LocateResult::InsideSimplex(simplex_key) if dt.contains_simplex(simplex_key) /// ); /// /// // Point outside the convex hull /// let outside_point = Point::try_from([2.0, 2.0, 2.0, 2.0])?; -/// let outside = locate(dt.tds(), &kernel, &outside_point, None)?; +/// let outside = dt.locate(&outside_point, None)?; /// std::assert_matches!(outside, LocateResult::Outside); /// # Ok(()) /// # } @@ -826,12 +813,12 @@ pub(crate) struct LocateTrace { /// let kernel = RobustKernel::::default(); /// /// // Get a simplex to use as hint (spatially close to query point) -/// let Some(hint_simplex) = dt.tds().simplex_keys().next() else { +/// let Some((hint_simplex, _)) = dt.simplices().next() else { /// return Ok(()); /// }; /// let query_point = Point::try_from([0.15, 0.15, 0.15, 0.15])?; /// -/// let located = locate(dt.tds(), &kernel, &query_point, Some(hint_simplex))?; +/// let located = dt.locate(&query_point, Some(hint_simplex))?; /// std::assert_matches!(located, LocateResult::InsideSimplex(_)); /// # Ok(()) /// # } @@ -844,8 +831,6 @@ pub fn locate( ) -> Result where K: Kernel, - U: DataType, - V: DataType, { locate_with_stats(tds, kernel, point, hint).map(|(result, _stats)| result) } @@ -889,7 +874,7 @@ where /// let kernel = FastKernel::::new(); /// /// let query_point = Point::try_from([0.3, 0.3])?; -/// let (_result, stats) = locate_with_stats(dt.tds(), &kernel, &query_point, None)?; +/// let (_result, stats) = dt.locate_with_stats(&query_point, None)?; /// /// // In well-conditioned cases, the facet-walk should converge without falling back. /// assert!(!stats.fell_back_to_scan()); @@ -904,8 +889,6 @@ pub fn locate_with_stats( ) -> Result<(LocateResult, LocateStats), LocateError> where K: Kernel, - U: DataType, - V: DataType, { let trace = locate_with_trace(tds, kernel, point, hint)?; Ok((trace.result, trace.stats)) @@ -924,8 +907,6 @@ pub(crate) fn locate_with_trace( ) -> Result where K: Kernel, - U: DataType, - V: DataType, { const MAX_STEPS: usize = 10000; @@ -1022,8 +1003,6 @@ pub(crate) fn locate_by_scan( ) -> Result where K: Kernel, - U: DataType, - V: DataType, { for (simplex_key, simplex) in tds.simplices() { let mut found_outside_facet = false; @@ -1082,8 +1061,6 @@ fn is_point_outside_facet( ) -> Result where K: Kernel, - U: DataType, - V: DataType, { let simplex = tds .simplex(simplex_key) @@ -1263,10 +1240,10 @@ const fn conflict_simplex_points_error( /// let query_point = Point::try_from([0.2, 0.2, 0.2, 0.2])?; /// /// // First locate the point -/// let location = locate(dt.tds(), &kernel, &query_point, None)?; +/// let location = dt.locate(&query_point, None)?; /// if let LocateResult::InsideSimplex(simplex_key) = location { /// // Find all simplices whose circumspheres contain the point -/// let conflict_simplices = find_conflict_region(dt.tds(), &kernel, &query_point, simplex_key)?; +/// let conflict_simplices = dt.find_conflict_region(&query_point, simplex_key)?; /// assert_eq!(conflict_simplices.len(), 1); // Single 4-simplex contains the point /// } /// # Ok(()) @@ -1284,8 +1261,6 @@ pub fn find_conflict_region( ) -> Result where K: Kernel, - U: DataType, - V: DataType, { #[cfg(debug_assertions)] let debug_config = conflict_debug_config(); @@ -1468,12 +1443,12 @@ where /// # { /// use delaunay::prelude::collections::SimplexKeyBuffer; /// use delaunay::prelude::diagnostics::verify_conflict_region_completeness; -/// use delaunay::prelude::geometry::{AdaptiveKernel, Coordinate, Point}; +/// use delaunay::prelude::geometry::{AdaptiveKernel, Point}; /// use delaunay::prelude::tds::Tds; /// /// let tds: Tds<(), (), 2> = Tds::empty(); /// let kernel = AdaptiveKernel::::new(); -/// let point = Point::try_from([0.25, 0.25])?; +/// let point = Point::<2>::default(); /// let bfs_conflicts = SimplexKeyBuffer::new(); /// /// let missed = verify_conflict_region_completeness( @@ -1485,7 +1460,7 @@ where /// assert_eq!(missed, 0); /// # } /// ``` -#[cfg(any(feature = "diagnostics", all(test, debug_assertions)))] +#[cfg(feature = "diagnostics")] #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] pub fn verify_conflict_region_completeness( tds: &Tds, @@ -1495,8 +1470,6 @@ pub fn verify_conflict_region_completeness( ) -> usize where K: Kernel, - U: DataType, - V: DataType, { let bfs_set: FastHashSet = bfs_conflict_simplices.iter().copied().collect(); let mut missed_count = 0usize; @@ -1649,8 +1622,8 @@ where /// /// /// ```rust -/// use delaunay::prelude::algorithms::{locate, find_conflict_region, extract_cavity_boundary, LocateResult}; -/// use delaunay::prelude::{DelaunayTriangulation, DelaunayTriangulationBuilder}; +/// use delaunay::prelude::algorithms::LocateResult; +/// use delaunay::prelude::DelaunayTriangulationBuilder; /// use delaunay::prelude::geometry::FastKernel; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; @@ -1664,6 +1637,8 @@ where /// # #[error(transparent)] /// # Conflict(#[from] delaunay::prelude::algorithms::ConflictError), /// # #[error(transparent)] +/// # Facet(#[from] delaunay::prelude::tds::FacetError), +/// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { @@ -1681,13 +1656,13 @@ where /// let query_point = Point::try_from([0.2, 0.2, 0.2, 0.2])?; /// /// // Locate and find conflict region -/// let location = locate(dt.tds(), &kernel, &query_point, None)?; +/// let location = dt.locate(&query_point, None)?; /// if let LocateResult::InsideSimplex(simplex_key) = location { -/// let conflict_simplices = find_conflict_region(dt.tds(), &kernel, &query_point, simplex_key)?; -/// +/// let conflict_simplices = dt.find_conflict_region(&query_point, simplex_key)?; +/// /// // Extract cavity boundary -/// let boundary_facets = extract_cavity_boundary(dt.tds(), &conflict_simplices)?; -/// +/// let boundary_facets = dt.simplex_facets(simplex_key)?.collect::, _>>()?; +/// /// // For a single 4-simplex, all 5 facets are on the boundary (convex hull) /// assert_eq!(boundary_facets.len(), 5); /// } @@ -1701,11 +1676,7 @@ where pub fn extract_cavity_boundary( tds: &Tds, conflict_simplices: &SimplexKeyBuffer, -) -> Result -where - U: DataType, - V: DataType, -{ +) -> Result { // Empty conflict region => empty boundary if conflict_simplices.is_empty() { return Ok(CavityBoundaryBuffer::new()); @@ -2551,7 +2522,7 @@ mod tests { let simplex_key = dt.tds().simplex_keys().next().unwrap(); // ⚠️ Dangerous test-only mutation: create a neighbor self-loop on every facet. - let simplex = dt.tds_mut().simplex_mut(simplex_key).unwrap(); + let simplex = dt.tds_mut_for_repair().simplex_mut(simplex_key).unwrap(); let mut neighbors = NeighborBuffer::>::new(); neighbors.resize(3, Some(simplex_key)); simplex.set_neighbors_from_keys(neighbors).unwrap(); @@ -3494,10 +3465,10 @@ mod tests { // ============================================================================= // VERIFY CONFLICT REGION COMPLETENESS TESTS // ============================================================================= - // The production diagnostic is feature-gated; keep the unit coverage in - // debug builds to avoid adding cost to release/bench test runs. + // The production diagnostic is feature-gated; keep the unit coverage on the + // same opt-in surface so tests do not expose a test-only API shape. - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] /// Macro to test `verify_conflict_region_completeness` across dimensions. /// Builds a single-simplex Delaunay triangulation, finds the conflict region /// for an interior point via BFS, then verifies the brute-force check agrees. @@ -3533,7 +3504,7 @@ mod tests { }}; } - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_2d() { test_verify_conflict_region_complete_dimension!( @@ -3545,7 +3516,7 @@ mod tests { ); } - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_3d() { test_verify_conflict_region_complete_dimension!( @@ -3558,7 +3529,7 @@ mod tests { ); } - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_4d() { test_verify_conflict_region_complete_dimension!( @@ -3572,7 +3543,7 @@ mod tests { ); } - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_5d() { test_verify_conflict_region_complete_dimension!( @@ -3588,7 +3559,7 @@ mod tests { } /// An empty BFS result should detect all conflict simplices as missed. - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_empty_bfs_detects_missed() { let vertices = vec![ @@ -3611,7 +3582,7 @@ mod tests { } /// Point far outside produces no conflict simplices; verify returns 0 missed. - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_outside_point_zero_missed() { let vertices = vec![ @@ -3646,7 +3617,7 @@ mod tests { /// the omission. Because the two triangles are adjacent, the dropped simplex /// has a neighbor still in the truncated BFS set, so the internal /// classification logs `REACHABLE_BUT_REJECTED` (observable via tracing). - #[cfg(debug_assertions)] + #[cfg(feature = "diagnostics")] #[test] fn test_verify_conflict_region_completeness_truncated_multi_simplex_detects_missed() { // Four corners of a rectangle — DT produces 2 triangles sharing a diagonal. diff --git a/src/core/collections/key_maps.rs b/src/core/collections/key_maps.rs index e3fa368a..2cf24004 100644 --- a/src/core/collections/key_maps.rs +++ b/src/core/collections/key_maps.rs @@ -65,16 +65,15 @@ pub type VertexUuidSet = FastHashSet; /// .topology_guarantee(TopologyGuarantee::PLManifold) /// .build()?; /// println!("Topology guarantee: {:?}", dt.topology_guarantee()); -/// let tds = dt.tds(); /// /// // Get first vertex key and its UUID -/// let Some((vertex_key, _)) = tds.vertices().next() else { +/// let Some((vertex_key, _)) = dt.vertices().next() else { /// return Err(ReverseLookupExampleError::MissingVertex); /// }; -/// let Some(vertex) = tds.vertex(vertex_key) else { +/// let Some(vertex_uuid) = dt.vertex_uuid_from_key(vertex_key) else { /// return Err(ReverseLookupExampleError::UnresolvedVertexKey); /// }; -/// let vertex_uuid = vertex.uuid(); +/// assert_eq!(dt.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); /// # Ok(()) /// # } /// ``` @@ -121,17 +120,16 @@ pub type UuidToVertexKeyMap = FastHashMap; /// .topology_guarantee(TopologyGuarantee::PLManifold) /// .build()?; /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); -/// let tds = dt.tds(); /// /// // Get first simplex key and its UUID -/// let Some((simplex_key, _)) = tds.simplices().next() else { +/// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Err(ReverseLookupExampleError::MissingSimplex); /// }; -/// let Some(simplex) = tds.simplex(simplex_key) else { +/// let Some(simplex) = dt.simplex(simplex_key) else { /// return Err(ReverseLookupExampleError::UnresolvedSimplexKey); /// }; /// let simplex_uuid = simplex.uuid(); -/// assert_eq!(tds.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); +/// assert_eq!(dt.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); /// # Ok(()) /// # } /// ``` diff --git a/src/core/collections/secondary_maps.rs b/src/core/collections/secondary_maps.rs index 74d1dcef..90eaf55a 100644 --- a/src/core/collections/secondary_maps.rs +++ b/src/core/collections/secondary_maps.rs @@ -49,11 +49,10 @@ use slotmap::SparseSecondaryMap; /// ]; /// let dt: DelaunayTriangulation<_, _, _, 3> = /// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); /// /// use delaunay::prelude::collections::SimplexSecondaryMap; /// let mut in_conflict: SimplexSecondaryMap = SimplexSecondaryMap::new(); -/// for (simplex_key, _) in tds.simplices() { +/// for (simplex_key, _) in dt.simplices() { /// in_conflict.insert(simplex_key, true); /// } /// # Ok(()) @@ -100,11 +99,10 @@ pub type SimplexSecondaryMap = SparseSecondaryMap; /// ]; /// let dt: DelaunayTriangulation<_, _, _, 3> = /// DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); /// /// use delaunay::prelude::collections::VertexSecondaryMap; /// let mut processing_order: VertexSecondaryMap = VertexSecondaryMap::new(); -/// for (idx, (vertex_key, _)) in tds.vertices().enumerate() { +/// for (idx, (vertex_key, _)) in dt.vertices().enumerate() { /// processing_order.insert(vertex_key, idx); /// } /// # Ok(()) diff --git a/src/core/edge.rs b/src/core/edge.rs index f0eded77..15fdf787 100644 --- a/src/core/edge.rs +++ b/src/core/edge.rs @@ -95,7 +95,6 @@ pub enum EdgeKeyError { /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -118,7 +117,7 @@ pub enum EdgeKeyError { /// }; /// let a = simplex.vertices()[0]; /// let b = simplex.vertices()[1]; -/// let edge = EdgeKey::try_new(dt.tds(), a, b)?; +/// let edge = dt.edge_key(a, b)?; /// assert_eq!(edge.endpoints(), (edge.v0(), edge.v1())); /// # Ok(()) /// # } @@ -148,7 +147,6 @@ impl EdgeKey { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -172,8 +170,8 @@ impl EdgeKey { /// let a = simplex.vertices()[0]; /// let b = simplex.vertices()[1]; /// - /// let e1 = EdgeKey::try_new(dt.tds(), a, b)?; - /// let e2 = EdgeKey::try_new(dt.tds(), b, a)?; + /// let e1 = dt.edge_key(a, b)?; + /// let e2 = dt.edge_key(b, a)?; /// assert_eq!(e1, e2); /// assert!(e1.v0() <= e1.v1()); /// # Ok(()) @@ -300,7 +298,6 @@ impl EdgeKey { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -324,7 +321,7 @@ impl EdgeKey { /// let a = simplex.vertices()[1]; /// let b = simplex.vertices()[0]; /// - /// let e = EdgeKey::try_new(dt.tds(), a, b)?; + /// let e = dt.edge_key(a, b)?; /// let v0 = e.v0(); /// let v1 = e.v1(); /// assert!(v0 <= v1); @@ -343,7 +340,6 @@ impl EdgeKey { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -367,7 +363,7 @@ impl EdgeKey { /// let a = simplex.vertices()[0]; /// let b = simplex.vertices()[1]; /// - /// let e = EdgeKey::try_new(dt.tds(), a, b)?; + /// let e = dt.edge_key(a, b)?; /// let v0 = e.v0(); /// let v1 = e.v1(); /// assert!(v0 <= v1); @@ -386,7 +382,6 @@ impl EdgeKey { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -410,7 +405,7 @@ impl EdgeKey { /// let a = simplex.vertices()[0]; /// let b = simplex.vertices()[1]; /// - /// let e = EdgeKey::try_new(dt.tds(), a, b)?; + /// let e = dt.edge_key(a, b)?; /// let (v0, v1) = e.endpoints(); /// assert_eq!(v0, e.v0()); /// assert_eq!(v1, e.v1()); @@ -440,7 +435,6 @@ impl EdgeKey { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::EdgeKey; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -462,8 +456,8 @@ impl EdgeKey { /// return Ok(()); /// }; /// - /// let edge = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; - /// let view = edge.view(dt.tds())?; + /// let edge = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// let view = dt.edge_view(edge)?; /// assert_eq!(view.endpoint_keys(), edge.endpoints()); /// # Ok(()) /// # } @@ -503,7 +497,7 @@ impl<'tds, U, V, const D: usize> EdgeView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::{EdgeKey, EdgeKeyError, EdgeView}; + /// use delaunay::prelude::tds::EdgeKeyError; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -525,8 +519,8 @@ impl<'tds, U, V, const D: usize> EdgeView<'tds, U, V, D> { /// return Ok(()); /// }; /// - /// let key = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; - /// let view = EdgeView::try_new(dt.tds(), key)?; + /// let key = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// let view = dt.edge_view(key)?; /// assert_eq!(view.key(), key); /// # Ok(()) /// # } @@ -564,7 +558,7 @@ impl<'tds, U, V, const D: usize> EdgeView<'tds, U, V, D> { /// Returns the borrowed TDS backing this view. #[inline] #[must_use] - pub const fn tds(&self) -> &'tds Tds { + pub(crate) const fn tds(&self) -> &'tds Tds { self.tds } diff --git a/src/core/facet.rs b/src/core/facet.rs index ff9aa8f5..1c22ab25 100644 --- a/src/core/facet.rs +++ b/src/core/facet.rs @@ -42,7 +42,6 @@ //! //! ```rust //! use delaunay::prelude::*; -//! use delaunay::prelude::tds::FacetView; //! //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { @@ -71,7 +70,7 @@ //! }; //! //! // Create a facet view (facet 0 excludes vertex 0) -//! let facet = FacetView::try_new(dt.tds(), simplex_key, 0)?; +//! let facet = dt.facet_view(dt.facet_handle(simplex_key, 0)?)?; //! assert_eq!(facet.vertices().count(), 3); // Facet (triangle) in 3D has 3 vertices //! # Ok(()) //! # } @@ -321,7 +320,6 @@ pub enum FacetError { /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::tds::{FacetHandle, FacetView}; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -349,10 +347,10 @@ pub enum FacetError { /// }; /// /// // Create a facet handle -/// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 0)?; +/// let handle = dt.facet_handle(simplex_key, 0)?; /// /// // Use it to create a FacetView -/// let facet = handle.view(dt.tds())?; +/// let facet = dt.facet_view(handle)?; /// # let _ = facet; /// # Ok(()) /// # } @@ -376,7 +374,6 @@ impl FacetHandle { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetHandle; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -397,7 +394,7 @@ impl FacetHandle { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 0)?; + /// let handle = dt.facet_handle(simplex_key, 0)?; /// assert_eq!(handle.facet_index(), 0); /// # Ok(()) /// # } @@ -443,7 +440,6 @@ impl FacetHandle { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetHandle; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -466,7 +462,7 @@ impl FacetHandle { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 0)?; + /// let handle = dt.facet_handle(simplex_key, 0)?; /// assert_eq!(handle.simplex_key(), simplex_key); /// # Ok(()) /// # } @@ -482,7 +478,6 @@ impl FacetHandle { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetHandle; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -503,7 +498,7 @@ impl FacetHandle { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 1)?; + /// let handle = dt.facet_handle(simplex_key, 1)?; /// assert_eq!(handle.facet_index(), 1); /// # Ok(()) /// # } @@ -529,7 +524,6 @@ impl FacetHandle { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetHandle; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -550,8 +544,8 @@ impl FacetHandle { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 0)?; - /// let view = handle.view(dt.tds())?; + /// let handle = dt.facet_handle(simplex_key, 0)?; + /// let view = dt.facet_view(handle)?; /// /// assert_eq!(view.handle(), handle); /// # Ok(()) @@ -674,7 +668,7 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// Returns the TDS reference. #[inline] #[must_use] - pub const fn tds(&self) -> &'tds Tds { + pub(crate) const fn tds(&self) -> &'tds Tds { self.tds } @@ -687,7 +681,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetHandle; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -708,8 +701,8 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let handle = FacetHandle::try_new(dt.tds(), simplex_key, 0)?; - /// let view = handle.view(dt.tds())?; + /// let handle = dt.facet_handle(simplex_key, 0)?; + /// let view = dt.facet_view(handle)?; /// /// assert_eq!(view.handle(), handle); /// # Ok(()) @@ -749,7 +742,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetView; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -774,7 +766,7 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let facet = FacetView::try_new(dt.tds(), simplex_key, 0)?; + /// let facet = dt.facet_view(dt.facet_handle(simplex_key, 0)?)?; /// assert_eq!(facet.facet_index(), 0); /// # Ok(()) /// # } @@ -853,7 +845,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetView; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -876,7 +867,7 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// if let Some((simplex_key, _)) = dt.simplices().next() { - /// let facet = FacetView::try_new(dt.tds(), simplex_key, 0)?; + /// let facet = dt.facet_view(dt.facet_handle(simplex_key, 0)?)?; /// let vertex_iter = facet.vertices(); /// assert_eq!(vertex_iter.count(), 3); // 3D facet has 3 vertices /// } @@ -899,7 +890,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetView; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -924,7 +914,7 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// return Ok(()); /// }; /// - /// let facet = FacetView::try_new(dt.tds(), simplex_key, 1)?; + /// let facet = dt.facet_view(dt.facet_handle(simplex_key, 1)?)?; /// let opposite = facet.opposite_vertex(); /// assert_eq!(opposite.point().coords().len(), 3); /// # Ok(()) @@ -946,7 +936,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetView; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -971,7 +960,7 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// return Ok(()); /// }; /// - /// let facet = FacetView::try_new(dt.tds(), simplex_key, 2)?; + /// let facet = dt.facet_view(dt.facet_handle(simplex_key, 2)?)?; /// let simplex = facet.simplex(); /// assert_eq!(simplex.number_of_vertices(), 4); /// # Ok(()) @@ -996,7 +985,6 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// /// ```rust /// use delaunay::prelude::*; - /// use delaunay::prelude::tds::FacetView; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -1021,9 +1009,9 @@ impl<'tds, U, V, const D: usize> FacetView<'tds, U, V, D> { /// return Ok(()); /// }; /// - /// let facet = FacetView::try_new(dt.tds(), simplex_key, 0)?; + /// let facet = dt.facet_view(dt.facet_handle(simplex_key, 0)?)?; /// let facet_key = facet.key(); - /// let index = dt.tds().build_facet_to_simplices_index()?; + /// let index = dt.facet_incidence_index()?; /// assert!(index.get(&facet_key).is_some()); /// # Ok(()) /// # } @@ -1219,7 +1207,7 @@ impl<'tds, U, V, const D: usize> FacetIncidenceView<'_, 'tds, U, V, D> { /// Returns the TDS that produced the borrowed incidence index entry. #[inline] #[must_use] - pub const fn tds(self) -> &'tds Tds { + pub(crate) const fn tds(self) -> &'tds Tds { self.tds } @@ -1298,7 +1286,7 @@ impl<'tds, U, V, const D: usize> FacetToSimplicesIndex<'tds, U, V, D> { /// Returns the borrowed TDS that produced this index. #[inline] #[must_use] - pub const fn tds(&self) -> &'tds Tds { + pub(crate) const fn tds(&self) -> &'tds Tds { self.tds } @@ -1514,7 +1502,7 @@ impl Eq for FacetView<'_, U, V, D> {} /// return Ok(()); /// }; /// -/// let facets = dt.tds().try_simplex_facets(simplex_key)?; +/// let facets = dt.simplex_facets(simplex_key)?; /// assert_eq!(facets.len(), 4); /// # Ok(()) /// # } @@ -1633,7 +1621,7 @@ impl FusedIterator for SimplexFacetsIter<'_, U, V, D> {} /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let count = dt.tds().facets() +/// let count = dt.facets() /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; /// assert_eq!(count, 4); /// # Ok(()) @@ -1716,7 +1704,7 @@ impl<'tds, U, V, const D: usize> AllFacetsIter<'tds, U, V, D> { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let mut iter = dt.tds().facets(); + /// let mut iter = dt.facets(); /// assert!(iter.next().transpose()?.is_some()); /// # Ok(()) /// # } @@ -1777,8 +1765,7 @@ impl Tds { /// }; /// /// let facet_count = dt - /// .tds() - /// .try_simplex_facets(simplex_key)? + /// .simplex_facets(simplex_key)? /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; /// /// assert_eq!(facet_count, 4); @@ -1825,7 +1812,6 @@ impl Tds { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// let facet_count = dt - /// .tds() /// .facets() /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; /// @@ -2032,6 +2018,8 @@ impl FusedIterator for BoundaryFacetsIter<'_, U, V, D> {} /// # #[error(transparent)] /// # Tds(#[from] delaunay::prelude::tds::TdsError), /// # #[error(transparent)] +/// # Query(#[from] delaunay::query::QueryError), +/// # #[error(transparent)] /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { @@ -2044,9 +2032,10 @@ impl FusedIterator for BoundaryFacetsIter<'_, U, V, D> {} /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// let one_sided_count = dt -/// .tds() -/// .one_sided_facets()? -/// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; +/// .facet_incidence_index()? +/// .iter() +/// .filter(|incidence| incidence.is_one_sided()) +/// .count(); /// assert_eq!(one_sided_count, 4); /// # Ok(()) /// # } @@ -2545,14 +2534,6 @@ mod tests { /// - Facet equality tests /// /// For a D-dimensional simplex, a facet is (D-1)-dimensional and has D vertices. - /// - /// # Usage - /// - /// ```ignore - /// test_facet_dimensions! { - /// facet_2d => 2 => "triangle" => 2 => vec![delaunay::vertex![0.0, 0.0]?, ...], - /// } - /// ``` macro_rules! test_facet_dimensions { ($( $test_name:ident => $dim:expr => $desc:expr => $expected_facet_vertices:expr => $vertices:expr diff --git a/src/core/facet_incidence.rs b/src/core/facet_incidence.rs index 6e7d18aa..5a800e6d 100644 --- a/src/core/facet_incidence.rs +++ b/src/core/facet_incidence.rs @@ -6,8 +6,6 @@ #![forbid(unsafe_code)] -#[cfg(test)] -use super::collections::FacetToSimplicesMap; use super::{ facet::{FacetError, FacetToSimplicesIndex, FacetView, OneSidedFacetsIter}, tds::{Tds, TdsError}, @@ -15,29 +13,6 @@ use super::{ }; use std::ptr; -/// Counts one-sided raw facet incidences and rejects non-manifold multiplicities. -/// -/// This test helper exercises raw multiplicity parsing only. Production -/// topology-aware boundary classification uses [`FacetToSimplicesIndex`] so -/// admissible periodic self-identifications remain closed topology instead of -/// being counted as boundary. -#[cfg(test)] -fn number_of_one_sided_facets_in_map( - facet_to_simplices: &FacetToSimplicesMap, -) -> Result { - let mut count = 0usize; - for (&facet_key, simplices) in facet_to_simplices { - match simplices.len() { - 1 => count = count.saturating_add(1), - 2 => {} - found => { - return Err(FacetError::InvalidFacetMultiplicity { facet_key, found }.into()); - } - } - } - Ok(count) -} - /// Implementation of `FacetIncidenceAnalysis` trait for `Tds`. /// /// This implementation provides efficient one-sided facet incidence analysis @@ -81,23 +56,10 @@ impl FacetIncidenceAnalysis for Tds { /// /// # Examples /// - /// ``` + /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// // Create a simple 3D triangulation (single tetrahedron) /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, @@ -113,11 +75,12 @@ impl FacetIncidenceAnalysis for Tds { /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; /// assert_eq!(high_level_count, 4); /// - /// // TDS-level API reports raw one-sided incidence. + /// // Raw incidence is available through the owner-bound facet index. /// let count = dt - /// .tds() - /// .one_sided_facets()? - /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; + /// .facet_incidence_index()? + /// .iter() + /// .filter(|incidence| incidence.is_one_sided()) + /// .count(); /// assert_eq!(count, 4); /// # Ok(()) /// # } @@ -161,23 +124,10 @@ impl FacetIncidenceAnalysis for Tds { /// /// # Examples /// - /// ``` + /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -186,11 +136,16 @@ impl FacetIncidenceAnalysis for Tds { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// // Get one-sided facets using the TDS incidence API. - /// let Some(first_facet) = dt.tds().one_sided_facets()?.next().transpose()? else { + /// // Inspect raw one-sided incidence without exposing storage internals. + /// let facet_index = dt.facet_incidence_index()?; + /// let Some(first_facet) = facet_index + /// .iter() + /// .find_map(|incidence| incidence.one_sided_handle()) + /// else { /// return Ok(()); /// }; - /// assert!(dt.tds().is_one_sided_facet(&first_facet)?); + /// let facet = dt.facet_view(first_facet)?; + /// assert!(facet_index.is_one_sided_facet_key(&facet.key())); /// # Ok(()) /// # } /// ``` @@ -226,23 +181,10 @@ impl FacetIncidenceAnalysis for Tds { /// /// # Examples /// - /// ``` + /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -251,13 +193,12 @@ impl FacetIncidenceAnalysis for Tds { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// // Build the facet index once for multiple queries - /// let facet_to_simplices = dt.tds().build_facet_to_simplices_index()?; + /// // Build the owner-bound facet index once for multiple raw incidence queries. + /// let facet_to_simplices = dt.facet_incidence_index()?; /// - /// // Check one-sided incidence efficiently using the iterator API. - /// for facet in dt.tds().one_sided_facets()? { - /// let facet = facet?; - /// let is_one_sided = dt.tds().is_one_sided_facet_with_index(&facet, &facet_to_simplices)?; + /// // Check one-sided incidence efficiently from the borrowed index entries. + /// for incidence in facet_to_simplices.iter() { + /// let is_one_sided = incidence.is_one_sided(); /// println!("Facet is one-sided: {is_one_sided}"); /// } /// # Ok(()) @@ -292,21 +233,10 @@ impl FacetIncidenceAnalysis for Tds { /// /// # Examples /// - /// ``` + /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -316,7 +246,12 @@ impl FacetIncidenceAnalysis for Tds { /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// // A single Euclidean tetrahedron has 4 one-sided facets. - /// assert_eq!(dt.tds().number_of_one_sided_facets()?, 4); + /// let one_sided_count = dt + /// .facet_incidence_index()? + /// .iter() + /// .filter(|incidence| incidence.is_one_sided()) + /// .count(); + /// assert_eq!(one_sided_count, 4); /// # Ok(()) /// # } /// ``` @@ -358,7 +293,7 @@ fn ensure_facet_index_owner( #[cfg(test)] mod tests { - use super::{FacetIncidenceAnalysis, number_of_one_sided_facets_in_map}; + use super::FacetIncidenceAnalysis; use crate::core::collections::{FacetToSimplicesMap, SmallBuffer}; use crate::core::facet::{FacetError, FacetHandle, FacetToSimplicesIndex, FacetView}; use crate::core::query::QueryError; @@ -370,6 +305,27 @@ mod tests { use crate::vertex; use std::assert_matches; + /// Counts one-sided raw facet incidences and rejects non-manifold multiplicities. + /// + /// This helper exercises raw multiplicity parsing only. Production topology-aware + /// boundary classification uses `FacetToSimplicesIndex` so admissible periodic + /// self-identifications remain closed topology instead of being counted as boundary. + fn number_of_one_sided_facets_in_map( + facet_to_simplices: &FacetToSimplicesMap, + ) -> Result { + let mut count = 0usize; + for (&facet_key, simplices) in facet_to_simplices { + match simplices.len() { + 1 => count = count.saturating_add(1), + 2 => {} + found => { + return Err(FacetError::InvalidFacetMultiplicity { facet_key, found }.into()); + } + } + } + Ok(count) + } + #[cfg(feature = "diagnostics")] macro_rules! test_debug { ($($arg:tt)*) => {{ diff --git a/src/core/insertion.rs b/src/core/insertion.rs index 1185ea5a..6f1e75f6 100644 --- a/src/core/insertion.rs +++ b/src/core/insertion.rs @@ -42,8 +42,6 @@ use crate::locality::{ append_live_unique_simplex_seeds, collect_local_exterior_conflict_seed_simplices, replace_simplices_and_record_removed, retain_simplices_and_record_removed, }; -#[cfg(debug_assertions)] -use crate::topology::manifold::validate_ridge_links; use std::borrow::Cow; use std::env; use std::sync::{ @@ -1925,7 +1923,7 @@ where #[cfg(debug_assertions)] if env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { - match validate_ridge_links(&self.tds) { + match self.validate_ridge_links() { Ok(()) => { tracing::debug!( "insert_with_conflict_region: ridge-link validation passed after insertion" @@ -2169,7 +2167,7 @@ where self.tds = new_tds; // Re-map vertex key to the rebuilt TDS - v_key = self.tds.vertex_key_from_uuid(&inserted_uuid).ok_or( + v_key = self.vertex_key_from_uuid(&inserted_uuid).ok_or( CavityFillingError::RebuiltVertexMissing { uuid: inserted_uuid, }, @@ -2777,7 +2775,7 @@ where #[cfg(debug_assertions)] if env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { - match validate_ridge_links(&self.tds) { + match self.validate_ridge_links() { Ok(()) => { tracing::debug!( "extend_hull: ridge-link validation passed after insertion" @@ -2864,8 +2862,8 @@ mod tests { DUPLICATE_DETECTION_FORCE_ENABLED.load(AtomicOrdering::Relaxed) } - fn set_duplicate_detection_force_enabled(enabled: bool) { - DUPLICATE_DETECTION_FORCE_ENABLED.store(enabled, AtomicOrdering::Relaxed); + fn set_duplicate_detection_force_enabled(enabled: bool) -> bool { + DUPLICATE_DETECTION_FORCE_ENABLED.swap(enabled, AtomicOrdering::Relaxed) } pub(super) fn take_force_next_insertion_retryable_failure() -> bool { @@ -3131,16 +3129,25 @@ mod tests { #[test] fn test_duplicate_detection_metrics_force_enable() { - struct DuplicateDetectionGuard; + struct DuplicateDetectionGuard { + prior: bool, + } + + impl DuplicateDetectionGuard { + fn enable() -> Self { + Self { + prior: set_duplicate_detection_force_enabled(true), + } + } + } impl Drop for DuplicateDetectionGuard { fn drop(&mut self) { - set_duplicate_detection_force_enabled(false); + set_duplicate_detection_force_enabled(self.prior); } } - let _guard = DuplicateDetectionGuard; - set_duplicate_detection_force_enabled(true); + let _guard = DuplicateDetectionGuard::enable(); let before = Triangulation::, (), (), 2>::duplicate_detection_metrics() .expect("duplicate detection metrics should be enabled"); diff --git a/src/core/query.rs b/src/core/query.rs index 446e3833..1efa2225 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -7,19 +7,33 @@ use crate::core::adjacency::{ EdgeIndex, IncidenceView, SimplexNeighborIndex, TopologyIndexBuildError, TriangulationAdjacency, }; +use crate::core::algorithms::flips::{FlipError, RidgeHandle}; +use crate::core::algorithms::locate::{ + ConflictError, LocateError, LocateResult, LocateStats, + find_conflict_region as find_conflict_region_in_tds, locate as locate_in_tds, + locate_with_stats as locate_with_stats_in_tds, +}; use crate::core::collections::{ - FastHashMap, FastHashSet, MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer, - fast_hash_map_with_capacity, fast_hash_set_with_capacity, + FastHashMap, FastHashSet, MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer, Uuid, + VertexKeyBuffer, fast_hash_map_with_capacity, fast_hash_set_with_capacity, }; use crate::core::edge::{EdgeKey, EdgeKeyError, EdgeView}; -use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter, FacetHandle}; +use crate::core::facet::{ + AllFacetsIter, BoundaryFacetsIter, FacetError, FacetHandle, FacetToSimplicesIndex, FacetView, + SimplexFacetsIter, +}; use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, TdsError, VertexKey}; use crate::core::triangulation::Triangulation; use crate::core::util::usize_to_u8; use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +use crate::geometry::point::Point; use crate::topology::manifold::{ManifoldError, boundary_facet_handles_from_index}; -use std::marker::PhantomData; +use crate::topology::ridge::{ + RidgeCandidate, RidgeCandidateError, RidgeQuery, RidgeView, + ridge_star_simplices as ridge_star_simplices_in_tds, +}; /// Errors returned by read-only triangulation queries. /// @@ -78,6 +92,29 @@ pub enum QueryError { #[source] source: Box, }, + + /// A simplex-local ridge handle cannot represent the omitted vertex index. + #[error( + "ridge index {original_index} for simplex {simplex_key:?} cannot fit in u8 handle storage; simplex has {vertex_count} vertices" + )] + RidgeIndexCapacityExceeded { + /// Simplex whose local ridge index overflowed handle storage. + simplex_key: SimplexKey, + /// Local omitted-vertex index that could not fit in `u8`. + original_index: usize, + /// Number of vertices in the simplex. + vertex_count: usize, + }, + + /// A simplex produced invalid ridge vertices while building a topology ridge query. + #[error("simplex {simplex_key:?} produced an invalid topology ridge: {source}")] + InvalidRidgeCandidate { + /// Simplex whose vertex set produced the invalid ridge candidate. + simplex_key: SimplexKey, + /// Typed ridge-candidate validation failure. + #[source] + source: RidgeCandidateError, + }, } impl From for QueryError { @@ -139,6 +176,169 @@ impl Triangulation { self.tds.simplices() } + /// Returns simplex keys paired with their stable UUIDs. + /// + /// This is a zero-allocation identity iterator for diagnostics, snapshots, + /// and downstream bookkeeping that should not borrow the storage owner. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, simplex_uuid)) = tri.simplex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.simplex_uuid_from_key(simplex_key), Some(simplex_uuid)); + /// assert_eq!(tri.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + /// # Ok(()) + /// # } + /// ``` + pub fn simplex_uuids(&self) -> impl Iterator + '_ { + self.tds + .simplices() + .map(|(simplex_key, simplex)| (simplex_key, simplex.uuid())) + } + + /// Returns a read-only simplex view by key. + /// + /// This is the keyed counterpart to [`Triangulation::simplices`](Self::simplices). + /// It lends only the requested element instead of exposing the underlying + /// topology owner. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::SimplexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(tri.simplex(simplex_key).is_some()); + /// assert!(tri.simplex(SimplexKey::default()).is_none()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn simplex(&self, key: SimplexKey) -> Option<&Simplex> { + self.tds.simplex(key) + } + + /// Returns a simplex key for a stable simplex UUID. + /// + /// This is the owner-bound counterpart to + /// [`Tds::simplex_key_from_uuid`](crate::prelude::tds::Tds::simplex_key_from_uuid). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use uuid::Uuid; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, simplex_uuid)) = tri.simplex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + /// assert_eq!(tri.simplex_key_from_uuid(&Uuid::nil()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn simplex_key_from_uuid(&self, simplex_uuid: &Uuid) -> Option { + self.tds.simplex_key_from_uuid(simplex_uuid) + } + + /// Returns the stable UUID for a live simplex key. + /// + /// This is the owner-bound counterpart to + /// [`Tds::simplex_uuid_from_key`](crate::prelude::tds::Tds::simplex_uuid_from_key). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::SimplexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, simplex_uuid)) = tri.simplex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.simplex_uuid_from_key(simplex_key), Some(simplex_uuid)); + /// assert_eq!(tri.simplex_uuid_from_key(SimplexKey::default()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn simplex_uuid_from_key(&self, simplex_key: SimplexKey) -> Option { + self.tds.simplex_uuid_from_key(simplex_key) + } + + /// Returns `true` when `key` identifies a live simplex in this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::SimplexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(tri.contains_simplex(simplex_key)); + /// assert!(!tri.contains_simplex(SimplexKey::default())); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn contains_simplex(&self, key: SimplexKey) -> bool { + self.tds.contains_simplex(key) + } + /// Returns an iterator over all vertices in the triangulation. /// /// Delegates to the underlying Tds. @@ -178,6 +378,169 @@ impl Triangulation { self.tds.vertices() } + /// Returns vertex keys paired with their stable UUIDs. + /// + /// This is a zero-allocation identity iterator for diagnostics, snapshots, + /// and downstream bookkeeping that should not borrow the storage owner. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((vertex_key, vertex_uuid)) = tri.vertex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.vertex_uuid_from_key(vertex_key), Some(vertex_uuid)); + /// assert_eq!(tri.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + /// # Ok(()) + /// # } + /// ``` + pub fn vertex_uuids(&self) -> impl Iterator + '_ { + self.tds + .vertices() + .map(|(vertex_key, vertex)| (vertex_key, vertex.uuid())) + } + + /// Returns a read-only vertex view by key. + /// + /// This is the keyed counterpart to [`Triangulation::vertices`](Self::vertices). + /// It lends only the requested element instead of exposing the underlying + /// topology owner. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::VertexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((vertex_key, _)) = tri.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(tri.vertex(vertex_key).is_some()); + /// assert!(tri.vertex(VertexKey::default()).is_none()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn vertex(&self, key: VertexKey) -> Option<&Vertex> { + self.tds.vertex(key) + } + + /// Returns a vertex key for a stable vertex UUID. + /// + /// This is the owner-bound counterpart to + /// [`Tds::vertex_key_from_uuid`](crate::prelude::tds::Tds::vertex_key_from_uuid). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use uuid::Uuid; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((vertex_key, vertex_uuid)) = tri.vertex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + /// assert_eq!(tri.vertex_key_from_uuid(&Uuid::nil()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn vertex_key_from_uuid(&self, vertex_uuid: &Uuid) -> Option { + self.tds.vertex_key_from_uuid(vertex_uuid) + } + + /// Returns the stable UUID for a live vertex key. + /// + /// This is the owner-bound counterpart to + /// [`Tds::vertex_uuid_from_key`](crate::prelude::tds::Tds::vertex_uuid_from_key). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::VertexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((vertex_key, vertex_uuid)) = tri.vertex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.vertex_uuid_from_key(vertex_key), Some(vertex_uuid)); + /// assert_eq!(tri.vertex_uuid_from_key(VertexKey::default()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn vertex_uuid_from_key(&self, vertex_key: VertexKey) -> Option { + self.tds.vertex_uuid_from_key(vertex_key) + } + + /// Returns `true` when `key` identifies a live vertex in this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::tds::VertexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((vertex_key, _)) = tri.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(tri.contains_vertex_key(vertex_key)); + /// assert!(!tri.contains_vertex_key(VertexKey::default())); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn contains_vertex_key(&self, key: VertexKey) -> bool { + self.tds.contains_vertex_key(key) + } + /// Returns the number of vertices in the triangulation. /// /// # Examples @@ -244,95 +607,589 @@ impl Triangulation { self.tds.number_of_simplices() } - /// Returns the dimension of the triangulation. + /// Returns the topology generation counter for this triangulation. + /// + /// The value changes after topology mutations and is useful for detecting + /// stale detached handles in tests and diagnostics. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// + /// assert_eq!(tri.topology_generation(), tri.topology_generation()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn topology_generation(&self) -> u64 { + self.tds.generation() + } + + /// Returns whether adjacent simplices have coherent opposite facet orientations. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// assert!(dt.as_triangulation().is_coherently_oriented()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn is_coherently_oriented(&self) -> bool { + self.tds.is_coherently_oriented() + } + + /// Returns the dimension of the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::*; + /// use delaunay::prelude::*; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Source(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// // Empty triangulation has dimension -1 + /// let empty: Triangulation, (), (), 3> = + /// Triangulation::new_empty(FastKernel::new()); + /// assert_eq!(empty.dim(), -1); + /// + /// // 3D tetrahedron has dimension 3 + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// assert_eq!(dt.as_triangulation().dim(), 3); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn dim(&self) -> i32 { + self.tds.dim() + } + + /// Returns an iterator over all facets in the triangulation. + /// + /// This provides efficient access to all facets without pre-allocating a vector. + /// Each successful facet is a lightweight `FacetView` that references the + /// underlying triangulation data. + /// + /// # Returns + /// + /// An iterator yielding `Result` items for all facets + /// in the triangulation. + /// + /// Individual iterator items return [`FacetError`] + /// if a facet view cannot be constructed from the current TDS state. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Query(#[from] delaunay::query::QueryError), + /// # #[error(transparent)] + /// # Facet(#[from] delaunay::prelude::tds::FacetError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// // Iterate over all facets + /// let facet_count = dt + /// .as_triangulation() + /// .facets() + /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; + /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn facets(&self) -> AllFacetsIter<'_, U, V, D> { + self.tds.facets() + } + + /// Returns an iterator over all facets of one simplex. + /// + /// # Errors + /// + /// Returns [`FacetError`] if `simplex_key` is missing or this dimension + /// cannot be represented by public facet-index storage. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(tri.simplex_facets(simplex_key)?.count(), 4); + /// # Ok(()) + /// # } + /// ``` + pub fn simplex_facets( + &self, + simplex_key: SimplexKey, + ) -> Result, FacetError> { + self.tds.try_simplex_facets(simplex_key) + } + + /// Validates and returns a simplex-local facet handle. + /// + /// This is the owner-bound counterpart to [`FacetHandle::try_new`]. It lets + /// callers validate a runtime simplex key and facet index without borrowing + /// the underlying storage. + /// + /// # Errors + /// + /// Returns [`FacetError`] if `simplex_key` is missing or `facet_index` is + /// outside the simplex's local facet range. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let facet = tri.facet_handle(simplex_key, 0)?; + /// assert_eq!(facet.simplex_key(), simplex_key); + /// # Ok(()) + /// # } + /// ``` + pub fn facet_handle( + &self, + simplex_key: SimplexKey, + facet_index: u8, + ) -> Result { + FacetHandle::try_new(&self.tds, simplex_key, facet_index) + } + + /// Revalidates a facet handle and returns a borrowed facet view. + /// + /// # Errors + /// + /// Returns [`FacetError`] if the handle is stale or no longer identifies a + /// live simplex-local facet. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// let facet = tri.facet_handle(simplex_key, 0)?; + /// + /// let view = tri.facet_view(facet)?; + /// assert_eq!(view.handle(), facet); + /// # Ok(()) + /// # } + /// ``` + pub fn facet_view(&self, facet: FacetHandle) -> Result, FacetError> { + facet.view(&self.tds) + } + + /// Builds the owner-bound facet-to-simplices incidence index. + /// + /// # Errors + /// + /// Returns [`TdsError`] if the triangulation's facet incidence is + /// structurally inconsistent. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// + /// let index = tri.facet_incidence_index()?; + /// assert_eq!(index.iter().filter(|facet| facet.is_one_sided()).count(), 3); + /// # Ok(()) + /// # } + /// ``` + pub fn facet_incidence_index(&self) -> Result, TdsError> { + self.tds.build_facet_to_simplices_index() + } + + /// Returns unique topology ridges in the triangulation. + /// + /// A ridge is a codimension-two face. In 2D, ridges are vertices; in 3D, + /// ridges are edges. Dimensions below 2 have no topological ridges and + /// yield an empty iterator. + /// + /// ## Allocation and iteration order + /// + /// This method streams ridge candidates while maintaining an internal + /// deduplication set for ridges shared by multiple simplices. The iteration + /// order follows the current simplex storage order and is not stable across + /// topology edits. + /// + /// # Errors + /// + /// Individual iterator items return [`QueryError::InvalidRidgeCandidate`] + /// if stored simplex vertices cannot form a valid codimension-two ridge. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Query(#[from] delaunay::query::QueryError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let ridge_count = dt + /// .as_triangulation() + /// .ridges() + /// .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?; + /// assert_eq!(ridge_count, 3); + /// # Ok(()) + /// # } + /// ``` + pub fn ridges(&self) -> impl Iterator, QueryError>> + '_ { + let simplex_cap = self.tds.number_of_simplices(); + let ridges_per_simplex = (D + 1).saturating_mul(D) / 2; + let mut seen = fast_hash_set_with_capacity(simplex_cap.saturating_mul(ridges_per_simplex)); + + self.tds + .simplices() + .filter(move |_| D >= 2) + .flat_map(|(simplex_key, simplex)| { + let vertices = simplex.vertices(); + (0..vertices.len()).flat_map(move |omit_a| { + ((omit_a + 1)..vertices.len()).map(move |omit_b| { + ridge_candidate_from_simplex_vertices::( + simplex_key, + vertices, + omit_a, + omit_b, + ) + }) + }) + }) + .filter_map(move |result| match result { + Ok(ridge) => { + if seen.insert(ridge.clone()) { + Some(Ok(ridge)) + } else { + None + } + } + Err(error) => Some(Err(error)), + }) + } + + /// Returns simplex-local ridge handles for `K3` Pachner moves. + /// + /// These handles identify codimension-two faces by a containing simplex and + /// two omitted vertex indices, matching the representation required by + /// `K3` Pachner moves. Dimensions below 3 have no `K3` Pachner ridge + /// candidates and yield an empty iterator. + /// + /// ## Allocation and iteration order + /// + /// This method streams handles in the current simplex storage order. The + /// order is not stable across topology edits. + /// + /// # Errors + /// + /// Individual iterator items return [`QueryError::RidgeIndexCapacityExceeded`] + /// if a simplex-local omitted vertex index cannot fit in the public + /// [`RidgeHandle`] index storage. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Query(#[from] delaunay::query::QueryError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let ridge_count = dt + /// .as_triangulation() + /// .ridge_handles() + /// .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?; + /// assert_eq!(ridge_count, 6); + /// # Ok(()) + /// # } + /// ``` + pub fn ridge_handles(&self) -> impl Iterator> + '_ { + self.tds + .simplices() + .filter(move |_| D >= 3) + .flat_map(|(simplex_key, simplex)| { + ridge_handles_for_simplex(simplex_key, simplex.number_of_vertices()) + }) + } + + /// Validates and returns a simplex-local ridge handle. + /// + /// This is the owner-bound counterpart to [`RidgeHandle::try_new`]. It lets + /// callers validate runtime omitted-vertex indices without borrowing the + /// underlying storage. + /// + /// # Errors + /// + /// Returns [`FlipError`] if the dimension does not support ridge flips, the + /// simplex key is missing, or either omitted vertex index is invalid. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((simplex_key, _)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let ridge = tri.ridge_handle(simplex_key, 0, 1)?; + /// assert_eq!(ridge.simplex_key(), simplex_key); + /// # Ok(()) + /// # } + /// ``` + pub fn ridge_handle( + &self, + simplex_key: SimplexKey, + omit_a: u8, + omit_b: u8, + ) -> Result { + RidgeHandle::try_new(&self.tds, simplex_key, omit_a, omit_b) + } + + /// Returns the simplex star incident to a ridge candidate. + /// + /// # Errors + /// + /// Returns [`ManifoldError`] if any ridge vertex is stale or incidence + /// bookkeeping cannot be traversed. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::query::RidgeCandidate; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Ok(ridge) = RidgeCandidate::<2>::try_from_vertices( + /// tri.vertices().map(|(key, _)| key).take(1), + /// ) else { + /// return Ok(()); + /// }; + /// + /// assert!(!tri.ridge_star_simplices(&ridge)?.is_empty()); + /// # Ok(()) + /// # } + /// ``` + pub fn ridge_star_simplices( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + ridge_star_simplices_in_tds(&self.tds, ridge_candidate) + } + + /// Revalidates a ridge candidate and returns a borrowed ridge query. + /// + /// Unlike [`Triangulation::ridge_view`], the query permits an empty + /// incident star. + /// + /// # Errors + /// + /// Returns [`ManifoldError`] if any ridge vertex is stale or incidence + /// bookkeeping cannot be traversed. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::geometry::*; /// use delaunay::prelude::*; + /// use delaunay::prelude::query::RidgeCandidate; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// // Empty triangulation has dimension -1 - /// let empty: Triangulation, (), (), 3> = - /// Triangulation::new_empty(FastKernel::new()); - /// assert_eq!(empty.dim(), -1); - /// - /// // 3D tetrahedron has dimension 3 - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert_eq!(dt.as_triangulation().dim(), 3); + /// let tri = dt.as_triangulation(); + /// let Ok(ridge) = RidgeCandidate::<2>::try_from_vertices( + /// tri.vertices().map(|(key, _)| key).take(1), + /// ) else { + /// return Ok(()); + /// }; + /// + /// let query = tri.ridge_query(&ridge)?; + /// assert_eq!(query.ridge_candidate(), &ridge); /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn dim(&self) -> i32 { - self.tds.dim() + pub fn ridge_query( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + ridge_candidate.query(&self.tds) } - /// Returns an iterator over all facets in the triangulation. - /// - /// This provides efficient access to all facets without pre-allocating a vector. - /// Each successful facet is a lightweight `FacetView` that references the - /// underlying triangulation data. - /// - /// # Returns + /// Revalidates a ridge candidate and returns a borrowed ridge view. /// - /// An iterator yielding `Result` items for all facets - /// in the triangulation. + /// # Errors /// - /// Individual iterator items return [`FacetError`](crate::prelude::tds::FacetError) - /// if a facet view cannot be constructed from the current TDS state. + /// Returns [`ManifoldError`] if any ridge vertex is stale, incidence + /// bookkeeping cannot be traversed, or the ridge has an empty star. /// /// # Examples /// /// ```rust /// use delaunay::prelude::*; + /// use delaunay::prelude::query::RidgeCandidate; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Ok(ridge) = RidgeCandidate::<2>::try_from_vertices( + /// tri.vertices().map(|(key, _)| key).take(1), + /// ) else { + /// return Ok(()); + /// }; /// - /// // Iterate over all facets - /// let facet_count = dt - /// .as_triangulation() - /// .facets() - /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; - /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets + /// let view = tri.ridge_view(&ridge)?; + /// assert_eq!(view.ridge_candidate(), &ridge); /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn facets(&self) -> AllFacetsIter<'_, U, V, D> { - self.tds.facets() + pub fn ridge_view( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + ridge_candidate.view(&self.tds) } /// Returns an iterator over boundary (hull) facets in the triangulation. @@ -389,7 +1246,7 @@ impl Triangulation { /// topology-aware boundary classification detects a closed topology that /// cannot contain open boundary facets, or another manifold-boundary /// inconsistency. - /// Individual iterator items return [`FacetError`](crate::prelude::tds::FacetError) + /// Individual iterator items return [`FacetError`] /// if a boundary facet handle cannot be reborrowed as a view. pub fn boundary_facets(&self) -> Result, QueryError> { let facet_index = self @@ -452,6 +1309,73 @@ impl Triangulation { self.collect_edges().into_iter() } + /// Validates and returns an edge key for two live vertices. + /// + /// # Errors + /// + /// Returns [`EdgeKeyError`] if the vertices are stale, duplicated, or do not + /// share a stored simplex edge. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((_simplex_key, simplex)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let edge = tri.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// assert!(tri.edges().any(|candidate| candidate == edge)); + /// # Ok(()) + /// # } + /// ``` + pub fn edge_key(&self, a: VertexKey, b: VertexKey) -> Result { + EdgeKey::try_new(&self.tds, a, b) + } + + /// Revalidates an edge key and returns a borrowed edge view. + /// + /// # Errors + /// + /// Returns [`EdgeKeyError`] if the key is stale or the maintained incidence + /// relation cannot prove a live edge star. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let Some((_simplex_key, simplex)) = tri.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let key = tri.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// let view = tri.edge_view(key)?; + /// assert_eq!(view.key(), key); + /// # Ok(()) + /// # } + /// ``` + pub fn edge_view(&self, edge: EdgeKey) -> Result, EdgeKeyError> { + edge.view(&self.tds) + } + /// Returns the number of unique edges in the triangulation. /// /// This is equivalent to `self.edges().count()`. @@ -513,6 +1437,161 @@ impl Triangulation { .filter(|&neighbor_key| self.tds.contains_simplex(neighbor_key)) } + /// Locates a point in this triangulation using the owner's kernel. + /// + /// This is the owner-bound counterpart to the low-level + /// [`locate_in_tds`](crate::prelude::algorithms::locate) function. It keeps + /// callers on the `Triangulation` API while preserving the same typed + /// location result. + /// + /// # Errors + /// + /// Returns [`LocateError`] if structural data or predicates fail during the + /// facet-walking query. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::algorithms::{LocateError, LocateResult}; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Locate(#[from] LocateError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// let location = dt.as_triangulation().locate(&query, None)?; + /// std::assert_matches!(location, LocateResult::InsideSimplex(_)); + /// # Ok(()) + /// # } + /// ``` + pub fn locate( + &self, + point: &Point, + hint: Option, + ) -> Result + where + K: Kernel, + { + locate_in_tds(&self.tds, &self.kernel, point, hint) + } + + /// Locates a point and returns facet-walk traversal statistics. + /// + /// # Errors + /// + /// Returns [`LocateError`] if structural data or predicates fail during the + /// facet-walking query. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::algorithms::LocateError; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Locate(#[from] LocateError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// let (_location, stats) = dt.as_triangulation().locate_with_stats(&query, None)?; + /// assert!(!stats.fell_back_to_scan()); + /// # Ok(()) + /// # } + /// ``` + pub fn locate_with_stats( + &self, + point: &Point, + hint: Option, + ) -> Result<(LocateResult, LocateStats), LocateError> + where + K: Kernel, + { + locate_with_stats_in_tds(&self.tds, &self.kernel, point, hint) + } + + /// Finds the conflict region for inserting `point` from a known start simplex. + /// + /// This is the owner-bound counterpart to the low-level + /// [`find_conflict_region_in_tds`](crate::prelude::algorithms::find_conflict_region) + /// function. + /// + /// # Errors + /// + /// Returns [`ConflictError`] if the start simplex is stale, structural data is + /// inconsistent, or the conflict boundary cannot be classified. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// use delaunay::prelude::algorithms::{ConflictError, LocateError, LocateResult}; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Locate(#[from] LocateError), + /// # #[error(transparent)] + /// # Conflict(#[from] ConflictError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let tri = dt.as_triangulation(); + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// if let LocateResult::InsideSimplex(start) = tri.locate(&query, None)? { + /// let conflict = tri.find_conflict_region(&query, start)?; + /// assert!(!conflict.is_empty()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn find_conflict_region( + &self, + point: &Point, + start_simplex: SimplexKey, + ) -> Result + where + K: Kernel, + { + find_conflict_region_in_tds(&self.tds, &self.kernel, point, start_simplex) + } + /// Returns an iterator over all unique edges incident to a vertex. /// /// If `v` is not present in this triangulation, the iterator is empty. @@ -675,7 +1754,7 @@ impl Triangulation { Ok(EdgeIndex { edge_count: seen_edges.len(), vertex_to_edges, - _tds: PhantomData, + _source_incidence: self.tds.vertex_to_simplices_index(), }) } @@ -727,7 +1806,7 @@ impl Triangulation { Ok(SimplexNeighborIndex { simplex_to_neighbors, - _tds: PhantomData, + _source_incidence: self.tds.vertex_to_simplices_index(), }) } @@ -772,6 +1851,52 @@ impl Triangulation { } } +/// Builds one topology ridge candidate from a simplex after omitting two vertices. +fn ridge_candidate_from_simplex_vertices( + simplex_key: SimplexKey, + vertices: &[VertexKey], + omit_a: usize, + omit_b: usize, +) -> Result, QueryError> { + let mut ridge_vertices = VertexKeyBuffer::with_capacity(D.saturating_sub(1)); + for (index, &vertex_key) in vertices.iter().enumerate() { + if index != omit_a && index != omit_b { + ridge_vertices.push(vertex_key); + } + } + + RidgeCandidate::try_from_vertices(ridge_vertices).map_err(|source| { + QueryError::InvalidRidgeCandidate { + simplex_key, + source, + } + }) +} + +/// Streams simplex-local Pachner ridge handles for one simplex. +fn ridge_handles_for_simplex( + simplex_key: SimplexKey, + vertex_count: usize, +) -> impl Iterator> { + (0..vertex_count).flat_map(move |omit_a| { + ((omit_a + 1)..vertex_count).map(move |omit_b| { + let omit_a = + u8::try_from(omit_a).map_err(|_| QueryError::RidgeIndexCapacityExceeded { + simplex_key, + original_index: omit_a, + vertex_count, + })?; + let omit_b = + u8::try_from(omit_b).map_err(|_| QueryError::RidgeIndexCapacityExceeded { + simplex_key, + original_index: omit_b, + vertex_count, + })?; + Ok(RidgeHandle::from_validated(simplex_key, omit_a, omit_b)) + }) + }) +} + impl Triangulation { /// Returns one simplex-local facet handle for an interior 2D edge. /// @@ -822,7 +1947,7 @@ impl Triangulation { /// return Ok(()); /// }; /// - /// let boundary_edge = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; + /// let boundary_edge = tri.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; /// assert!(tri.try_interior_facet_for_edge_2d(boundary_edge)?.is_none()); /// # Ok(()) /// # } @@ -880,7 +2005,7 @@ impl Triangulation { /// return Ok(()); /// }; /// - /// let boundary_edge = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; + /// let boundary_edge = tri.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; /// assert_eq!(tri.try_incident_facets_to_edge_2d(boundary_edge)?.count(), 1); /// # Ok(()) /// # } @@ -1113,11 +2238,35 @@ mod tests { .unwrap(), 0 ); + assert_eq!( + empty + .ridges() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1)) + .unwrap(), + 0 + ); + assert_eq!( + empty + .ridge_handles() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1)) + .unwrap(), + 0 + ); let vertices = vec![ $(vertex!($simplex_coords).unwrap()),+ ]; let expected_vertex_count = vertices.len(); + let expected_ridge_count = if $dim >= 2 { + expected_vertex_count * (expected_vertex_count - 1) / 2 + } else { + 0 + }; + let expected_ridge_handle_count = if $dim >= 3 { + expected_ridge_count + } else { + 0 + }; let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) @@ -1145,6 +2294,18 @@ mod tests { .unwrap(), expected_vertex_count ); + assert_eq!( + tri.ridges() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1)) + .unwrap(), + expected_ridge_count + ); + assert_eq!( + tri.ridge_handles() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1)) + .unwrap(), + expected_ridge_handle_count + ); } } }; diff --git a/src/core/repair.rs b/src/core/repair.rs index 5db09ca8..3a382792 100644 --- a/src/core/repair.rs +++ b/src/core/repair.rs @@ -21,8 +21,6 @@ use crate::core::tds::{InvariantError, SimplexKey, TdsError, VertexKey}; use crate::core::traits::data_type::DataType; use crate::core::triangulation::Triangulation; use crate::core::validation::{TriangulationValidationError, insertion_error_to_invariant_error}; -#[cfg(test)] -use crate::deletion::DeleteVertexError; use crate::geometry::kernel::Kernel; use crate::geometry::quality::{QualityError, QualitySimplexVerticesError, radius_ratio}; use std::env; @@ -1344,7 +1342,7 @@ where /// DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// // Empty issues map => nothing to remove. - /// let mut tri = dt.as_triangulation().clone(); + /// let mut tri = dt.into_triangulation(); /// let removed = tri.repair_local_facet_issues(&FacetIssuesMap::default(), 0)?; /// assert_eq!(removed, 0); /// # Ok(()) @@ -1409,6 +1407,7 @@ mod tests { use crate::core::simplex::{NeighborSlot, Simplex}; use crate::core::tds::Tds; use crate::core::vertex::Vertex; + use crate::deletion::DeleteVertexError; use crate::geometry::kernel::FastKernel; use crate::vertex; use std::assert_matches; @@ -2011,7 +2010,7 @@ mod tests { }) .map(|(key, _)| key) .unwrap(); - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); let outcome = tri.remove_vertex_with_repair_seeds(vertex_key).unwrap(); @@ -2037,7 +2036,7 @@ mod tests { ]; let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); let missing_vertex = VertexKey::from(KeyData::from_ffi(0xBAD)); let vertex_count = tri.tds.number_of_vertices(); let simplex_count = tri.tds.number_of_simplices(); @@ -2063,7 +2062,7 @@ mod tests { ]; let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); let isolated_vertex = tri .tds .insert_vertex_with_mapping(vertex![0.5, 0.5, 0.5].unwrap()) @@ -2360,7 +2359,7 @@ mod tests { ]; let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); // Add a duplicate simplex with the same vertices as an existing simplex. let (_, existing_simplex) = tri.tds.simplices().next().unwrap(); diff --git a/src/core/simplex.rs b/src/core/simplex.rs index 82b45c57..6c7ce6fa 100644 --- a/src/core/simplex.rs +++ b/src/core/simplex.rs @@ -19,14 +19,7 @@ //! ```rust //! use delaunay::prelude::*; //! -//! # #[derive(Debug, thiserror::Error)] -//! # enum ExampleError { -//! # #[error(transparent)] -//! # Source(#[from] DelaunayTriangulationConstructionError), -//! # #[error(transparent)] -//! # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -//! # } -//! # fn main() -> Result<(), ExampleError> { +//! # fn main() -> DelaunayResult<()> { //! // Create vertices for a tetrahedron //! let vertices = vec![ //! delaunay::vertex![0.0, 0.0, 0.0]?, @@ -69,8 +62,6 @@ use super::{ traits::{DataDeserialize, DataSerialize}, util::{UuidValidationError, make_uuid, validate_uuid}, }; -#[cfg(test)] -use crate::core::collections::FastHashSet; use crate::core::collections::{ FastHashMap, NeighborBuffer, PeriodicOffsetBuffer, SimplexVertexKeyBuffer, SimplexVertexUuidBuffer, fast_hash_map_with_capacity, @@ -82,8 +73,6 @@ use serde::{ de::{self, IgnoredAny, MapAccess, Visitor}, ser::SerializeStruct, }; -#[cfg(test)] -use std::iter::once; use std::{ cmp, fmt::{self, Debug}, @@ -334,14 +323,7 @@ impl NeighborSlot { /// use delaunay::prelude::collections::Uuid; /// use delaunay::prelude::*; /// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Source(#[from] DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { +/// # fn main() -> DelaunayResult<()> { /// // Create a triangulation with some vertices /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, @@ -354,9 +336,8 @@ impl NeighborSlot { /// let Some((simplex_key, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; -/// let tds = dt.tds(); /// for &vertex_key in simplex.vertices() { -/// let Some(vertex) = tds.vertex(vertex_key) else { +/// let Some(vertex) = dt.vertex(vertex_key) else { /// return Ok(()); /// }; /// // use vertex... @@ -695,17 +676,10 @@ impl Simplex { /// /// # Example /// - /// ```rust + /// ``` /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -742,17 +716,10 @@ impl Simplex { /// /// # Example /// - /// ```rust + /// ``` /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -790,14 +757,7 @@ impl Simplex { /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -838,14 +798,7 @@ impl Simplex { /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -855,12 +808,11 @@ impl Simplex { /// let Some((_, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let tds = dt.tds(); /// /// if let Some(neighbors) = simplex.neighbors() { /// for (i, neighbor_key_opt) in neighbors.enumerate() { /// if let Some(neighbor_key) = neighbor_key_opt { - /// let Some(neighbor_simplex) = tds.simplex(neighbor_key) else { + /// let Some(neighbor_simplex) = dt.simplex(neighbor_key) else { /// continue; /// }; /// // neighbor_simplex is opposite to vertex i @@ -914,42 +866,20 @@ impl Simplex { /// /// # Examples /// - /// ```rust + /// ``` /// use delaunay::prelude::tds::NeighborSlot; /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let mut tds = dt.tds().clone(); - /// let Some(simplex_key) = tds.simplex_keys().next() else { - /// return Ok(()); - /// }; - /// - /// tds.set_neighbors_by_key(simplex_key, &[None, None, None])?; - /// let Some(simplex) = tds.simplex(simplex_key) else { + /// let Some((_, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// /// let Some(slots) = simplex.neighbor_slots() else { /// return Ok(()); /// }; @@ -978,20 +908,7 @@ impl Simplex { /// use delaunay::prelude::collections::Uuid; /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1001,10 +918,9 @@ impl Simplex { /// let Some((_, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let tds = dt.tds(); /// /// for &vkey in simplex.vertices() { - /// let Some(vertex) = tds.vertex(vkey) else { + /// let Some(vertex) = dt.vertex(vkey) else { /// continue; /// }; /// // use vertex data... @@ -1208,20 +1124,7 @@ impl Simplex { /// ```rust /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1232,8 +1135,7 @@ impl Simplex { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let tds = dt.tds(); - /// let Some(simplex) = tds.simplex(simplex_key) else { + /// let Some(simplex) = dt.simplex(simplex_key) else { /// return Ok(()); /// }; /// assert_eq!(simplex.number_of_vertices(), 4); @@ -1257,20 +1159,7 @@ impl Simplex { /// use delaunay::prelude::collections::Uuid; /// use delaunay::prelude::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 1.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1299,20 +1188,7 @@ impl Simplex { /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1369,37 +1245,25 @@ impl Simplex { /// # Examples /// /// ```rust - /// use delaunay::prelude::*; + /// use delaunay::prelude::construction::DelaunayResult; + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, /// delaunay::vertex![0.0, 1.0, 0.0]?, /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// let tds = + /// Triangulation::, (), (), 3>::build_initial_simplex(&vertices)?; + /// let Some((simplex_key, _)) = tds.simplices().next() else { /// return Ok(()); /// }; - /// let tds = dt.tds(); /// let Some(simplex) = tds.simplex(simplex_key) else { /// return Ok(()); /// }; - /// let uuids = simplex.vertex_uuids(tds)?; + /// let uuids = simplex.vertex_uuids(&tds)?; /// assert_eq!(uuids.len(), 4); /// # Ok(()) /// # } @@ -1439,36 +1303,26 @@ impl Simplex { /// # Examples /// /// ```rust - /// use delaunay::prelude::*; + /// use delaunay::prelude::construction::DelaunayResult; + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// let tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; + /// let Some((simplex_key, _)) = tds.simplices().next() else { /// return Ok(()); /// }; - /// let tds = dt.tds(); /// let Some(simplex) = tds.simplex(simplex_key) else { /// return Ok(()); /// }; - /// let uuids: Vec<_> = simplex.vertex_uuid_iter(tds).collect::, _>>()?; + /// let uuids: Vec<_> = simplex + /// .vertex_uuid_iter(&tds) + /// .collect::, _>>()?; /// assert_eq!(uuids.len(), 3); /// # Ok(()) /// # } @@ -1497,20 +1351,7 @@ impl Simplex { /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 1.0]?, /// delaunay::vertex![0.0, 1.0, 0.0]?, @@ -1552,22 +1393,7 @@ impl Simplex { /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// // Create two separate triangulations /// let vertices1 = vec![ /// delaunay::vertex![0.0, 0.0, 1.0]?, @@ -1585,10 +1411,10 @@ impl Simplex { /// DelaunayTriangulationBuilder::new(&vertices1).build()?; /// let dt2: DelaunayTriangulation<_, (), (), 3> = /// DelaunayTriangulationBuilder::new(&vertices2).build()?; - /// let Some((_, simplex1)) = dt1.tds().simplices().next() else { + /// let Some((_, simplex1)) = dt1.simplices().next() else { /// return Ok(()); /// }; - /// let Some((_, simplex2)) = dt2.tds().simplices().next() else { + /// let Some((_, simplex2)) = dt2.simplices().next() else { /// return Ok(()); /// }; /// let simplex1 = simplex1.clone(); @@ -1676,20 +1502,7 @@ impl Simplex { /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 1.0]?, /// delaunay::vertex![0.0, 1.0, 0.0]?, @@ -1833,35 +1646,21 @@ impl Simplex { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::construction::DelaunayResult; + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// // Example 1: Comparing simplices from different TDS instances with same coordinates /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt1: DelaunayTriangulation<_, _, _, 2> = - /// DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let dt2: DelaunayTriangulation<_, _, _, 2> = - /// DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds1 = dt1.tds(); - /// let tds2 = dt2.tds(); + /// let tds1 = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; + /// let tds2 = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// /// let Some((_, simplex1)) = tds1.simplices().next() else { /// return Ok(()); @@ -1871,28 +1670,16 @@ impl Simplex { /// }; /// /// // Different TDS instances, but same vertex coordinates - /// assert!(simplex1.eq_by_vertices(tds1, simplex2, tds2)); + /// assert!(simplex1.eq_by_vertices(&tds1, simplex2, &tds2)); /// # Ok(()) /// # } /// ``` /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::construction::DelaunayResult; + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// // Example 2: Comparing simplices with different coordinates returns false /// let vertices1 = vec![ /// delaunay::vertex![0.0, 0.0]?, @@ -1904,12 +1691,10 @@ impl Simplex { /// delaunay::vertex![2.0, 0.0]?, // Different coordinate /// delaunay::vertex![0.0, 2.0]?, // Different coordinate /// ]; - /// let dt1: DelaunayTriangulation<_, _, _, 2> = - /// DelaunayTriangulationBuilder::new(&vertices1).build()?; - /// let dt2: DelaunayTriangulation<_, _, _, 2> = - /// DelaunayTriangulationBuilder::new(&vertices2).build()?; - /// let tds1 = dt1.tds(); - /// let tds2 = dt2.tds(); + /// let tds1 = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices1)?; + /// let tds2 = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices2)?; /// /// let Some((_, simplex1)) = tds1.simplices().next() else { /// return Ok(()); @@ -1919,7 +1704,7 @@ impl Simplex { /// }; /// /// // Different coordinates mean simplices are not equal - /// assert!(!simplex1.eq_by_vertices(tds1, simplex2, tds2)); + /// assert!(!simplex1.eq_by_vertices(&tds1, simplex2, &tds2)); /// # Ok(()) /// # } /// ``` @@ -2040,6 +1825,7 @@ impl Hash for Simplex { mod tests { use super::*; use crate::builder::DelaunayTriangulationBuilder; + use crate::core::collections::FastHashSet; use crate::core::facet::FacetError; use crate::core::vertex::Vertex; use crate::geometry::kernel::AdaptiveKernel; @@ -2051,6 +1837,7 @@ mod tests { use crate::vertex; use approx::assert_relative_eq; use std::assert_matches; + use std::iter::once; use std::{ cmp, collections::{HashSet, hash_map::DefaultHasher}, @@ -2091,14 +1878,6 @@ mod tests { /// - Basic simplex creation and property validation /// - Serialization roundtrip (Some and None data) /// - UUID validation - /// - /// # Usage - /// - /// ```ignore - /// test_simplex_dimensions! { - /// simplex_2d => 2 => vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?], - /// } - /// ``` macro_rules! test_simplex_dimensions { ($( $test_name:ident => $dim:expr => $vertices:expr @@ -3393,10 +3172,10 @@ mod tests { let vertices = vec![vertex1, vertex2, vertex3, vertex4]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); let simplex_key = dt.simplices().next().unwrap().0; - let simplex = &dt.tds().simplex(simplex_key).unwrap(); + let simplex = dt.simplex(simplex_key).unwrap(); // Create a vertex key for the outside vertex - it won't be in the simplex - let outside_key = dt.tds().vertex_key_from_uuid(&vertex_outside.uuid()); + let outside_key = dt.vertex_key_from_uuid(&vertex_outside.uuid()); assert!(outside_key.is_none() || !simplex.contains_vertex(outside_key.unwrap())); } @@ -3563,23 +3342,19 @@ mod tests { // Get all facet views let facet_views = dt - .tds() - .try_simplex_facets(simplex_key) + .simplex_facets(simplex_key) .expect("Failed to get facet iterator") .collect::, _>>() .expect("Failed to get facet views"); for facet_view in &facet_views { let opposite_vertex = facet_view.opposite_vertex(); - let opposite_vertex_key = dt - .tds() - .vertex_key_from_uuid(&opposite_vertex.uuid()) - .unwrap(); + let opposite_vertex_key = dt.vertex_key_from_uuid(&opposite_vertex.uuid()).unwrap(); let facet_vertices = facet_view.vertices(); // Collect facet vertex keys let facet_vertex_keys: Vec<_> = facet_vertices - .map(|v| dt.tds().vertex_key_from_uuid(&v.uuid()).unwrap()) + .map(|v| dt.vertex_key_from_uuid(&v.uuid()).unwrap()) .collect(); // Verify the opposite vertex key is NOT in the facet vertices diff --git a/src/core/tds/keys.rs b/src/core/tds/keys.rs index 5570318c..9fa7f15a 100644 --- a/src/core/tds/keys.rs +++ b/src/core/tds/keys.rs @@ -34,7 +34,7 @@ new_key_type! { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some(key) = dt.tds().vertex_keys().next() else { + /// let Some((key, _)) = dt.vertices().next() else { /// return Ok(()); /// }; /// let _ = key; @@ -76,7 +76,7 @@ new_key_type! { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some(key) = dt.tds().simplex_keys().next() else { + /// let Some((key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; /// let _ = key; diff --git a/src/core/tds/mutation.rs b/src/core/tds/mutation.rs index 44201475..c0f6ee5f 100644 --- a/src/core/tds/mutation.rs +++ b/src/core/tds/mutation.rs @@ -5,7 +5,7 @@ use super::errors::{ }; use super::incidence::SimplexIncidenceRemoval; use super::storage::{SimplexUuidSortKey, Tds}; -use super::{SimplexKey, VertexKey}; +use super::{SimplexKey, TdsRollbackTransaction, VertexKey}; use crate::core::collections::{ CLEANUP_OPERATION_BUFFER_SIZE, Entry, FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, NeighborBuffer, SimplexKeySet, SimplexRemovalBuffer, SmallBuffer, VertexKeySet, @@ -13,13 +13,7 @@ use crate::core::collections::{ }; use crate::core::simplex::{NeighborSlot, Simplex}; use crate::core::vertex::Vertex; -#[cfg(test)] -use crate::deletion::DeleteVertexError; use std::collections::VecDeque; -use std::sync::{ - Arc, - atomic::{AtomicU64, Ordering}, -}; /// Topology validation mode for checked TDS simplex insertion. #[derive(Clone, Copy)] @@ -197,9 +191,9 @@ impl Tds { /// operations. It works entirely with vertex keys, simplex keys, and topological relationships. /// /// **Internal use only**: This method rebuilds ALL neighbor pointers from scratch, which is - /// inefficient for most use cases. For external use, prefer - /// [`repair_neighbor_pointers`](crate::prelude::insertion::repair_neighbor_pointers), - /// which rebuilds neighbor pointers from facet incidence. + /// inefficient for most use cases. Public callers should use owning + /// triangulation repair and validation workflows instead of mutating raw + /// neighbor storage directly. /// /// # Errors /// @@ -647,30 +641,30 @@ impl Tds { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::tds::TdsMutationError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Mutation(#[from] TdsMutationError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { - /// let vertices: [Vertex; 3] = [ + /// let vertices = [ /// delaunay::vertex![0.0, 0.0; data = 10i32]?, /// delaunay::vertex![1.0, 0.0; data = 20]?, /// delaunay::vertex![0.0, 1.0; data = 30]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let mut tds = dt.tds().clone(); + /// let mut tds = + /// Triangulation::, i32, (), 2>::build_initial_simplex(&vertices)?; /// let Some(key) = tds.vertex_keys().next() else { /// return Ok(()); /// }; @@ -731,21 +725,21 @@ impl Tds { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::tds::TdsMutationError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Mutation(#[from] TdsMutationError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = [ @@ -753,8 +747,8 @@ impl Tds { /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).simplex_data_type::().build()?; - /// let mut tds = dt.tds().clone(); + /// let mut tds = + /// Triangulation::, (), i32, 2>::build_initial_simplex(&vertices)?; /// let Some(key) = tds.simplex_keys().next() else { /// return Ok(()); /// }; @@ -848,21 +842,21 @@ impl Tds { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::tds::TdsMutationError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Mutation(#[from] TdsMutationError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = [ @@ -870,8 +864,8 @@ impl Tds { /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let mut tds = dt.tds().clone(); + /// let mut tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// let Some(simplex_key) = tds.simplex_keys().next() else { /// return Ok(()); /// }; @@ -1337,19 +1331,17 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::*; + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = [ @@ -1357,8 +1349,8 @@ impl Tds { /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); + /// let tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// let Some((simplex_key, _)) = tds.simplices().next() else { /// return Ok(()); /// }; @@ -1696,21 +1688,21 @@ impl Tds { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::tds::TdsMutationError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Mutation(#[from] TdsMutationError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = [ @@ -1718,8 +1710,8 @@ impl Tds { /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let mut tds = dt.tds().clone(); + /// let mut tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// let Some(simplex_key) = tds.simplex_keys().next() else { /// return Ok(()); /// }; @@ -1816,21 +1808,21 @@ impl Tds { /// /// # Examples /// - /// ``` - /// use delaunay::prelude::*; + /// ```rust + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::tds::TdsMutationError; + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, + /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { - /// # #[error(transparent)] Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] Insertion(#[from] delaunay::prelude::insertion::InsertionError), - /// # #[error(transparent)] Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # #[error(transparent)] TdsConstruction(#[from] delaunay::prelude::tds::TdsConstructionError), - /// # #[error(transparent)] Invariant(#[from] delaunay::prelude::tds::InvariantError), - /// # #[error(transparent)] Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] Simplex(#[from] delaunay::prelude::tds::SimplexValidationError), /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # Construction(#[from] TriangulationConstructionError), + /// # #[error(transparent)] + /// # Mutation(#[from] TdsMutationError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices = [ @@ -1838,8 +1830,8 @@ impl Tds { /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let mut tds = dt.tds().clone(); + /// let mut tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// tds.assign_incident_simplices()?; /// let all_assigned = tds.vertices().all(|(_, v)| v.incident_simplex().is_some()); /// assert!(all_assigned); @@ -2112,16 +2104,14 @@ impl Tds { /// /// Returns the number of duplicate simplices that were removed. /// - /// Duplicate removal is applied to a cloned trial [`Tds`], then the + /// Duplicate removal runs inside the shared rollback transaction, then the /// topology (neighbor relationships and incident simplices) is rebuilt to /// maintain data structure invariants and prevent stale references. If the - /// rebuild or validation fails, the original structure is left unchanged. + /// rebuild or validation fails, the original structure is restored. /// - /// When duplicates are present, the rollback guarantee is implemented by - /// cloning the current [`Tds`] before removal. This keeps failed mutations - /// atomic, but the snapshot cost is linear in the size of the stored - /// topology. The method therefore requires the stored coordinates and - /// payloads to be cloneable so the trial structure can preserve them. + /// When duplicates are present, the rollback snapshot cost is linear in the + /// size of the stored topology. The method therefore requires stored + /// coordinates and payloads to be cloneable so rollback can preserve them. /// /// # Errors /// @@ -2172,24 +2162,23 @@ impl Tds { return Ok(0); } - let original_generation = self.generation(); - let mut trial = self.clone_for_rollback(); - trial.generation = Arc::new(AtomicU64::new(original_generation)); - let removed = trial.remove_simplices_by_keys(&simplices_to_remove)?; + let mut transaction = TdsRollbackTransaction::begin(self); + let removed = transaction + .tds_mut() + .remove_simplices_by_keys(&simplices_to_remove)?; let rebuild_result = (|| -> Result<(), TdsMutationError> { - trial.assign_neighbors().map_err(TdsMutationError::from)?; - trial.assign_incident_simplices()?; - trial.is_valid().map_err(TdsMutationError::from)?; - Ok(()) + let tds = transaction.tds_mut(); + tds.assign_neighbors().map_err(TdsMutationError::from)?; + tds.assign_incident_simplices()?; + tds.is_valid().map_err(TdsMutationError::from) })(); if let Err(error) = rebuild_result { - self.generation - .store(original_generation, Ordering::Relaxed); + transaction.rollback(); return Err(error); } - *self = trial; + transaction.commit(); Ok(removed) } } @@ -2202,6 +2191,7 @@ mod tests { use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::simplex::Simplex; use crate::core::tds::errors::TriangulationConstructionState; + use crate::deletion::DeleteVertexError; use crate::geometry::point::Point; use crate::vertex; use slotmap::KeyData; @@ -2296,10 +2286,7 @@ mod tests { dt.number_of_simplices() >= initial_simplex_count, "Simplex count should not decrease" ); - assert!( - dt.as_triangulation().tds.is_valid().is_ok(), - "TDS should remain valid" - ); + assert!(dt.is_valid_structure().is_ok(), "TDS should remain valid"); } #[test] @@ -2315,18 +2302,14 @@ mod tests { assert_eq!(dt.number_of_vertices(), 5); // Vertex should be findable by UUID. - let vertex_key = dt.as_triangulation().tds.vertex_key_from_uuid(&uuid); + let vertex_key = dt.vertex_key_from_uuid(&uuid); assert!( vertex_key.is_some(), "Added vertex should be findable by UUID" ); // Vertex should be in the vertices collection. - let stored_vertex = dt - .as_triangulation() - .tds - .vertex(vertex_key.unwrap()) - .unwrap(); + let stored_vertex = dt.vertex(vertex_key.unwrap()).unwrap(); let coords = *stored_vertex.point().coords(); let expected = [1.0, 2.0, 3.0]; assert!( @@ -2369,10 +2352,7 @@ mod tests { // Verify the vertex was removed assert!( - dt.as_triangulation() - .tds - .vertex_key_from_uuid(&vertex_uuid) - .is_none(), + dt.vertex_key_from_uuid(&vertex_uuid).is_none(), "Vertex should be removed from TDS" ); assert!( @@ -2396,10 +2376,7 @@ mod tests { for (i, neighbor_opt) in neighbors.enumerate() { if let Some(neighbor_key) = neighbor_opt { assert!( - dt.as_triangulation() - .tds - .simplices - .contains_key(neighbor_key), + dt.contains_simplex(neighbor_key), "Simplex {simplex_key:?} has dangling neighbor reference at index {i}: {neighbor_key:?}" ); } @@ -2409,7 +2386,7 @@ mod tests { // Verify the TDS is valid (this should pass with the bug fix) assert!( - dt.as_triangulation().tds.is_valid().is_ok(), + dt.is_valid_structure().is_ok(), "TDS should be valid after removing vertex" ); } @@ -2496,7 +2473,7 @@ mod tests { let vertex_key = dt_2d.vertices().next().unwrap().0; let simplices_removed = dt_2d.delete_vertex(vertex_key).unwrap(); assert!(simplices_removed > 0); - assert!(dt_2d.as_triangulation().tds.is_valid().is_ok()); + assert!(dt_2d.is_valid_structure().is_ok()); } // 3D test @@ -2525,7 +2502,7 @@ mod tests { .0; let simplices_removed = dt_3d.delete_vertex(vertex_key).unwrap(); assert!(simplices_removed > 0); - assert!(dt_3d.as_triangulation().tds.is_valid().is_ok()); + assert!(dt_3d.is_valid_structure().is_ok()); } // 4D test @@ -2555,7 +2532,7 @@ mod tests { .0; let simplices_removed = dt_4d.delete_vertex(vertex_key).unwrap(); assert!(simplices_removed > 0); - assert!(dt_4d.as_triangulation().tds.is_valid().is_ok()); + assert!(dt_4d.is_valid_structure().is_ok()); } } @@ -2611,17 +2588,11 @@ mod tests { // CRITICAL CHECK 2: The vertex should no longer exist in TDS assert!( - dt.as_triangulation() - .tds - .vertex_key_from_uuid(&removed_vertex_uuid) - .is_none(), + dt.vertex_key_from_uuid(&removed_vertex_uuid).is_none(), "Deleted vertex UUID should not be in mapping" ); assert!( - dt.as_triangulation() - .tds - .vertex(removed_vertex_key) - .is_none(), + dt.vertex(removed_vertex_key).is_none(), "Deleted vertex key should not exist in storage" ); @@ -2629,19 +2600,12 @@ mod tests { for (vertex_key, vertex) in dt.vertices() { if let Some(incident_simplex_key) = vertex.incident_simplex() { assert!( - dt.as_triangulation() - .tds - .simplices - .contains_key(incident_simplex_key), + dt.contains_simplex(incident_simplex_key), "Vertex {vertex_key:?} has dangling incident_simplex pointer to {incident_simplex_key:?}" ); // Verify the incident simplex actually contains this vertex - let incident_simplex = dt - .as_triangulation() - .tds - .simplex(incident_simplex_key) - .unwrap(); + let incident_simplex = dt.simplex(incident_simplex_key).unwrap(); assert!( incident_simplex.contains_vertex(vertex_key), "Vertex {vertex_key:?} incident_simplex {incident_simplex_key:?} does not contain the vertex" @@ -2651,7 +2615,7 @@ mod tests { // CRITICAL CHECK 4: TDS should be valid assert!( - dt.as_triangulation().tds.is_valid().is_ok(), + dt.is_valid_structure().is_ok(), "TDS should be valid after vertex removal" ); } diff --git a/src/core/tds/storage.rs b/src/core/tds/storage.rs index c925ee91..8b5cfa8e 100644 --- a/src/core/tds/storage.rs +++ b/src/core/tds/storage.rs @@ -153,10 +153,10 @@ //! let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; //! //! // Level 2: structural only (fast) -//! assert!(dt.tds().is_valid().is_ok()); +//! assert!(dt.is_valid_structure().is_ok()); //! //! // Levels 1–2: elements + structural -//! assert!(dt.tds().validate().is_ok()); +//! assert!(dt.validate_structure().is_ok()); //! //! // Full report across Levels 1–4 //! match dt.validation_report() { @@ -450,9 +450,9 @@ pub trait TopologyOwner { /// [`Triangulation`](crate::prelude::triangulation::Triangulation) and /// [`crate::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. +/// Most users should construct triangulations via `DelaunayTriangulation` and use the +/// owner query and validation methods on that type. Use [`Tds::empty`](Self::empty) +/// for low-level or test scenarios where you want to manipulate the topology directly. /// /// ```rust /// use delaunay::prelude::*; @@ -1032,7 +1032,7 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let keys: Vec<_> = dt.tds().vertex_keys().collect(); + /// let keys: Vec<_> = dt.vertices().map(|(key, _)| key).collect(); /// assert_eq!(keys.len(), 3); /// # Ok(()) /// # } @@ -1071,7 +1071,7 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let keys: Vec<_> = dt.tds().simplex_keys().collect(); + /// let keys: Vec<_> = dt.simplices().map(|(key, _)| key).collect(); /// assert_eq!(keys.len(), 1); /// # Ok(()) /// # } @@ -1117,11 +1117,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(simplex_key) = tds.simplex_keys().next() else { + /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let Some(simplex) = tds.simplex(simplex_key) else { + /// let Some(simplex) = dt.simplex(simplex_key) else { /// return Ok(()); /// }; /// assert_eq!(simplex.number_of_vertices(), 3); @@ -1164,11 +1163,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(simplex_key) = tds.simplex_keys().next() else { + /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// assert!(tds.contains_simplex(simplex_key)); + /// assert!(dt.contains_simplex(simplex_key)); /// # Ok(()) /// # } /// ``` @@ -1532,10 +1530,11 @@ impl Tds { /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert!(dt.tds().is_connected()); + /// assert!(dt.as_triangulation().is_valid_topology().is_ok()); /// - /// let empty = dt.tds().number_of_simplices() == 0 || dt.tds().is_connected(); - /// assert!(empty); + /// let empty_or_connected = + /// dt.number_of_simplices() == 0 || dt.as_triangulation().is_valid_topology().is_ok(); + /// assert!(empty_or_connected); /// # Ok(()) /// # } /// ``` @@ -1724,11 +1723,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(simplex_key) = tds.simplex_keys().next() else { + /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let keys = tds.simplex_vertices(simplex_key)?; + /// let keys = dt.simplex_vertices(simplex_key)?; /// assert_eq!(keys.len(), 3); /// # Ok(()) /// # } @@ -1802,16 +1800,15 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first simplex and its UUID - /// let Some((simplex_key, simplex)) = tds.simplices().next() else { + /// let Some((simplex_key, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; /// let simplex_uuid = simplex.uuid(); /// /// // Use the helper function to find the simplex key from its UUID - /// let found_key = tds.simplex_key_from_uuid(&simplex_uuid); + /// let found_key = dt.simplex_key_from_uuid(&simplex_uuid); /// assert_eq!(found_key, Some(simplex_key)); /// # Ok(()) /// # } @@ -1879,16 +1876,15 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first vertex and its UUID - /// let Some((vertex_key, vertex)) = tds.vertices().next() else { + /// let Some((vertex_key, vertex)) = dt.vertices().next() else { /// return Ok(()); /// }; /// let vertex_uuid = vertex.uuid(); /// /// // Use the helper function to find the vertex key from its UUID - /// let found_key = tds.vertex_key_from_uuid(&vertex_uuid); + /// let found_key = dt.vertex_key_from_uuid(&vertex_uuid); /// assert_eq!(found_key, Some(vertex_key)); /// # Ok(()) /// # } @@ -1956,16 +1952,15 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first simplex key and expected UUID - /// let Some((simplex_key, simplex)) = tds.simplices().next() else { + /// let Some((simplex_key, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; /// let expected_uuid = simplex.uuid(); /// /// // Use the helper function to get UUID from the simplex key - /// let found_uuid = tds.simplex_uuid_from_key(simplex_key); + /// let found_uuid = dt.simplex_uuid_from_key(simplex_key); /// assert_eq!(found_uuid, Some(expected_uuid)); /// # Ok(()) /// # } @@ -1998,19 +1993,18 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first simplex's UUID - /// let Some((_, simplex)) = tds.simplices().next() else { + /// let Some((_, simplex)) = dt.simplices().next() else { /// return Ok(()); /// }; /// let original_uuid = simplex.uuid(); /// /// // Convert UUID to key, then key back to UUID - /// let Some(simplex_key) = tds.simplex_key_from_uuid(&original_uuid) else { + /// let Some(simplex_key) = dt.simplex_key_from_uuid(&original_uuid) else { /// return Ok(()); /// }; - /// let round_trip_uuid = tds.simplex_uuid_from_key(simplex_key); + /// let round_trip_uuid = dt.simplex_uuid_from_key(simplex_key); /// assert_eq!(Some(original_uuid), round_trip_uuid); /// # Ok(()) /// # } @@ -2065,16 +2059,15 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first vertex key and expected UUID - /// let Some((vertex_key, vertex)) = tds.vertices().next() else { + /// let Some((vertex_key, vertex)) = dt.vertices().next() else { /// return Ok(()); /// }; /// let expected_uuid = vertex.uuid(); /// /// // Use the helper function to get UUID from the vertex key - /// let found_uuid = tds.vertex_uuid_from_key(vertex_key); + /// let found_uuid = dt.vertex_uuid_from_key(vertex_key); /// assert_eq!(found_uuid, Some(expected_uuid)); /// # Ok(()) /// # } @@ -2107,19 +2100,18 @@ impl Tds { /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); /// /// // Get the first vertex's UUID - /// let Some((_, vertex)) = tds.vertices().next() else { + /// let Some((_, vertex)) = dt.vertices().next() else { /// return Ok(()); /// }; /// let original_uuid = vertex.uuid(); /// /// // Convert UUID to key, then key back to UUID - /// let Some(vertex_key) = tds.vertex_key_from_uuid(&original_uuid) else { + /// let Some(vertex_key) = dt.vertex_key_from_uuid(&original_uuid) else { /// return Ok(()); /// }; - /// let round_trip_uuid = tds.vertex_uuid_from_key(vertex_key); + /// let round_trip_uuid = dt.vertex_uuid_from_key(vertex_key); /// assert_eq!(Some(original_uuid), round_trip_uuid); /// # Ok(()) /// # } @@ -2193,11 +2185,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(vertex_key) = tds.vertex_keys().next() else { + /// let Some((vertex_key, _)) = dt.vertices().next() else { /// return Ok(()); /// }; - /// assert!(tds.vertex(vertex_key).is_some()); + /// assert!(dt.vertex(vertex_key).is_some()); /// # Ok(()) /// # } /// ``` @@ -2257,11 +2248,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(simplex_key) = tds.simplex_keys().next() else { + /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// assert!(tds.contains_simplex_key(simplex_key)); + /// assert!(dt.contains_simplex(simplex_key)); /// # Ok(()) /// # } /// ``` @@ -2305,11 +2295,10 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let Some(vertex_key) = tds.vertex_keys().next() else { + /// let Some((vertex_key, _)) = dt.vertices().next() else { /// return Ok(()); /// }; - /// assert!(tds.contains_vertex_key(vertex_key)); + /// assert!(dt.contains_vertex_key(vertex_key)); /// # Ok(()) /// # } /// ``` diff --git a/src/core/tds/validation.rs b/src/core/tds/validation.rs index ef2abfff..5ccac66c 100644 --- a/src/core/tds/validation.rs +++ b/src/core/tds/validation.rs @@ -71,8 +71,7 @@ impl Tds { /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// let facet_index = tds.build_facet_to_simplices_index()?; + /// let facet_index = dt.facet_incidence_index()?; /// assert!(!facet_index.is_empty()); /// # Ok(()) /// # } @@ -578,7 +577,7 @@ impl Tds { /// let dt: DelaunayTriangulation<_, (), (), 2> = /// DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// assert!(dt.tds().is_coherently_oriented()); + /// assert!(dt.is_coherently_oriented()); /// # Ok(()) /// # } /// ``` @@ -1001,8 +1000,8 @@ impl Tds { /// let dt: DelaunayTriangulation<_, (), (), 4> = /// DelaunayTriangulationBuilder::new(&vertices_4d).build()?; /// - /// // Level 2: TDS structural validation - /// assert!(dt.tds().is_valid().is_ok()); + /// // Level 2: structural validation + /// assert!(dt.is_valid_structure().is_ok()); /// # Ok(()) /// # } /// ``` @@ -1092,8 +1091,8 @@ impl Tds { /// let dt: DelaunayTriangulation<_, (), (), 4> = /// DelaunayTriangulationBuilder::new(&vertices_4d).build()?; /// - /// // Levels 1–2: elements + TDS structure - /// assert!(dt.tds().validate().is_ok()); + /// // Levels 1–2: elements + structure + /// assert!(dt.validate_structure().is_ok()); /// # Ok(()) /// # } /// ``` diff --git a/src/core/traits/facet_incidence_analysis.rs b/src/core/traits/facet_incidence_analysis.rs index e1c88b59..5d0a0988 100644 --- a/src/core/traits/facet_incidence_analysis.rs +++ b/src/core/traits/facet_incidence_analysis.rs @@ -11,10 +11,17 @@ use crate::core::{ /// incident to one or two D-simplices. That is deliberately weaker than manifold /// boundary semantics. A one-sided facet can be a Euclidean boundary facet, but /// in a periodic quotient triangulation it can also be a closed -/// self-identification. Use [`Triangulation::boundary_facets`] or +/// self-identification. Public owner-level callers can inspect raw incidence +/// through [`Triangulation::facet_incidence_index`] or +/// [`DelaunayTriangulation::facet_incidence_index`] and +/// [`FacetIncidenceView::is_one_sided`]. Use +/// [`Triangulation::boundary_facets`] or /// [`DelaunayTriangulation::boundary_facets`] for topology-aware boundary /// queries. /// +/// [`Triangulation::facet_incidence_index`]: crate::Triangulation::facet_incidence_index +/// [`DelaunayTriangulation::facet_incidence_index`]: crate::DelaunayTriangulation::facet_incidence_index +/// [`FacetIncidenceView::is_one_sided`]: crate::tds::FacetIncidenceView::is_one_sided /// [`Triangulation::boundary_facets`]: crate::Triangulation::boundary_facets /// [`DelaunayTriangulation::boundary_facets`]: crate::DelaunayTriangulation::boundary_facets pub trait FacetIncidenceAnalysis { diff --git a/src/core/triangulation.rs b/src/core/triangulation.rs index fffd6ad5..eb806ca1 100644 --- a/src/core/triangulation.rs +++ b/src/core/triangulation.rs @@ -169,7 +169,7 @@ where /// // Clear data /// let prev = dt.set_vertex_data(key, None)?; /// assert_eq!(prev, Some(99)); - /// let vertex = dt.tds().vertex(key).ok_or(ExampleError::MissingVertex)?; + /// let vertex = dt.vertex(key).ok_or(ExampleError::MissingVertex)?; /// assert_eq!(vertex.data(), None); /// # Ok(()) /// # } @@ -229,7 +229,7 @@ where /// // Clear data /// let prev = dt.set_simplex_data(key, None)?; /// assert_eq!(prev, Some(42)); - /// let simplex = dt.tds().simplex(key).ok_or(ExampleError::MissingSimplex)?; + /// let simplex = dt.simplex(key).ok_or(ExampleError::MissingSimplex)?; /// assert_eq!(simplex.data(), None); /// # Ok(()) /// # } diff --git a/src/core/util/canonical_points.rs b/src/core/util/canonical_points.rs index 4d03fa43..b9d7f4b8 100644 --- a/src/core/util/canonical_points.rs +++ b/src/core/util/canonical_points.rs @@ -89,12 +89,6 @@ pub(crate) enum CanonicalFacetPointError { /// * `tds` - The triangulation data structure for vertex lookups /// * `simplex` - The simplex whose vertices to collect /// -/// # Examples -/// -/// ```rust,ignore -/// let points = sorted_simplex_points(tds, simplex)?; -/// let sign = kernel.in_sphere(&points, &query_point)?; -/// ``` pub(crate) fn sorted_simplex_points( tds: &Tds, simplex: &Simplex, @@ -140,12 +134,6 @@ pub(crate) fn sorted_simplex_points( /// * `facet_keys` - Vertex keys forming the facet (will be sorted internally) /// * `extra` - The extra point to append at position D (opposite vertex or query) /// -/// # Examples -/// -/// ```rust,ignore -/// let points = sorted_facet_points_with_extra(tds, &facet_keys, opposite_point)?; -/// let orient = kernel.orientation(&points)?; -/// ``` pub(crate) fn sorted_facet_points_with_extra( tds: &Tds, facet_keys: &[VertexKey], diff --git a/src/core/util/facet_keys.rs b/src/core/util/facet_keys.rs index 2d4c7f4b..0aa5c188 100644 --- a/src/core/util/facet_keys.rs +++ b/src/core/util/facet_keys.rs @@ -52,10 +52,9 @@ use thiserror::Error; /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); /// /// // Get facet vertex keys from a simplex - no need to materialize Vertex objects -/// if let Some(simplex) = tds.simplices().map(|(_, simplex)| simplex).next() { +/// if let Some((_, simplex)) = dt.simplices().next() { /// let facet_vertex_keys: Vec<_> = simplex.vertices().iter().skip(1).copied().collect(); // Skip 1 vertex to get D vertices /// assert_eq!(facet_vertex_keys.len(), 3); // For 3D triangulation, facet has 3 vertices /// let facet_key = checked_facet_key_from_vertex_keys::<3>(&facet_vertex_keys)?; @@ -475,7 +474,7 @@ mod tests { vertex!([0.0, 0.0, 1.0]).unwrap(), ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; + let tds = dt.tds(); // Test 1: Basic functionality - successful key derivation println!(" Testing basic functionality..."); @@ -615,7 +614,7 @@ mod tests { vertex!([0.0, 0.0, 1.0]).unwrap(), ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; + let tds = dt.tds(); let simplex_key = tds.simplex_keys().next().unwrap(); assert!(verify_facet_index_consistency(tds, simplex_key, simplex_key, 0).unwrap()); diff --git a/src/core/util/facet_utils.rs b/src/core/util/facet_utils.rs index c2cf6061..99b7a997 100644 --- a/src/core/util/facet_utils.rs +++ b/src/core/util/facet_utils.rs @@ -352,8 +352,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -443,8 +443,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -492,8 +492,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -543,7 +543,7 @@ mod tests { ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; + let tds = dt.tds(); let simplex_key = tds.simplex_keys().next().unwrap(); // All facets of the same tetrahedron should be different from each other @@ -583,7 +583,7 @@ mod tests { ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; + let tds = dt.tds(); let simplex_key = tds.simplex_keys().next().unwrap(); let facet1 = FacetView::try_new(tds, simplex_key, 0).unwrap(); @@ -630,8 +630,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -662,8 +662,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -726,8 +726,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); @@ -787,8 +787,8 @@ mod tests { let dt1 = DelaunayTriangulation::builder(&vertices1).build().unwrap(); let dt2 = DelaunayTriangulation::builder(&vertices2).build().unwrap(); - let tds1 = &dt1.as_triangulation().tds; - let tds2 = &dt2.as_triangulation().tds; + let tds1 = dt1.tds(); + let tds2 = dt2.tds(); let simplex1_key = tds1.simplex_keys().next().unwrap(); let simplex2_key = tds2.simplex_keys().next().unwrap(); diff --git a/src/core/util/jaccard.rs b/src/core/util/jaccard.rs index 8d409c4e..0c1dd64f 100644 --- a/src/core/util/jaccard.rs +++ b/src/core/util/jaccard.rs @@ -3,15 +3,14 @@ #![forbid(unsafe_code)] use crate::core::facet::FacetError; -use crate::core::tds::Tds; use crate::core::traits::data_type::DataType; -use crate::core::traits::facet_incidence_analysis::FacetIncidenceAnalysis; use crate::core::triangulation::Triangulation; use crate::geometry::algorithms::convex_hull::{ConvexHull, ConvexHullConstructionError}; use crate::geometry::point::Point; use std::collections::HashSet; use std::fmt::Debug; use std::hash::{BuildHasher, Hash}; +use std::sync::Arc; use thiserror::Error; /// Errors that can occur during Jaccard similarity computation. @@ -192,7 +191,7 @@ where /// /// # Arguments /// -/// * `tds` - The triangulation data structure to extract vertex coordinates from +/// * `tri` - The triangulation to extract vertex coordinates from /// /// # Returns /// @@ -219,20 +218,20 @@ where /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); -/// -/// let coord_set = extract_vertex_coordinate_set(tds); +/// let coord_set = extract_vertex_coordinate_set(dt.as_triangulation()); /// assert_eq!(coord_set.len(), 4); /// # Ok(()) /// # } /// ``` #[must_use] -pub fn extract_vertex_coordinate_set(tds: &Tds) -> HashSet> +pub fn extract_vertex_coordinate_set( + tri: &Triangulation, +) -> HashSet> where U: DataType, V: DataType, { - tds.vertices().map(|(_, vertex)| *vertex.point()).collect() + tri.vertices().map(|(_, vertex)| *vertex.point()).collect() } /// Canonicalize an edge by ordering vertex UUIDs. @@ -250,7 +249,7 @@ const fn canonical_edge(u: u128, v: u128) -> (u128, u128) { /// /// # Arguments /// -/// * `tds` - The triangulation data structure to extract edges from +/// * `tri` - The triangulation to extract edges from /// /// # Returns /// @@ -285,16 +284,14 @@ const fn canonical_edge(u: u128, v: u128) -> (u128, u128) { /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); -/// -/// let edge_set = extract_edge_set(tds)?; +/// let edge_set = extract_edge_set(dt.as_triangulation())?; /// // A tetrahedron has 6 edges /// assert_eq!(edge_set.len(), 6); /// # Ok(()) /// # } /// ``` -pub fn extract_edge_set( - tds: &Tds, +pub fn extract_edge_set( + tri: &Triangulation, ) -> Result, FacetError> where U: DataType, @@ -302,17 +299,17 @@ where { let mut edges = HashSet::new(); - for (_, simplex) in tds.simplices() { + for (_, simplex) in tri.simplices() { let vertex_keys = simplex.vertices(); // Generate all pairs of vertices (edges) for i in 0..vertex_keys.len() { for j in (i + 1)..vertex_keys.len() { - let v_i = tds.vertex(vertex_keys[i]).ok_or( + let v_i = tri.vertex(vertex_keys[i]).ok_or( FacetError::VertexKeyNotFoundInTriangulation { key: vertex_keys[i], }, )?; - let v_j = tds.vertex(vertex_keys[j]).ok_or( + let v_j = tri.vertex(vertex_keys[j]).ok_or( FacetError::VertexKeyNotFoundInTriangulation { key: vertex_keys[j], }, @@ -335,7 +332,7 @@ where /// /// # Arguments /// -/// * `tds` - The triangulation data structure to extract facet identifiers from +/// * `tri` - The triangulation to extract facet identifiers from /// /// # Returns /// @@ -368,16 +365,14 @@ where /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); -/// -/// let facet_set = extract_facet_identifier_set(tds)?; +/// let facet_set = extract_facet_identifier_set(dt.as_triangulation())?; /// // A tetrahedron has 4 facets /// assert_eq!(facet_set.len(), 4); /// # Ok(()) /// # } /// ``` -pub fn extract_facet_identifier_set( - tds: &Tds, +pub fn extract_facet_identifier_set( + tri: &Triangulation, ) -> Result, FacetError> where U: DataType, @@ -385,19 +380,16 @@ where { let mut facet_ids = HashSet::new(); - // one_sided_facets() returns TDS-level incidence candidates. - // Wrap the underlying error for better diagnostics - let boundary_facets = - tds.one_sided_facets() - .map_err(|e| FacetError::BoundaryFacetRetrievalFailed { - source: std::sync::Arc::new(e), + let facet_index = + tri.facet_incidence_index() + .map_err(|source| FacetError::BoundaryFacetRetrievalFailed { + source: Arc::new(source), })?; - - for facet_view in boundary_facets { - let facet_view = facet_view?; - // Use the existing FacetView::key() method - let facet_id = facet_view.key(); - facet_ids.insert(facet_id); + for incidence in facet_index + .iter() + .filter(|incidence| incidence.is_one_sided()) + { + facet_ids.insert(incidence.facet_key()); } Ok(facet_ids) @@ -648,7 +640,8 @@ mod tests { use crate::vertex; use std::assert_matches; - use crate::core::tds::{Tds, VertexKey}; + use crate::core::tds::VertexKey; + use crate::geometry::kernel::FastKernel; use crate::triangulation::DelaunayTriangulation; use approx::assert_relative_eq; use slotmap::KeyData; @@ -711,10 +704,10 @@ mod tests { vertex!([0.0, 0.0, 1.0]).unwrap(), ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; + let tri = dt.as_triangulation(); // Sub-test: Vertex coordinate extraction - let coord_set = extract_vertex_coordinate_set(tds); + let coord_set = extract_vertex_coordinate_set(tri); assert_eq!(coord_set.len(), 4, "Should have 4 unique coordinates"); assert!( coord_set.contains(&Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates")) @@ -730,11 +723,11 @@ mod tests { ); // Sub-test: Edge extraction - tetrahedron has 6 edges (binomial(4,2)) - let edge_set = extract_edge_set(tds).unwrap(); + let edge_set = extract_edge_set(tri).unwrap(); assert_eq!(edge_set.len(), 6, "Tetrahedron should have 6 edges"); // Sub-test: Facet identifier extraction - tetrahedron has 4 facets - let facet_set = extract_facet_identifier_set(tds).unwrap(); + let facet_set = extract_facet_identifier_set(tri).unwrap(); assert_eq!(facet_set.len(), 4, "Tetrahedron should have 4 facets"); // Sub-test: Hull facet extraction @@ -755,7 +748,7 @@ mod tests { ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let simplex_key = dt.as_triangulation().tds.simplex_keys().next().unwrap(); + let simplex_key = dt.simplices().next().unwrap().0; let invalid_vkey = VertexKey::from(KeyData::from_ffi(u64::MAX)); dt.tri .tds @@ -763,7 +756,7 @@ mod tests { .unwrap() .push_vertex_key(invalid_vkey); - let err = extract_edge_set(&dt.as_triangulation().tds).unwrap_err(); + let err = extract_edge_set(dt.as_triangulation()).unwrap_err(); assert_matches!( err, FacetError::VertexKeyNotFoundInTriangulation { key } if key == invalid_vkey @@ -780,7 +773,7 @@ mod tests { ]; let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let simplex_key = dt.as_triangulation().tds.simplex_keys().next().unwrap(); + let simplex_key = dt.simplices().next().unwrap().0; let invalid_vkey = VertexKey::from(KeyData::from_ffi(u64::MAX)); dt.tri .tds @@ -788,7 +781,7 @@ mod tests { .unwrap() .push_vertex_key(invalid_vkey); - let err = extract_facet_identifier_set(&dt.as_triangulation().tds).unwrap_err(); + let err = extract_facet_identifier_set(dt.as_triangulation()).unwrap_err(); assert_matches!(err, FacetError::BoundaryFacetRetrievalFailed { .. }); } @@ -822,15 +815,16 @@ mod tests { let hull = ConvexHull::try_from_triangulation(tri).unwrap(); let hull_facet_set = extract_hull_facet_set(&hull, tri).unwrap(); - let boundary_set = extract_facet_identifier_set(&tri.tds).unwrap(); + let boundary_set = extract_facet_identifier_set(tri).unwrap(); assert_eq!(hull_facet_set, boundary_set); } #[test] fn test_extract_edge_set_empty_tds_is_empty() { - let tds: Tds<(), (), 3> = Tds::empty(); - let edges = extract_edge_set(&tds).unwrap(); + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + let edges = extract_edge_set(&tri).unwrap(); assert!(edges.is_empty()); } } diff --git a/src/core/validation.rs b/src/core/validation.rs index 7361ace1..af868df6 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -118,12 +118,21 @@ use crate::core::tds::{ use crate::core::traits::data_type::DataType; use crate::core::triangulation::Triangulation; use crate::geometry::kernel::Kernel; -use crate::topology::characteristics::euler::TopologyClassification; -use crate::topology::characteristics::validation::validate_triangulation_euler_from_validated_facet_map; +use crate::topology::characteristics::euler::{ + FVector, TopologyClassification, classify_triangulation, count_boundary_simplices, + count_simplices, +}; +use crate::topology::characteristics::validation::{ + TopologyCheckResult, validate_triangulation_euler, + validate_triangulation_euler_from_validated_facet_map, +}; use crate::topology::manifold::{ ManifoldError, ValidatedFacetDegreeMap, validate_closed_boundary_from_validated_facet_map, - validate_local_pseudomanifold_for_simplices, validate_ridge_links, - validate_ridge_links_for_simplices, validate_vertex_links_from_validated_facet_map, + validate_local_pseudomanifold_for_simplices, + validate_ridge_links as validate_ridge_links_in_tds, + validate_ridge_links_for_simplices as validate_ridge_links_for_simplices_in_tds, + validate_vertex_links as validate_vertex_links_in_index, + validate_vertex_links_from_validated_facet_map, }; use crate::topology::traits::topological_space::{GlobalTopology, TopologyError, TopologyKind}; use std::time::{Duration, Instant}; @@ -776,6 +785,281 @@ pub(crate) enum InsertionValidationWork { } impl Triangulation { + /// Fast-fail Level 2 structural validation. + /// + /// This checks the triangulation data structure's key mappings, incidence + /// bookkeeping, duplicate-simplex invariant, facet sharing, neighbor + /// consistency, and coherent orientation without exposing the underlying + /// storage owner. + /// + /// Use [`Triangulation::validate_structure`](Self::validate_structure) for + /// cumulative Levels 1-2 element plus structure validation, or + /// [`Triangulation::validate`](Self::validate) for cumulative Levels 1-3. + /// + /// # Errors + /// + /// Returns the first [`TdsError`] encountered by the structural validator. + pub fn is_valid_structure(&self) -> Result<(), TdsError> { + self.tds.is_valid() + } + + /// Cumulative Levels 1-2 validation for the triangulation structure. + /// + /// This validates all stored vertices and simplices first, then runs + /// [`Triangulation::is_valid_structure`](Self::is_valid_structure). + /// + /// # Errors + /// + /// Returns the first [`TdsError`] encountered by element or structural + /// validation. + pub fn validate_structure(&self) -> Result<(), TdsError> { + self.tds.validate() + } + + /// Returns the first actionable Level 2 structural diagnostic, if any. + #[must_use] + pub fn structure_diagnostic(&self) -> Option { + self.tds.structure_diagnostic() + } + + /// Runs Level 2 structure checks and returns all checkable failures. + /// + /// This is the aggregate-report counterpart to + /// [`Triangulation::is_valid_structure`](Self::is_valid_structure). + /// + /// # Errors + /// + /// Returns a [`TriangulationValidationReport`] containing all invariant + /// violations if any structural validation step fails. + pub fn structure_report(&self) -> Result<(), TriangulationValidationReport> { + self.tds.structure_report() + } + + /// Counts simplices by dimension for this triangulation. + /// + /// # Errors + /// + /// Returns [`TopologyError`] if the facet-incidence map required for + /// intermediate-dimensional counts cannot be built. + pub fn simplex_counts(&self) -> Result { + count_simplices(&self.tds) + } + + /// Counts simplices on the topology-approved boundary only. + /// + /// # Errors + /// + /// Returns [`TopologyError`] if boundary facets cannot be classified or + /// enumerated for this triangulation. + pub fn boundary_simplex_counts(&self) -> Result { + count_boundary_simplices(&self.tds, self.global_topology) + } + + /// Classifies this triangulation for Euler-characteristic checks. + /// + /// # Errors + /// + /// Returns [`TopologyError`] if facet incidence cannot be classified for + /// the provided global topology. + pub fn topology_classification_for( + &self, + global_topology: GlobalTopology, + ) -> Result { + classify_triangulation(&self.tds, global_topology) + } + + /// Validates this triangulation's Euler characteristic against its topology metadata. + /// + /// # Errors + /// + /// Returns [`TopologyError`] if simplex counts or boundary classification + /// cannot be computed. + pub fn euler_check(&self) -> Result { + self.euler_check_for_topology(self.global_topology) + } + + /// Validates this triangulation's Euler characteristic against explicit topology metadata. + /// + /// # Errors + /// + /// Returns [`TopologyError`] if simplex counts or boundary classification + /// cannot be computed. + pub fn euler_check_for_topology( + &self, + global_topology: GlobalTopology, + ) -> Result { + validate_triangulation_euler(&self.tds, global_topology) + } + + /// Validates all ridge links for the Level 3 PL-manifold codimension-2 condition. + /// + /// Ridge-link validation checks that every `(D-2)`-simplex has a 1-dimensional + /// link that is a connected path for boundary ridges or a connected cycle for + /// interior ridges. It is cheaper and more local than vertex-link validation + /// and catches many branching or wedge singularities, but in dimensions + /// `D >= 3` it is not by itself a complete PL-manifold certificate. + /// + /// This explicit check runs regardless of [`TopologyGuarantee`]. Use + /// [`Triangulation::validate_vertex_links`](Self::validate_vertex_links) for + /// the canonical vertex-link PL-manifold certification, or + /// [`Triangulation::is_valid_topology`](Self::is_valid_topology) for the + /// configured full Level 3 topology policy. + /// + /// # Errors + /// + /// Returns [`ManifoldError::Tds`] if the triangulation storage is + /// structurally inconsistent while resolving ridge stars. Returns + /// [`ManifoldError::RidgeLinkNotManifold`] when a ridge link is not a + /// connected path or cycle. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayTriangulationConstructionError, vertex, + /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::validation::ManifoldError; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Manifold(#[from] ManifoldError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// dt.as_triangulation().validate_ridge_links()?; + /// # Ok(()) + /// # } + /// ``` + pub fn validate_ridge_links(&self) -> Result<(), ManifoldError> { + validate_ridge_links_in_tds(&self.tds) + } + + /// Validates ridge links incident to a selected set of simplices. + /// + /// This is the localized form of + /// [`Triangulation::validate_ridge_links`](Self::validate_ridge_links). It + /// checks only ridges touching the supplied simplex keys, which is useful + /// after local insertion or flip work when a caller wants targeted Level 3 + /// diagnostics without rebuilding the global ridge-star map. + /// + /// Missing simplex keys are ignored by the underlying local ridge-star + /// builder because they may have been removed by a preceding topology edit. + /// + /// # Errors + /// + /// Returns [`ManifoldError::Tds`] if the triangulation storage is + /// structurally inconsistent while resolving ridge stars. Returns + /// [`ManifoldError::RidgeLinkNotManifold`] when a checked ridge link is not + /// a connected path or cycle. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayTriangulationConstructionError, vertex, + /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::validation::ManifoldError; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Manifold(#[from] ManifoldError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let touched = dt.simplices().map(|(key, _)| key); + /// + /// dt.as_triangulation().validate_ridge_links_for_simplices(touched)?; + /// # Ok(()) + /// # } + /// ``` + pub fn validate_ridge_links_for_simplices( + &self, + simplices: impl IntoIterator, + ) -> Result<(), ManifoldError> { + validate_ridge_links_for_simplices_in_tds(&self.tds, simplices) + } + + /// Validates all vertex links for the canonical Level 3 PL-manifold condition. + /// + /// A pure `D`-dimensional simplicial complex is a PL-manifold with boundary + /// only when every vertex link is a `(D-1)`-sphere for interior vertices or + /// a `(D-1)`-ball for boundary vertices. This check is the public, + /// owner-level entry point for that scientific validity criterion. + /// + /// This explicit check runs regardless of [`TopologyGuarantee`]. It uses the + /// triangulation's [`GlobalTopology`] metadata to distinguish true boundary + /// facets from periodic identifications. + /// + /// # Errors + /// + /// Returns [`ManifoldError::Tds`] if facet incidence cannot be built from + /// the current storage. Returns [`ManifoldError::VertexLinkNotManifold`] + /// when a vertex link fails the expected sphere/ball manifold condition. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayTriangulationConstructionError, vertex, + /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::validation::ManifoldError; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Manifold(#[from] ManifoldError), + /// # #[error(transparent)] + /// # Coordinate(#[from] CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices = vec![ + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// dt.as_triangulation().validate_vertex_links()?; + /// # Ok(()) + /// # } + /// ``` + pub fn validate_vertex_links(&self) -> Result<(), ManifoldError> { + let facet_to_simplices = self.tds.build_facet_to_simplices_index()?; + validate_vertex_links_in_index(&facet_to_simplices, self.global_topology) + } + /// Returns the topology guarantee used for Level 3 topology validation. #[inline] #[must_use] @@ -881,7 +1165,7 @@ impl Triangulation { // 2c. Ridge-link validation for PLManifold/PLManifoldStrict (fast, catches many PL issues). if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links(&self.tds)?; + validate_ridge_links_in_tds(&self.tds)?; } // 2d. PL-manifold vertex-link condition during insertion (strict mode). if self @@ -1192,7 +1476,12 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::*; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayTriangulationConstructionError, vertex, + /// }; + /// use delaunay::prelude::geometry::CoordinateConversionError; + /// use delaunay::prelude::validation::ManifoldError; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -1367,7 +1656,10 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::*; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + /// }; + /// use delaunay::DelaunayTriangulation; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -1600,7 +1892,7 @@ where )?; if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links(&self.tds)?; + validate_ridge_links_in_tds(&self.tds)?; } if self @@ -1749,7 +2041,7 @@ where validate_local_pseudomanifold_for_simplices(&self.tds, self.global_topology, simplices)?; if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links_for_simplices(&self.tds, simplices.iter().copied())?; + validate_ridge_links_for_simplices_in_tds(&self.tds, simplices.iter().copied())?; } self.validate_geometric_simplex_orientation_for_simplices(simplices)?; diff --git a/src/core/vertex.rs b/src/core/vertex.rs index f49cf0b3..dc1acc74 100644 --- a/src/core/vertex.rs +++ b/src/core/vertex.rs @@ -1465,15 +1465,6 @@ mod tests { /// - Basic vertex creation and property validation /// - Serialization roundtrip (Some and None data) /// - UUID validation - /// - /// # Usage - /// - /// ```ignore - /// test_vertex_dimensions! { - /// vertex_2d => 2 => [1.0, 2.0], - /// vertex_3d => 3 => [1.0, 2.0, 3.0], - /// } - /// ``` macro_rules! test_vertex_dimensions { ($( $test_name:ident => $dim:expr => [$($coord:expr),+ $(,)?] diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index b104af40..b9de1ffc 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -109,7 +109,7 @@ //! //! assert_eq!(dt.number_of_vertices(), 7); //! // Every vertex has a valid incident simplex (no boundary). -//! assert!(dt.tds().is_valid().is_ok()); +//! assert!(dt.is_valid_structure().is_ok()); //! # Ok(()) //! # } //! ``` @@ -960,7 +960,7 @@ impl<'v, U, V, const D: usize> DelaunayTriangulationBuilder<'v, U, D, V> { /// .build_with_kernel(&kernel)?; /// /// assert_eq!(dt.number_of_vertices(), 7); - /// assert!(dt.tds().is_valid().is_ok()); + /// assert!(dt.is_valid_structure().is_ok()); /// # Ok(()) /// # } /// ``` @@ -2430,7 +2430,11 @@ where let canonical_present = canonical_uuids .iter() - .filter(|uuid| candidate_dt.tds().vertex_key_from_uuid(uuid).is_some()) + .filter(|uuid| { + candidate_dt + .vertices() + .any(|(_, vertex)| vertex.uuid() == **uuid) + }) .count(); if canonical_present > best_fallback_stats.0 || (canonical_present == best_fallback_stats.0 @@ -2442,7 +2446,7 @@ where if canonical_present == n && candidate_dt.number_of_simplices() > 0 - && candidate_dt.tds().is_valid().is_ok() + && candidate_dt.is_valid_structure().is_ok() { built = Some(candidate_dt); break; @@ -2469,12 +2473,15 @@ where Err(err) => return Err(err), }; - let tds_ref = full_dt.tds(); + let uuid_to_key: FastHashMap = full_dt + .vertices() + .map(|(key, vertex)| (vertex.uuid(), key)) + .collect(); // Map canonical UUIDs → VertexKeys in the full DT. let Some(central_keys) = canonical_uuids .iter() - .map(|uuid| tds_ref.vertex_key_from_uuid(uuid)) + .map(|uuid| uuid_to_key.get(uuid).copied()) .collect::>>() else { return Err( @@ -2489,16 +2496,13 @@ where // Map every full-DT vertex key to its canonical key and lattice offset. let mut vertex_key_to_lifted: FastHashMap = FastHashMap::default(); - for vk in tds_ref.vertex_keys() { - let Some(vertex) = tds_ref.vertex(vk) else { - continue; - }; + for (vk, vertex) in full_dt.vertices() { let Some((canonical_uuid, offset)) = image_uuid_to_canonical_with_offset.get(&vertex.uuid()) else { continue; }; - let Some(canonical_key) = tds_ref.vertex_key_from_uuid(canonical_uuid) else { + let Some(canonical_key) = uuid_to_key.get(canonical_uuid).copied() else { continue; }; vertex_key_to_lifted.insert(vk, (canonical_key, *offset)); @@ -2506,7 +2510,7 @@ where let normalize_simplex_lifted = |simplex_key: SimplexKey| -> Option> { - let simplex = tds_ref.simplex(simplex_key)?; + let simplex = full_dt.simplex(simplex_key)?; let mut lifted: Vec<(VertexKey, [i8; D])> = simplex .vertices() .iter() @@ -2532,11 +2536,11 @@ where Some(lifted) }; let simplex_circumcenter_in_fundamental_domain = |simplex_key: SimplexKey| -> Option { - let simplex = tds_ref.simplex(simplex_key)?; + let simplex = full_dt.simplex(simplex_key)?; let mut points: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = SmallBuffer::with_capacity(D + 1); for vk in simplex.vertices() { - let vertex = tds_ref.vertex(*vk)?; + let vertex = full_dt.vertex(*vk)?; points.push(*vertex.point()); } let center = circumcenter(&points).ok()?; @@ -2557,7 +2561,7 @@ where // `normalize_simplex_lifted` (it is not canonical-key-sorted). let mut candidates_by_symbolic: FastHashMap, PeriodicCandidate> = FastHashMap::default(); - for ck in tds_ref.simplex_keys() { + for (ck, _) in full_dt.simplices() { let Some(lifted_vertices) = normalize_simplex_lifted(ck) else { continue; }; @@ -2591,7 +2595,7 @@ where if candidates.is_empty() { return Err( TriangulationConstructionError::PeriodicQuotientNoCandidates { - full_simplex_count: tds_ref.number_of_simplices(), + full_simplex_count: full_dt.number_of_simplices(), canonical_vertex_count: central_key_set.len(), } .into(), @@ -2887,8 +2891,8 @@ where TriangulationConstructionError::PeriodicQuotientSelectionBoundaryFacets { boundary_facet_count: best_boundary_count, search_attempts, - full_vertex_count: tds_ref.number_of_vertices(), - full_simplex_count: tds_ref.number_of_simplices(), + full_vertex_count: full_dt.number_of_vertices(), + full_simplex_count: full_dt.number_of_simplices(), canonical_vertex_count: central_key_set.len(), candidate_count: candidates.len(), selected_simplex_count: best_selected_count, @@ -2930,6 +2934,7 @@ where } // Clone TDS and rebuild simplex complex from quotient representatives. + let tds_ref = full_dt.tds(); let mut tds_mut = tds_ref.clone(); // Remove all simplices first. @@ -3610,7 +3615,7 @@ mod tests { .build_with_kernel(&kernel) .unwrap(); assert_eq!(dt.number_of_vertices(), n); - assert!(dt.tds().is_valid().is_ok()); + assert!(dt.is_valid_structure().is_ok()); assert_matches!( dt.global_topology(), GlobalTopology::Toroidal { diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index 04d6a3b0..e07c932d 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -43,6 +43,7 @@ #![forbid(unsafe_code)] use crate::builder::{DelaunayTriangulationBuilder, ExplicitConstructionError}; +use crate::core::adjacency::TopologyIndexBuildError; use crate::core::algorithms::flips::{ DelaunayRepairError, DelaunayRepairStats, FlipError, LocalRepairPhaseTiming, repair_delaunay_local_single_pass, repair_delaunay_local_single_pass_timed, @@ -63,7 +64,9 @@ use crate::core::construction::{ FinalDelaunayValidationContext, FinalTopologyValidationContext, PeriodicQuotientFacetKeyDerivationFailure, TriangulationConstructionError, }; +use crate::core::edge::EdgeKeyError; use crate::core::embedding::TriangulationEmbeddingValidationError; +use crate::core::facet::FacetError; use crate::core::insertion::record_duplicate_detection_metrics; use crate::core::operations::{ DelaunayInsertionState, InsertionOutcome, InsertionResult, InsertionStatistics, @@ -82,6 +85,7 @@ use crate::core::validation::{ TopologyGuarantee, TriangulationValidationError, ValidationConfigurationError, ValidationPolicy, }; use crate::core::vertex::Vertex; +use crate::delaunay_property_validation::DelaunayValidationError; use crate::deletion::DeleteVertexError; use crate::diagnostics::{BatchLocalRepairTrigger, ConstructionTelemetry, LocalRepairSample}; use crate::geometry::coordinate_range::CoordinateRange; @@ -98,8 +102,9 @@ use crate::io::visualization::{VisualizationDataValidationError, VisualizationEx use crate::locality::{ accumulate_live_simplex_seeds, clear_simplex_seed_set, retain_live_simplex_seeds, }; -use crate::query::{SimplexBarycenterError, SimplexDataFillError}; +use crate::query::{QueryError, SimplexBarycenterError, SimplexDataFillError}; use crate::repair::DelaunayRepairPolicy; +use crate::topology::manifold::ManifoldError; use crate::topology::traits::{ GlobalTopology, GlobalTopologyModelError, TopologyKind, ToroidalConstructionMode, ToroidalDomainError, @@ -131,85 +136,6 @@ fn batch_repair_trace_enabled() -> bool { env::var_os("DELAUNAY_BATCH_REPAIR_TRACE").is_some() } -#[cfg(test)] -pub(crate) mod test_hooks { - use crate::core::algorithms::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairError, RepairQueueOrder, - }; - use std::cell::Cell; - - thread_local! { - static FORCE_HEURISTIC_REBUILD: Cell = const { Cell::new(false) }; - static FORCE_REPAIR_NONCONVERGENT: Cell = const { Cell::new(false) }; - static BATCH_LOCAL_REPAIR_CALLS: Cell = const { Cell::new(0) }; - } - - pub fn force_heuristic_rebuild_enabled() -> bool { - FORCE_HEURISTIC_REBUILD.with(Cell::get) - } - - #[must_use] - pub fn set_force_heuristic_rebuild(enabled: bool) -> bool { - FORCE_HEURISTIC_REBUILD.with(|flag| { - let prior = flag.get(); - flag.set(enabled); - prior - }) - } - - pub fn restore_force_heuristic_rebuild(prior: bool) { - FORCE_HEURISTIC_REBUILD.with(|flag| flag.set(prior)); - } - - pub fn force_repair_nonconvergent_enabled() -> bool { - FORCE_REPAIR_NONCONVERGENT.with(Cell::get) - } - - #[must_use] - pub fn set_force_repair_nonconvergent(enabled: bool) -> bool { - FORCE_REPAIR_NONCONVERGENT.with(|flag| { - let prior = flag.get(); - flag.set(enabled); - prior - }) - } - - pub fn restore_force_repair_nonconvergent(prior: bool) { - FORCE_REPAIR_NONCONVERGENT.with(|flag| flag.set(prior)); - } - - pub fn reset_batch_local_repair_calls() { - BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(0)); - } - - pub fn batch_local_repair_calls() -> usize { - BATCH_LOCAL_REPAIR_CALLS.with(Cell::get) - } - - pub fn record_batch_local_repair_call() { - BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(calls.get().saturating_add(1))); - } - - #[must_use] - pub fn synthetic_nonconvergent_error() -> DelaunayRepairError { - DelaunayRepairError::NonConvergent { - max_flips: 0, - diagnostics: Box::new(DelaunayRepairDiagnostics { - facets_checked: 0, - flips_performed: 0, - max_queue_len: 0, - ambiguous_predicates: 0, - ambiguous_predicate_samples: Vec::new(), - predicate_failures: 0, - cycle_detections: 0, - cycle_signature_samples: Vec::new(), - attempt: 0, - queue_order: RepairQueueOrder::Fifo, - }), - } - } -} - /// Common errors for user-facing Delaunay triangulation workflows. /// /// This convenience error covers the fallible path most examples use: @@ -217,21 +143,26 @@ pub(crate) mod test_hooks { /// [`DelaunayTriangulation`], computing simplex barycenters for local editing, /// editing it through the Delaunay insertion/deletion API or explicit /// flip/Pachner APIs, updating auxiliary vertex/simplex data through checked -/// keys, validating its Delaunay invariants, exporting mesh data, and -/// validating the export schema. More specialized workflows such as convex hull -/// extraction, repair, and delaunayize continue to expose their narrower error -/// types directly. +/// keys, running owner-bound query views, point-location and conflict-region +/// queries, topology-index and edge-key views, Level 3 PL-manifold checks, +/// cumulative invariant-validation roll-ups, validating its Delaunay +/// invariants, exporting mesh data, and validating the export schema. More +/// specialized workflows such as convex hull extraction, repair, and +/// delaunayize continue to expose their narrower error types directly. /// Each variant keeps the concrete typed source error behind a box so the /// umbrella result stays small without erasing matchable failure details. /// /// # Examples /// /// Use [`DelaunayResult`] for examples, binaries, and quick workflows whose -/// fallible operations stay inside coordinate conversion, construction, -/// random-triangulation builder setup, checked auxiliary-data mutation, -/// simplex barycenter queries, post-construction simplex-data filling, -/// insertion/deletion, explicit flip editing, validation, mesh export, and -/// export-schema validation: +/// fallible operations stay inside coordinate conversion, Delaunay or generic +/// triangulation construction, random-triangulation builder setup, checked +/// auxiliary-data mutation, owner-bound query views, point-location and +/// conflict-region queries, topology-index and edge-key views, low-level +/// TDS/facet/simplex helpers, simplex barycenter queries, post-construction +/// simplex-data filling, insertion/deletion, explicit flip editing, Level 3 +/// PL-manifold checks, cumulative invariant-validation roll-ups, validation, +/// mesh export, and export-schema validation: /// /// ```rust /// use delaunay::prelude::construction::{ @@ -263,6 +194,14 @@ pub enum DelaunayError { source: Box, }, + /// Generic triangulation construction failed. + #[error("{source}")] + TriangulationConstruction { + /// Underlying generic triangulation construction failure. + #[source] + source: Box, + }, + /// Coordinate conversion or validation failed before construction. #[error("{source}")] CoordinateConversion { @@ -303,6 +242,94 @@ pub enum DelaunayError { source: Box, }, + /// TDS construction failed. + #[error("{source}")] + TdsConstruction { + /// Underlying TDS construction failure. + #[source] + source: Box, + }, + + /// TDS validation or lookup failed. + #[error("{source}")] + Tds { + /// Underlying TDS failure. + #[source] + source: Box, + }, + + /// Facet query or view construction failed. + #[error("{source}")] + Facet { + /// Underlying facet failure. + #[source] + source: Box, + }, + + /// Simplex validation failed. + #[error("{source}")] + SimplexValidation { + /// Underlying simplex validation failure. + #[source] + source: Box, + }, + + /// Read-only triangulation query failed. + #[error("{source}")] + Query { + /// Underlying query failure. + #[source] + source: Box, + }, + + /// Point-location query failed. + #[error("{source}")] + Locate { + /// Underlying point-location failure. + #[source] + source: Box, + }, + + /// Conflict-region query failed. + #[error("{source}")] + Conflict { + /// Underlying conflict-region failure. + #[source] + source: Box, + }, + + /// Topology index or borrowed topology view construction failed. + #[error("{source}")] + TopologyIndex { + /// Underlying topology-index construction failure. + #[source] + source: Box, + }, + + /// Edge-key parsing or edge-view construction failed. + #[error("{source}")] + EdgeKey { + /// Underlying edge-key failure. + #[source] + source: Box, + }, + + /// PL-manifold topology validation failed. + #[error("{source}")] + Manifold { + /// Underlying PL-manifold validation failure. + #[source] + source: Box, + }, + + /// Cumulative invariant validation failed. + #[error("{source}")] + Invariant { + /// Underlying invariant roll-up failure. + #[source] + source: Box, + }, + /// Post-construction simplex payload filling failed. #[error("{source}")] SimplexDataFill { @@ -335,6 +362,14 @@ pub enum DelaunayError { source: Box, }, + /// Local Delaunay-property validation failed. + #[error("{source}")] + DelaunayPropertyValidation { + /// Underlying local Delaunay-property validation failure. + #[source] + source: Box, + }, + /// Visualization or mesh export failed. #[error("{source}")] VisualizationExport { @@ -368,6 +403,14 @@ impl From for DelaunayError { } } +impl From for DelaunayError { + fn from(source: TriangulationConstructionError) -> Self { + Self::TriangulationConstruction { + source: Box::new(source), + } + } +} + impl From for DelaunayError { fn from(source: RandomTriangulationBuilderError) -> Self { DelaunayTriangulationConstructionError::from(source).into() @@ -414,6 +457,94 @@ impl From for DelaunayError { } } +impl From for DelaunayError { + fn from(source: TdsConstructionError) -> Self { + Self::TdsConstruction { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: TdsError) -> Self { + Self::Tds { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: FacetError) -> Self { + Self::Facet { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: SimplexValidationError) -> Self { + Self::SimplexValidation { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: QueryError) -> Self { + Self::Query { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: LocateError) -> Self { + Self::Locate { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: ConflictError) -> Self { + Self::Conflict { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: TopologyIndexBuildError) -> Self { + Self::TopologyIndex { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: EdgeKeyError) -> Self { + Self::EdgeKey { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: ManifoldError) -> Self { + Self::Manifold { + source: Box::new(source), + } + } +} + +impl From for DelaunayError { + fn from(source: InvariantError) -> Self { + Self::Invariant { + source: Box::new(source), + } + } +} + impl From for DelaunayError { fn from(source: SimplexDataFillError) -> Self { Self::SimplexDataFill { @@ -446,6 +577,14 @@ impl From for DelaunayError { } } +impl From for DelaunayError { + fn from(source: DelaunayValidationError) -> Self { + Self::DelaunayPropertyValidation { + source: Box::new(source), + } + } +} + impl From for DelaunayError { fn from(source: VisualizationExportError) -> Self { Self::VisualizationExport { @@ -474,9 +613,13 @@ impl From for DelaunayError { /// /// This is equivalent to `Result` with [`DelaunayError`] as /// the error type, and is intended for caller-facing examples and applications -/// that use the standard construction, checked auxiliary-data mutation, +/// that use standard construction, generic triangulation construction, +/// random-triangulation builder setup, checked auxiliary-data mutation, +/// owner-bound query views, point-location and conflict-region queries, +/// topology-index and edge-key views, low-level TDS/facet/simplex helpers, /// simplex barycenter queries, post-construction simplex-data filling, -/// insertion/deletion, explicit flip editing, validation, mesh export, and +/// insertion/deletion, explicit flip editing, Level 3 PL-manifold checks, +/// cumulative invariant-validation roll-ups, validation, mesh export, and /// export-schema validation APIs. pub type DelaunayResult = Result; @@ -3193,7 +3336,7 @@ where log_construction_retry_result(0, None, 0_u64, "succeeded", None, None); return Ok(candidate); } - match candidate.is_delaunay_via_flips() { + match candidate.verify_via_flip_predicates() { Ok(()) => { log_construction_retry_result(0, None, 0_u64, "succeeded", None, None); return Ok(candidate); @@ -3293,7 +3436,7 @@ where ); return Ok(candidate); } - match candidate.is_delaunay_via_flips() { + match candidate.verify_via_flip_predicates() { Ok(()) => { log_construction_retry_result( attempt, @@ -3440,7 +3583,7 @@ where return Ok((candidate, aggregate_stats)); } let delaunay_started = Instant::now(); - let delaunay_result = candidate.is_delaunay_via_flips(); + let delaunay_result = candidate.verify_via_flip_predicates(); stats .telemetry .record_construction_final_delaunay_validation_timing( @@ -3577,7 +3720,7 @@ where return Ok((candidate, aggregate_stats)); } let delaunay_started = Instant::now(); - let delaunay_result = candidate.is_delaunay_via_flips(); + let delaunay_result = candidate.verify_via_flip_predicates(); stats .telemetry .record_construction_final_delaunay_validation_timing( @@ -4069,7 +4212,7 @@ where } #[cfg(test)] - test_hooks::record_batch_local_repair_call(); + tests::record_batch_local_repair_call(); let seed_simplices_len = pending_seed_simplices.len(); let max_flips = local_repair_flip_budget::(seed_simplices_len); @@ -4104,8 +4247,8 @@ where ) }; #[cfg(test)] - let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { - Err(test_hooks::synthetic_nonconvergent_error()) + let repair_result = if tests::force_repair_nonconvergent_enabled() { + Err(tests::synthetic_nonconvergent_error()) } else { repair_result }; @@ -5668,6 +5811,7 @@ mod tests { use crate::vertex; use slotmap::KeyData; use std::assert_matches; + use std::cell::Cell; use std::num::NonZeroUsize; use std::sync::Once; use std::time::Instant; @@ -5675,6 +5819,64 @@ mod tests { type TestDelaunay = DelaunayTriangulation, (), (), D>; + // Last-resort fault injection for rollback branches that are hard to + // trigger deterministically; thread-local state avoids cross-test leakage. + // Remove this once a cleaner harness can reach the branch directly. + thread_local! { + static FORCE_REPAIR_NONCONVERGENT: Cell = const { Cell::new(false) }; + static BATCH_LOCAL_REPAIR_CALLS: Cell = const { Cell::new(0) }; + } + + #[must_use] + pub(super) fn force_repair_nonconvergent_enabled() -> bool { + FORCE_REPAIR_NONCONVERGENT.with(Cell::get) + } + + #[must_use] + pub(super) fn synthetic_nonconvergent_error() -> DelaunayRepairError { + DelaunayRepairError::NonConvergent { + max_flips: 0, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 0, + flips_performed: 0, + max_queue_len: 0, + ambiguous_predicates: 0, + ambiguous_predicate_samples: Vec::new(), + predicate_failures: 0, + cycle_detections: 0, + cycle_signature_samples: Vec::new(), + attempt: 0, + queue_order: RepairQueueOrder::Fifo, + }), + } + } + + #[must_use] + fn set_force_repair_nonconvergent(enabled: bool) -> bool { + FORCE_REPAIR_NONCONVERGENT.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + + fn restore_force_repair_nonconvergent(prior: bool) { + FORCE_REPAIR_NONCONVERGENT.with(|flag| flag.set(prior)); + } + + fn reset_batch_local_repair_calls() { + BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(0)); + } + + #[must_use] + fn batch_local_repair_calls() -> usize { + BATCH_LOCAL_REPAIR_CALLS.with(Cell::get) + } + + pub(super) fn record_batch_local_repair_call() { + BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(calls.get().saturating_add(1))); + } + fn synthetic_delaunay_verification_error() -> DelaunayTriangulationValidationError { DelaunayTriangulationValidationError::VerificationFailed { source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed { @@ -5770,14 +5972,14 @@ mod tests { impl ForceRepairNonconvergentGuard { fn enable() -> Self { - let prior = test_hooks::set_force_repair_nonconvergent(true); + let prior = set_force_repair_nonconvergent(true); Self { prior } } } impl Drop for ForceRepairNonconvergentGuard { fn drop(&mut self) { - test_hooks::restore_force_repair_nonconvergent(self.prior); + restore_force_repair_nonconvergent(self.prior); } } @@ -6223,7 +6425,7 @@ mod tests { vertex!([0.35, 0.25, 0.15, 0.3]).unwrap(), ]; - test_hooks::reset_batch_local_repair_calls(); + reset_batch_local_repair_calls(); let _guard = ForceRepairNonconvergentGuard::enable(); let kernel = RobustKernel::::new(); let options = ConstructionOptions::default() @@ -6235,7 +6437,7 @@ mod tests { assert_eq!(dt.number_of_vertices(), vertices.len()); assert_eq!(stats.inserted, vertices.len()); - assert_eq!(test_hooks::batch_local_repair_calls(), 1); + assert_eq!(batch_local_repair_calls(), 1); assert!(dt.validate().is_ok()); } @@ -7330,7 +7532,7 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); assert_eq!( - *dt.as_triangulation().tds.construction_state(), + *dt.tds().construction_state(), TriangulationConstructionState::Constructed ); } diff --git a/src/delaunay/delaunayize.rs b/src/delaunay/delaunayize.rs index f0b7de7d..2a24411b 100644 --- a/src/delaunay/delaunayize.rs +++ b/src/delaunay/delaunayize.rs @@ -69,7 +69,7 @@ use crate::core::algorithms::pl_manifold_repair::{ }; use crate::core::collections::{Entry, FastHashMap, SimplexVertexUuidBuffer}; use crate::core::simplex::Simplex; -use crate::core::tds::{SimplexKey, Tds, TdsMutationError}; +use crate::core::tds::{SimplexKey, TdsMutationError}; use crate::core::traits::data_type::DataType; use crate::core::vertex::Vertex; use crate::delaunay_rollback::{DelaunayRollbackTransaction, DelaunaySpatialIndexRollback}; @@ -78,55 +78,6 @@ use crate::repair::DelaunayRepairHeuristicConfig; use crate::triangulation::DelaunayTriangulation; use thiserror::Error; -#[cfg(test)] -mod test_hooks { - use super::DelaunayRepairError; - use crate::core::algorithms::flips::{DelaunayRepairDiagnostics, RepairQueueOrder}; - use std::cell::Cell as ThreadCell; - - thread_local! { - static FORCE_DELAUNAY_REPAIR_FAILURE: ThreadCell = const { ThreadCell::new(false) }; - } - - /// Enables or disables a synthetic Delaunay repair failure for branch tests. - pub(super) fn set_force_delaunay_repair_failure(enabled: bool) -> bool { - FORCE_DELAUNAY_REPAIR_FAILURE.with(|flag| { - let prior = flag.get(); - flag.set(enabled); - prior - }) - } - - /// Restores the previous synthetic Delaunay repair failure state. - pub(super) fn restore_force_delaunay_repair_failure(prior: bool) { - FORCE_DELAUNAY_REPAIR_FAILURE.with(|flag| flag.set(prior)); - } - - /// Reports whether synthetic Delaunay repair failure is enabled. - pub(super) fn force_delaunay_repair_failure_enabled() -> bool { - FORCE_DELAUNAY_REPAIR_FAILURE.with(ThreadCell::get) - } - - /// Builds the synthetic non-convergence error used by fallback branch tests. - pub(super) fn synthetic_repair_error() -> DelaunayRepairError { - DelaunayRepairError::NonConvergent { - max_flips: 0, - diagnostics: Box::new(DelaunayRepairDiagnostics { - facets_checked: 1, - flips_performed: 0, - max_queue_len: 1, - ambiguous_predicates: 0, - ambiguous_predicate_samples: Vec::new(), - predicate_failures: 0, - cycle_detections: 0, - cycle_signature_samples: Vec::new(), - attempt: 1, - queue_order: RepairQueueOrder::Fifo, - }), - } - } -} - // ============================================================================= // CONFIGURATION // ============================================================================= @@ -498,18 +449,18 @@ impl FallbackRebuildSnapshot { /// /// Returns [`SimplexValidationError`] if any simplex cannot resolve all vertex /// UUIDs needed to build its order-independent payload signature. -fn snapshot_rebuild_state( - tds: &Tds, +fn snapshot_rebuild_state( + dt: &DelaunayTriangulation, ) -> Result, SimplexValidationError> where U: Copy, V: Copy, { - let vertices = tds + let vertices = dt .vertices() .map(|(_, v)| Vertex::from_validated_point_with_uuid(*v.point(), v.uuid(), v.data)) .collect::>(); - let simplex_data = collect_simplex_data(tds)?; + let simplex_data = collect_simplex_data(dt)?; Ok(FallbackRebuildSnapshot { vertices, simplex_data, @@ -523,15 +474,15 @@ where /// /// Returns [`SimplexValidationError`] if a simplex references a vertex whose /// UUID cannot be resolved. -fn collect_simplex_data( - tds: &Tds, +fn collect_simplex_data( + dt: &DelaunayTriangulation, ) -> Result, SimplexValidationError> where V: Copy, { let mut simplex_data = FastHashMap::default(); - for (_, simplex) in tds.simplices() { - let vertex_uuids = simplex_vertex_uuids(tds, simplex)?; + for (_, simplex) in dt.simplices() { + let vertex_uuids = simplex_vertex_uuids(dt, simplex)?; match simplex_data.entry(vertex_uuids) { Entry::Vacant(entry) => { entry.insert(SimplexDataMatch::Unique(simplex.data().copied())); @@ -551,13 +502,17 @@ where /// /// Returns [`SimplexValidationError`] if any simplex vertex key cannot be /// resolved to its stable vertex UUID. -fn simplex_vertex_uuids( - tds: &Tds, +fn simplex_vertex_uuids( + dt: &DelaunayTriangulation, simplex: &Simplex, ) -> Result { - let mut vertex_uuids = simplex - .vertex_uuid_iter(tds) - .collect::>()?; + let mut vertex_uuids = SimplexVertexUuidBuffer::new(); + for &vertex_key in simplex.vertices() { + let vertex_uuid = dt + .vertex_uuid_from_key(vertex_key) + .ok_or(SimplexValidationError::VertexKeyNotFound { key: vertex_key })?; + vertex_uuids.push(vertex_uuid); + } vertex_uuids.sort_unstable(); Ok(vertex_uuids) } @@ -578,7 +533,7 @@ where { let mut assignments: Vec<(SimplexKey, V)> = Vec::new(); for (simplex_key, simplex) in rebuilt.simplices() { - let vertex_uuids = simplex_vertex_uuids(rebuilt.tds(), simplex)?; + let vertex_uuids = simplex_vertex_uuids(rebuilt, simplex)?; let Some(SimplexDataMatch::Unique(Some(data))) = original_simplex_data.get(&vertex_uuids) else { continue; @@ -823,7 +778,7 @@ where let failed_delaunay_stats = failed_delaunay_repair_stats(&repair_err); let fallback_result = { - let kernel = &transaction.delaunay_mut().as_triangulation().kernel; + let kernel = transaction.delaunay_mut().kernel(); rebuild_preserving_data(kernel, &fallback_snapshot) }; match fallback_result { @@ -955,8 +910,7 @@ where // Step 1: PL-manifold topology repair. let fallback_snapshot = if config.fallback_rebuild { - let tds = &transaction.delaunay_mut().as_triangulation().tds; - match snapshot_rebuild_state(tds) { + match snapshot_rebuild_state(transaction.delaunay_mut()) { Ok(snapshot) => Some(snapshot), Err(source) => { transaction.rollback(); @@ -982,7 +936,7 @@ where }; let failed_topology_stats = failed_topology_repair_stats(&topo_err); let fallback_result = { - let kernel = &transaction.delaunay_mut().as_triangulation().kernel; + let kernel = transaction.delaunay_mut().kernel(); rebuild_preserving_data(kernel, &fallback_snapshot) }; match fallback_result { @@ -1010,14 +964,13 @@ where // Step 2: Flip-based Delaunay repair. // This is rebuild input only; rollback remains owned by `transaction`. let pre_delaunay_fallback_snapshot = if config.fallback_rebuild { - let tds = &transaction.delaunay_mut().as_triangulation().tds; - Some(snapshot_rebuild_state(tds)) + Some(snapshot_rebuild_state(transaction.delaunay_mut())) } else { None }; #[cfg(test)] - let delaunay_result = if test_hooks::force_delaunay_repair_failure_enabled() { - Err(test_hooks::synthetic_repair_error()) + let delaunay_result = if tests::force_delaunay_repair_failure_enabled() { + Err(tests::synthetic_repair_error()) } else { run_configured_delaunay_repair(transaction.delaunay_mut(), config) }; @@ -1039,17 +992,116 @@ where #[cfg(test)] mod tests { use super::*; + use crate::core::algorithms::flips::{DelaunayRepairDiagnostics, RepairQueueOrder}; use crate::geometry::kernel::AdaptiveKernel; use crate::geometry::point::Point; - use crate::tds::{TdsError, VertexKey}; + use crate::tds::{Tds, TdsError, VertexKey}; use crate::try_vertices_from_points; use crate::vertex; use crate::{DelaunayTriangulationBuilder, TriangulationConstructionError}; use slotmap::KeyData; use std::assert_matches; + use std::cell::Cell; use std::error::Error as StdError; use uuid::Uuid; + // Last-resort fault injection for rollback branches that are hard to + // trigger deterministically; thread-local state avoids cross-test leakage. + // Remove this once a cleaner harness can reach the branch directly. + thread_local! { + static FORCE_DELAUNAY_REPAIR_FAILURE: Cell = const { Cell::new(false) }; + } + + #[must_use] + pub(super) fn force_delaunay_repair_failure_enabled() -> bool { + FORCE_DELAUNAY_REPAIR_FAILURE.with(Cell::get) + } + + #[must_use] + pub(super) fn synthetic_repair_error() -> DelaunayRepairError { + DelaunayRepairError::NonConvergent { + max_flips: 0, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 1, + flips_performed: 0, + max_queue_len: 1, + ambiguous_predicates: 0, + ambiguous_predicate_samples: Vec::new(), + predicate_failures: 0, + cycle_detections: 0, + cycle_signature_samples: Vec::new(), + attempt: 1, + queue_order: RepairQueueOrder::Fifo, + }), + } + } + + #[must_use] + fn set_force_delaunay_repair_failure(enabled: bool) -> bool { + FORCE_DELAUNAY_REPAIR_FAILURE.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + + fn restore_force_delaunay_repair_failure(prior: bool) { + FORCE_DELAUNAY_REPAIR_FAILURE.with(|flag| flag.set(prior)); + } + + /// Snapshots deliberately malformed raw TDS fixtures for payload-disambiguation tests. + fn snapshot_rebuild_state_from_tds( + tds: &Tds, + ) -> Result, SimplexValidationError> + where + U: Copy, + V: Copy, + { + let vertices = tds + .vertices() + .map(|(_, v)| Vertex::from_validated_point_with_uuid(*v.point(), v.uuid(), v.data)) + .collect::>(); + let simplex_data = collect_simplex_data_from_tds(tds)?; + Ok(FallbackRebuildSnapshot { + vertices, + simplex_data, + }) + } + + /// Hashes simplex payloads from invalid raw TDS fixtures by sorted vertex UUIDs. + fn collect_simplex_data_from_tds( + tds: &Tds, + ) -> Result, SimplexValidationError> + where + V: Copy, + { + let mut simplex_data = FastHashMap::default(); + for (_, simplex) in tds.simplices() { + let vertex_uuids = simplex_vertex_uuids_from_tds(tds, simplex)?; + match simplex_data.entry(vertex_uuids) { + Entry::Vacant(entry) => { + entry.insert(SimplexDataMatch::Unique(simplex.data().copied())); + } + Entry::Occupied(mut entry) => { + entry.insert(SimplexDataMatch::Ambiguous); + } + } + } + Ok(simplex_data) + } + + /// Builds the sorted vertex-UUID simplex identity for invalid raw TDS fixtures. + fn simplex_vertex_uuids_from_tds( + tds: &Tds, + simplex: &Simplex, + ) -> Result { + let mut vertex_uuids = simplex + .vertex_uuid_iter(tds) + .collect::>()?; + vertex_uuids.sort_unstable(); + Ok(vertex_uuids) + } + // ============================================================================= // HELPER FUNCTIONS // ============================================================================= @@ -1177,7 +1229,7 @@ mod tests { fn boundary_ridge_multiplicity_dt() -> DelaunayTriangulation, (), (), 3> { let vertices = unit_simplex_vertices::<3>(); let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - *dt.tds_mut() = make_boundary_ridge_multiplicity_tds(); + *dt.tds_mut_for_repair() = make_boundary_ridge_multiplicity_tds(); dt } @@ -1185,7 +1237,7 @@ mod tests { fn cone_on_torus_dt() -> DelaunayTriangulation, (), (), 3> { let vertices = unit_simplex_vertices::<3>(); let mut dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - *dt.tds_mut() = make_cone_on_torus_tds(); + *dt.tds_mut_for_repair() = make_cone_on_torus_tds(); dt } @@ -1322,14 +1374,14 @@ mod tests { /// Enables synthetic Delaunay repair failure until the guard is dropped. fn enable() -> Self { Self { - prior: test_hooks::set_force_delaunay_repair_failure(true), + prior: set_force_delaunay_repair_failure(true), } } } impl Drop for ForceDelaunayRepairFailureGuard { fn drop(&mut self) { - test_hooks::restore_force_delaunay_repair_failure(self.prior); + restore_force_delaunay_repair_failure(self.prior); } } @@ -1428,7 +1480,7 @@ mod tests { let simplex = Simplex::try_new_with_data(vertex_keys, Some(7)).unwrap(); tds.remove_isolated_vertex(missing).unwrap(); - let err = simplex_vertex_uuids(&tds, &simplex).unwrap_err(); + let err = simplex_vertex_uuids_from_tds(&tds, &simplex).unwrap_err(); assert_eq!( err, @@ -1884,10 +1936,9 @@ mod tests { let original_simplex_key = dt.simplices().next().unwrap().0; dt.set_simplex_data(original_simplex_key, Some(42)).unwrap(); - let tds = &dt.as_triangulation().tds; - let snapshot = snapshot_rebuild_state(tds).unwrap(); + let snapshot = snapshot_rebuild_state(&dt).unwrap(); - let rebuilt = rebuild_preserving_data(&dt.as_triangulation().kernel, &snapshot).unwrap(); + let rebuilt = rebuild_preserving_data(dt.kernel(), &snapshot).unwrap(); let (_, rebuilt_simplex) = rebuilt.simplices().next().unwrap(); assert_eq!(rebuilt_simplex.data(), Some(&42)); @@ -1915,7 +1966,7 @@ mod tests { tds.insert_simplex_bypassing_topology_checks_for_test(duplicate_b) .unwrap(); - let snapshot = snapshot_rebuild_state(&tds).unwrap(); + let snapshot = snapshot_rebuild_state_from_tds(&tds).unwrap(); let kernel = AdaptiveKernel::new(); let mut rebuilt: DelaunayTriangulation<_, (), i32, 2> = DelaunayTriangulationBuilder::new(snapshot.vertices()) diff --git a/src/delaunay/deletion.rs b/src/delaunay/deletion.rs index b62395b8..4386137b 100644 --- a/src/delaunay/deletion.rs +++ b/src/delaunay/deletion.rs @@ -8,10 +8,8 @@ #![forbid(unsafe_code)] use crate::construction::local_repair_flip_budget; -#[cfg(test)] -use crate::construction::test_hooks; use crate::core::algorithms::flips::{ - FlipError, apply_bistellar_flip_k1_inverse, repair_delaunay_with_flips_k2_k3, + FlipError, apply_bistellar_flip_k1_inverse_raw, repair_delaunay_with_flips_k2_k3, }; use crate::core::collections::SimplexKeyBuffer; use crate::core::tds::{InvariantError, NeighborValidationError, TdsError, VertexKey}; @@ -52,6 +50,41 @@ impl From for DeleteVertexError { } } +/// Tries the inverse-k1 fast path before falling back to fan deletion so the +/// transaction can reuse the cheapest valid removal proof available. +fn remove_with_fast_path_or_fallback( + transaction: &mut DelaunayRollbackTransaction<'_, K, U, V, D>, + vertex_key: VertexKey, +) -> Result<(usize, Option), InvariantError> +where + K: Kernel, + U: DataType, + V: DataType, +{ + let fast_path_result = { + let delaunay = transaction.delaunay_mut(); + apply_bistellar_flip_k1_inverse_raw(&mut delaunay.tri.tds, vertex_key) + }; + match fast_path_result { + Ok(info) => Ok((info.removed_simplices.len(), Some(info.new_simplices))), + Err(FlipError::NeighborWiring { reason }) => { + transaction.restore(); + Err(TdsError::InvalidNeighbors { + reason: NeighborValidationError::FlipNeighborWiring { reason }, + } + .into()) + } + Err(_) => { + transaction.restore(); + let delaunay = transaction.delaunay_mut(); + let outcome = delaunay.tri.remove_vertex_with_repair_seeds(vertex_key)?; + let seed_simplices = (!outcome.repair_seed_simplices.is_empty()) + .then_some(outcome.repair_seed_simplices); + Ok((outcome.simplices_removed, seed_simplices)) + } + } +} + // ============================================================================= // VERTEX DELETION (Requires Numeric Scalar Bounds) // ============================================================================= @@ -132,18 +165,9 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # DeleteVertex(#[from] delaunay::DeleteVertexError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let interior = delaunay::vertex![0.3, 0.3]?; /// let interior_uuid = interior.uuid(); /// let vertices = [ @@ -172,21 +196,12 @@ where /// Deletions that would leave a non-manifold remnant fail and roll back: /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::deletion::DeleteVertexError; /// use delaunay::prelude::tds::InvariantError; /// use delaunay::prelude::triangulation::TriangulationValidationError; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # #[error(transparent)] - /// # Delete(#[from] DeleteVertexError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -220,33 +235,12 @@ where let mut transaction = DelaunayRollbackTransaction::begin(self, DelaunaySpatialIndexRollback::Restore); - let result: Result = { - let delaunay = transaction.delaunay_mut(); - (|| { - // Fast path: inverse k=1 flip when the vertex star is a simplex. - let mut seed_simplices: Option = None; - let simplices_removed = - match apply_bistellar_flip_k1_inverse(&mut delaunay.tri.tds, vertex_key) { - Ok(info) => { - seed_simplices = Some(info.new_simplices); - info.removed_simplices.len() - } - Err(FlipError::NeighborWiring { reason }) => { - return Err(TdsError::InvalidNeighbors { - reason: NeighborValidationError::FlipNeighborWiring { reason }, - } - .into()); - } - Err(_) => { - let outcome = - delaunay.tri.remove_vertex_with_repair_seeds(vertex_key)?; - if !outcome.repair_seed_simplices.is_empty() { - seed_simplices = Some(outcome.repair_seed_simplices); - } - outcome.simplices_removed - } - }; + let result: Result = (|| { + let (simplices_removed, seed_simplices) = + remove_with_fast_path_or_fallback(&mut transaction, vertex_key)?; + { + let delaunay = transaction.delaunay_mut(); let topology = delaunay.tri.topology_guarantee(); if delaunay.should_run_delaunay_repair_after_mutation(topology) { let seed_ref = seed_simplices.as_deref(); @@ -266,8 +260,8 @@ where }; #[cfg(test)] - let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { - Err(test_hooks::synthetic_nonconvergent_error()) + let repair_result = if tests::force_repair_nonconvergent_enabled() { + Err(tests::synthetic_nonconvergent_error()) } else { repair_result }; @@ -297,10 +291,10 @@ where .is_valid_delaunay() .map_err(InvariantError::Delaunay)?; } + } - Ok(simplices_removed) - })() - }; + Ok(simplices_removed) + })(); match result { Ok(simplices_removed) => { @@ -323,7 +317,9 @@ where #[cfg(test)] mod tests { use super::*; - use crate::core::algorithms::flips::DelaunayRepairError; + use crate::core::algorithms::flips::{ + DelaunayRepairDiagnostics, DelaunayRepairError, RepairQueueOrder, + }; use crate::core::collections::spatial_hash_grid::HashGridIndex; use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; use crate::core::vertex::Vertex; @@ -333,9 +329,50 @@ mod tests { use crate::repair::DelaunayRepairPolicy; use crate::vertex; use std::assert_matches; + use std::cell::Cell; use std::sync::Once; use uuid::Uuid; + // Last-resort fault injection for rollback branches that are hard to + // trigger deterministically; thread-local state avoids cross-test leakage. + // Remove this once a cleaner harness can reach the branch directly. + thread_local! { + static FORCE_REPAIR_NONCONVERGENT: Cell = const { Cell::new(false) }; + } + + #[must_use] + pub(super) fn force_repair_nonconvergent_enabled() -> bool { + FORCE_REPAIR_NONCONVERGENT.with(Cell::get) + } + + #[must_use] + pub(super) fn synthetic_nonconvergent_error() -> DelaunayRepairError { + DelaunayRepairError::NonConvergent { + max_flips: 0, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 0, + flips_performed: 0, + max_queue_len: 0, + ambiguous_predicates: 0, + ambiguous_predicate_samples: Vec::new(), + predicate_failures: 0, + cycle_detections: 0, + cycle_signature_samples: Vec::new(), + attempt: 0, + queue_order: RepairQueueOrder::Fifo, + }), + } + } + + #[must_use] + fn set_force_repair_nonconvergent(enabled: bool) -> bool { + FORCE_REPAIR_NONCONVERGENT.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + fn init_tracing() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -355,14 +392,14 @@ mod tests { impl ForceRepairNonconvergentGuard { fn enable() -> Self { Self { - previous: test_hooks::set_force_repair_nonconvergent(true), + previous: set_force_repair_nonconvergent(true), } } } impl Drop for ForceRepairNonconvergentGuard { fn drop(&mut self) { - let _ = test_hooks::set_force_repair_nonconvergent(self.previous); + let _ = set_force_repair_nonconvergent(self.previous); } } diff --git a/src/delaunay/flips.rs b/src/delaunay/flips.rs index 5d7c353b..cda57be3 100644 --- a/src/delaunay/flips.rs +++ b/src/delaunay/flips.rs @@ -1,7 +1,10 @@ //! Triangulation editing operations (bistellar flips). //! -//! This module exposes **high-level** flip methods for explicit triangulation editing. +//! This module exposes direct bistellar-flip methods for explicit triangulation editing. //! These operations do **not** automatically restore the Delaunay property. +//! For queued, randomized, or Monte-Carlo-style local edits, prefer the staged +//! [`PachnerMoves`](crate::pachner::PachnerMoves) workflow: +//! `propose_pachner(...)?.attempt_on(...)`. //! For Delaunay construction/deletion, use //! [`crate::DelaunayTriangulation::insert_vertex`] and //! [`crate::DelaunayTriangulation::delete_vertex`]. @@ -19,42 +22,81 @@ pub use crate::core::algorithms::flips::{ FlipNeighborHullExtensionFailureKind, FlipNeighborRepairDiagnostics, FlipNeighborRepairFailure, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, RidgeHandle, - TriangleHandle, TriangleHandleError, verify_delaunay_for_triangulation, - verify_delaunay_via_flip_predicates, + TriangleHandle, TriangleHandleError, }; pub use crate::tds::{EdgeKey, FacetHandle, SimplexKey, VertexKey}; use crate::core::algorithms::flips::{ - apply_bistellar_flip_dynamic, apply_bistellar_flip_k1, apply_bistellar_flip_k1_inverse, - apply_bistellar_flip_k2, apply_bistellar_flip_k3, build_k2_flip_context, + apply_bistellar_flip_dynamic_raw, apply_bistellar_flip_k1_inverse_raw, + apply_bistellar_flip_k1_raw, apply_bistellar_flip_raw, build_k2_flip_context, build_k2_flip_context_from_edge, build_k3_flip_context, build_k3_flip_context_from_triangle, validate_bistellar_flip_dynamic, validate_bistellar_flip_k1_insert, validate_bistellar_flip_k1_inverse, validate_bistellar_flip_k2, validate_bistellar_flip_k3, }; -#[cfg(test)] -use crate::core::facet::FacetError; +use crate::core::rollback::TriangulationRollbackTransaction; use crate::core::traits::data_type::DataType; use crate::core::triangulation::Triangulation; use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; use crate::triangulation::DelaunayTriangulation; -/// High-level triangulation editing operations via bistellar flips. + +/// Applies a high-level flip transaction and preserves topology/embedding invariants. +fn apply_embedded_flip( + tri: &mut Triangulation, + apply: impl FnOnce(&mut Triangulation) -> Result, FlipError>, +) -> Result, FlipError> +where + K: Kernel, + U: DataType, + V: DataType, +{ + let mut transaction = TriangulationRollbackTransaction::begin(tri); + let result = apply(transaction.triangulation_mut()); + let info = match result { + Ok(info) => info, + Err(error) => { + transaction.rollback(); + return Err(error); + } + }; + + if let Err(error) = transaction + .triangulation_mut() + .normalize_and_promote_positive_orientation() + { + transaction.rollback(); + return Err(FlipError::PostconditionRepair { + source: Box::new(error), + }); + } + if let Err(source) = transaction.triangulation_mut().validate_embedding() { + transaction.rollback(); + return Err(FlipError::EmbeddingValidation { + source: Box::new(source), + }); + } + + transaction.commit(); + Ok(info) +} +/// Direct triangulation editing operations via bistellar flips. +/// +/// This trait is the primitive/expert editing layer. Public workflows that +/// store, randomize, or queue moves should normally parse a raw +/// [`PachnerMove`](crate::pachner::PachnerMove) into a provenanced +/// [`PachnerProposal`](crate::pachner::PachnerProposal), then call +/// [`PachnerProposal::attempt_on`](crate::pachner::PachnerProposal::attempt_on) +/// as the mutating terminal step. /// /// # Example /// /// ```rust -/// use delaunay::prelude::construction::{DelaunayTriangulationBuilder, TopologyGuarantee}; +/// use delaunay::prelude::construction::{ +/// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, +/// }; /// use delaunay::flips::BistellarFlips; /// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Flip(#[from] delaunay::flips::FlipError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { +/// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -87,19 +129,12 @@ pub trait BistellarFlips { /// # Example /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder, TopologyGuarantee}; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, + /// }; /// use delaunay::flips::BistellarFlips; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Flip(#[from] delaunay::flips::FlipError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -185,19 +220,12 @@ pub trait BistellarFlips { /// # Example /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder, TopologyGuarantee}; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, + /// }; /// use delaunay::flips::BistellarFlips; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Flip(#[from] delaunay::flips::FlipError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -275,19 +303,10 @@ pub trait BistellarFlips { /// # Example /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::flips::{BistellarFlips, FacetHandle}; - /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::flips::BistellarFlips; + /// + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -297,18 +316,24 @@ pub trait BistellarFlips { /// ]; /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// // Find an interior facet and attempt a k=2 flip - /// // Note: k=2 flips require specific geometric conditions - /// let simplex_key = dt.simplices().next().map(|(k, _)| k); - /// if let Some(key) = simplex_key { - /// let has_neighbor = dt.tds().simplex(key) - /// .and_then(|simplex| simplex.neighbors()) - /// .is_some_and(|mut neighbors| neighbors.any(|n| n.is_some())); - /// if has_neighbor { - /// let facet = FacetHandle::try_new(dt.tds(), key, 0)?; - /// let _ = dt.flip_k2(facet); // May succeed or fail depending on configuration + /// // Find an interior facet and attempt a k=2 flip. k=2 flips require + /// // specific geometric conditions, so this may still fail. + /// let mut interior_facet = None; + /// for facet in dt.facets() { + /// let facet = facet?; + /// if facet + /// .simplex() + /// .neighbor_key(usize::from(facet.facet_index())) + /// .flatten() + /// .is_some() + /// { + /// interior_facet = Some(facet.handle()); + /// break; /// } /// } + /// if let Some(facet) = interior_facet { + /// let _ = dt.flip_k2(facet); + /// } /// # Ok(()) /// # } /// ``` @@ -327,7 +352,7 @@ pub trait BistellarFlips { /// # Examples /// /// ```rust - /// use delaunay::flips::{BistellarFlipKind, BistellarFlips, FacetHandle}; + /// use delaunay::flips::{BistellarFlipKind, BistellarFlips}; /// use delaunay::prelude::construction::{ /// DelaunayResult, DelaunayTriangulationBuilder, /// DelaunayTriangulationConstructionError, @@ -349,24 +374,21 @@ pub trait BistellarFlips { /// .build()?; /// /// let mut accepted = None; - /// 'simplices: for (simplex_key, simplex) in dt.simplices() { - /// let Some(neighbors) = simplex.neighbors() else { + /// for facet in dt.facets() { + /// let Ok(facet) = facet else { /// continue; /// }; - /// for (facet_index, neighbor) in neighbors.enumerate() { - /// if neighbor.is_none() { - /// continue; - /// } - /// let Ok(facet_index) = u8::try_from(facet_index) else { - /// continue; - /// }; - /// let Ok(facet) = FacetHandle::try_new(dt.tds(), simplex_key, facet_index) else { - /// continue; - /// }; - /// if let Ok(feasibility) = dt.can_flip_k2(facet) { - /// accepted = Some(feasibility); - /// break 'simplices; - /// } + /// if facet + /// .simplex() + /// .neighbor_key(usize::from(facet.facet_index())) + /// .flatten() + /// .is_none() + /// { + /// continue; + /// } + /// if let Ok(feasibility) = dt.can_flip_k2(facet.handle()) { + /// accepted = Some(feasibility); + /// break; /// } /// } /// @@ -386,17 +408,10 @@ pub trait BistellarFlips { /// # Example /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::flips::{BistellarFlips, RidgeHandle}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -427,7 +442,7 @@ pub trait BistellarFlips { /// # Examples /// /// ```rust - /// use delaunay::flips::{BistellarFlips, FlipError, RidgeHandle}; + /// use delaunay::flips::{BistellarFlips, FlipError}; /// use delaunay::prelude::construction::{ /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, /// }; @@ -445,7 +460,7 @@ pub trait BistellarFlips { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let Ok(ridge) = RidgeHandle::try_new(dt.tds(), simplex_key, 0, 1) else { + /// let Ok(ridge) = dt.ridge_handle(simplex_key, 0, 1) else { /// return Ok(()); /// }; /// @@ -477,7 +492,7 @@ pub trait BistellarFlips { /// # Examples /// /// ```rust - /// use delaunay::flips::{BistellarFlips, EdgeKey, FlipError}; + /// use delaunay::flips::{BistellarFlips, FlipError}; /// use delaunay::prelude::construction::{ /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, /// }; @@ -492,13 +507,7 @@ pub trait BistellarFlips { /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .topology_guarantee(TopologyGuarantee::PLManifold) /// .build()?; - /// let Some((_, simplex)) = dt.simplices().next() else { - /// return Ok(()); - /// }; - /// let [a, b, ..] = simplex.vertices() else { - /// return Ok(()); - /// }; - /// let Ok(edge) = EdgeKey::try_new(dt.tds(), *a, *b) else { + /// let Some(edge) = dt.edges().next() else { /// return Ok(()); /// }; /// @@ -577,6 +586,7 @@ pub trait BistellarFlips { impl BistellarFlips for Triangulation where + K: Kernel, U: DataType, V: DataType, { @@ -587,7 +597,9 @@ where simplex_key: SimplexKey, vertex: Vertex, ) -> Result, FlipError> { - apply_bistellar_flip_k1(&mut self.tds, simplex_key, vertex) + apply_embedded_flip(self, |tri| { + apply_bistellar_flip_k1_raw(&mut tri.tds, simplex_key, vertex) + }) } fn can_flip_k1_insert( @@ -599,7 +611,9 @@ where } fn flip_k1_remove(&mut self, vertex_key: VertexKey) -> Result, FlipError> { - apply_bistellar_flip_k1_inverse(&mut self.tds, vertex_key) + apply_embedded_flip(self, |tri| { + apply_bistellar_flip_k1_inverse_raw(&mut tri.tds, vertex_key) + }) } fn can_flip_k1_remove(&self, vertex_key: VertexKey) -> Result, FlipError> { @@ -607,8 +621,10 @@ where } fn flip_k2(&mut self, facet: FacetHandle) -> Result, FlipError> { - let context = build_k2_flip_context(&self.tds, facet)?; - apply_bistellar_flip_k2(&mut self.tds, &context) + apply_embedded_flip(self, |tri| { + let context = build_k2_flip_context(&tri.tds, facet)?; + apply_bistellar_flip_raw::(&mut tri.tds, &context) + }) } fn can_flip_k2(&self, facet: FacetHandle) -> Result, FlipError> { @@ -617,8 +633,10 @@ where } fn flip_k3(&mut self, ridge: RidgeHandle) -> Result, FlipError> { - let context = build_k3_flip_context(&self.tds, ridge)?; - apply_bistellar_flip_k3(&mut self.tds, &context) + apply_embedded_flip(self, |tri| { + let context = build_k3_flip_context(&tri.tds, ridge)?; + apply_bistellar_flip_raw::(&mut tri.tds, &context) + }) } fn can_flip_k3(&self, ridge: RidgeHandle) -> Result, FlipError> { @@ -627,8 +645,10 @@ where } fn flip_k2_inverse_from_edge(&mut self, edge: EdgeKey) -> Result, FlipError> { - let context = build_k2_flip_context_from_edge(&self.tds, edge)?; - apply_bistellar_flip_dynamic(&mut self.tds, D, &context) + apply_embedded_flip(self, |tri| { + let context = build_k2_flip_context_from_edge(&tri.tds, edge)?; + apply_bistellar_flip_dynamic_raw(&mut tri.tds, D, &context) + }) } fn can_flip_k2_inverse_from_edge( @@ -647,15 +667,17 @@ where return Err(FlipError::UnsupportedDimension { dimension: D }); } - let context = build_k3_flip_context_from_triangle(&self.tds, triangle)?; + apply_embedded_flip(self, |tri| { + let context = build_k3_flip_context_from_triangle(&tri.tds, triangle)?; - // Avoid const-eval underflow for invalid instantiations (e.g. D=0), even though - // the public contract for this method requires D>=4. - let k_move = D - .checked_sub(1) - .ok_or(FlipError::UnsupportedDimension { dimension: D })?; + // Avoid const-eval underflow for invalid instantiations (e.g. D=0), even though + // the public contract for this method requires D>=4. + let k_move = D + .checked_sub(1) + .ok_or(FlipError::UnsupportedDimension { dimension: D })?; - apply_bistellar_flip_dynamic(&mut self.tds, k_move, &context) + apply_bistellar_flip_dynamic_raw(&mut tri.tds, k_move, &context) + }) } fn can_flip_k3_inverse_from_triangle( @@ -680,6 +702,7 @@ where impl BistellarFlips for DelaunayTriangulation where + K: Kernel, U: DataType, V: DataType, { @@ -778,11 +801,15 @@ where #[cfg(test)] mod tests { use super::*; + use crate::core::embedding::TriangulationEmbeddingValidationError; + use crate::core::facet::FacetError; use crate::vertex; use std::assert_matches; use crate::TopologyGuarantee; - use crate::core::collections::spatial_hash_grid::HashGridIndex; + use crate::core::collections::{ + SimplexKeyBuffer, SmallBuffer, spatial_hash_grid::HashGridIndex, + }; use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; use slotmap::KeyData; @@ -798,7 +825,7 @@ mod tests { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); let simplex_key = tri.simplices().next().unwrap().0; let inserted = tri @@ -813,6 +840,75 @@ mod tests { assert!(tri.validate().is_ok()); } + #[test] + fn triangulation_flip_k1_insert_rolls_back_degenerate_insert() { + let vertices = vec![ + vertex!([0.0, 0.0]).unwrap(), + vertex!([1.0, 0.0]).unwrap(), + vertex!([0.0, 1.0]).unwrap(), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build() + .unwrap(); + let mut tri = dt.into_triangulation(); + let simplex_key = tri.simplices().next().unwrap().0; + let before_vertices = tri.tds.number_of_vertices(); + let before_simplices = tri.tds.number_of_simplices(); + let inserted = vertex!([0.5, 0.0]).unwrap(); + let inserted_uuid = inserted.uuid(); + + let err = tri.flip_k1_insert(simplex_key, inserted).unwrap_err(); + + assert_matches!(err, FlipError::DegenerateSimplex); + assert_eq!(tri.tds.number_of_vertices(), before_vertices); + assert_eq!(tri.tds.number_of_simplices(), before_simplices); + assert!(tri.vertex_key_from_uuid(&inserted_uuid).is_none()); + assert!(tri.validate().is_ok()); + assert!(tri.is_valid_embedding().is_ok()); + } + + #[test] + fn embedded_flip_transaction_rolls_back_topology_validation_failure() { + let vertices = vec![ + vertex!([0.0, 0.0]).unwrap(), + vertex!([1.0, 0.0]).unwrap(), + vertex!([0.0, 1.0]).unwrap(), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build() + .unwrap(); + let mut tri = dt.into_triangulation(); + let before_vertices = tri.tds.number_of_vertices(); + let before_simplices = tri.tds.number_of_simplices(); + + let err = apply_embedded_flip(&mut tri, |tri| { + tri.tds + .insert_vertex_with_mapping(vertex!([2.0, 2.0]).unwrap()) + .unwrap(); + Ok(FlipInfo { + kind: BistellarFlipKind::k1(2), + direction: FlipDirection::Forward, + removed_simplices: SimplexKeyBuffer::default(), + new_simplices: SimplexKeyBuffer::default(), + removed_face_vertices: SmallBuffer::default(), + inserted_face_vertices: SmallBuffer::default(), + }) + }) + .unwrap_err(); + + assert_matches!( + err, + FlipError::EmbeddingValidation { source } + if matches!(*source, TriangulationEmbeddingValidationError::Triangulation(_)) + ); + assert_eq!(tri.tds.number_of_vertices(), before_vertices); + assert_eq!(tri.tds.number_of_simplices(), before_simplices); + assert!(tri.validate().is_ok()); + assert!(tri.validate_embedding().is_ok()); + } + #[test] fn flip_k1_insert_invalidates_caches() { let vertices: Vec> = vec![ @@ -852,10 +948,10 @@ mod tests { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let tri = dt.as_triangulation().clone(); + let tri = dt.into_triangulation(); let simplex_key = tri.simplices().next().unwrap().0; - let err = FacetHandle::try_new(&tri.tds, simplex_key, u8::MAX).unwrap_err(); + let err = tri.facet_handle(simplex_key, u8::MAX).unwrap_err(); assert_matches!( err, diff --git a/src/delaunay/insertion.rs b/src/delaunay/insertion.rs index 612d222b..4bf67f56 100644 --- a/src/delaunay/insertion.rs +++ b/src/delaunay/insertion.rs @@ -15,8 +15,6 @@ #![forbid(unsafe_code)] -#[cfg(test)] -use crate::construction::test_hooks; use crate::core::algorithms::flips::{ DelaunayRepairError, DelaunayRepairRun, repair_delaunay_with_flips_k2_k3_run, }; @@ -32,13 +30,9 @@ use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; use crate::core::vertex::Vertex; use crate::delaunay_rollback::{DelaunayRollbackTransaction, DelaunaySpatialIndexRollback}; use crate::geometry::kernel::Kernel; -use crate::topology::manifold::{ManifoldError, validate_ridge_links_for_simplices}; +use crate::topology::manifold::ManifoldError; use crate::triangulation::DelaunayTriangulation; -#[cfg(test)] -use crate::validation::DelaunayTriangulationCandidate; use std::env; -#[cfg(test)] -use std::iter::once; fn ridge_link_repair_validation_error(err: ManifoldError) -> InsertionError { match TriangulationValidationError::try_from(err) { @@ -490,8 +484,8 @@ where }; #[cfg(test)] - let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { - Err(test_hooks::synthetic_nonconvergent_error()) + let repair_result = if tests::force_repair_nonconvergent_enabled() { + Err(tests::synthetic_nonconvergent_error()) } else { repair_result }; @@ -559,14 +553,12 @@ where run.touched_simplices.len() ); } - return validate_ridge_links_for_simplices( - &self.tri.tds, - run.touched_simplices.iter().copied(), - ) - .map_err(ridge_link_repair_validation_error); + return self + .validate_ridge_links_for_simplices(run.touched_simplices.iter().copied()) + .map_err(ridge_link_repair_validation_error); } - validate_ridge_links_for_simplices(&self.tri.tds, self.tri.tds.simplex_keys()) + self.validate_ridge_links_for_simplices(self.simplices().map(|(key, _)| key)) .map_err(ridge_link_repair_validation_error) } @@ -620,18 +612,63 @@ where #[cfg(test)] mod tests { use super::*; - use crate::core::algorithms::flips::DelaunayRepairStats; + use crate::core::algorithms::flips::{ + DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, RepairQueueOrder, + }; use crate::core::simplex::Simplex; use crate::core::tds::{Tds, TdsError}; use crate::geometry::kernel::{AdaptiveKernel, RobustKernel}; use crate::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; use crate::topology::traits::topological_space::GlobalTopology; + use crate::validation::DelaunayTriangulationCandidate; use crate::vertex; use slotmap::KeyData; use std::assert_matches; + use std::cell::Cell; + use std::iter::once; use std::num::NonZeroUsize; use std::sync::Once; + // Last-resort fault injection for rollback branches that are hard to + // trigger deterministically; thread-local state avoids cross-test leakage. + // Remove this once a cleaner harness can reach the branch directly. + thread_local! { + static FORCE_REPAIR_NONCONVERGENT: Cell = const { Cell::new(false) }; + } + + #[must_use] + pub(super) fn force_repair_nonconvergent_enabled() -> bool { + FORCE_REPAIR_NONCONVERGENT.with(Cell::get) + } + + #[must_use] + pub(super) fn synthetic_nonconvergent_error() -> DelaunayRepairError { + DelaunayRepairError::NonConvergent { + max_flips: 0, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 0, + flips_performed: 0, + max_queue_len: 0, + ambiguous_predicates: 0, + ambiguous_predicate_samples: Vec::new(), + predicate_failures: 0, + cycle_detections: 0, + cycle_signature_samples: Vec::new(), + attempt: 0, + queue_order: RepairQueueOrder::Fifo, + }), + } + } + + #[must_use] + fn set_force_repair_nonconvergent(enabled: bool) -> bool { + FORCE_REPAIR_NONCONVERGENT.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + fn init_tracing() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -651,14 +688,14 @@ mod tests { impl ForceRepairNonconvergentGuard { fn enable() -> Self { Self { - previous: test_hooks::set_force_repair_nonconvergent(true), + previous: set_force_repair_nonconvergent(true), } } } impl Drop for ForceRepairNonconvergentGuard { fn drop(&mut self) { - let _ = test_hooks::set_force_repair_nonconvergent(self.previous); + let _ = set_force_repair_nonconvergent(self.previous); } } diff --git a/src/delaunay/property_validation.rs b/src/delaunay/property_validation.rs index ab17ffbe..04767a5e 100644 --- a/src/delaunay/property_validation.rs +++ b/src/delaunay/property_validation.rs @@ -1,7 +1,7 @@ -//! Delaunay empty-circumsphere property scans over bare TDS storage. +//! Delaunay empty-circumsphere property scans over triangulation storage. //! //! This module is the reusable Level 5 property engine: it answers whether a -//! [`Tds`](crate::tds::Tds) violates the Delaunay empty-circumsphere condition and returns +//! topology storage violates the Delaunay empty-circumsphere condition and returns //! repair-oriented keys for offending simplices, vertices, and neighbors. It //! does not own wrapper-level validation policy, cumulative roll-up, or //! construction proofs; those live in `validation`. @@ -94,9 +94,12 @@ pub enum DelaunayValidationError { /// Structured summary of Delaunay empty-circumsphere violations. /// /// This diagnostic report is intended for repair planning, bug reports, -/// regression tests, and local investigation. It records stable TDS keys rather -/// than copying all coordinates; callers can look up coordinates, UUIDs, and -/// simplex data in the original [`Tds`]. +/// regression tests, and local investigation. It records runtime topology +/// handles rather than copying every coordinate; callers can inspect the source +/// triangulation through public owner accessors such as +/// [`DelaunayTriangulation::simplex`](crate::DelaunayTriangulation::simplex), +/// [`DelaunayTriangulation::vertex`](crate::DelaunayTriangulation::vertex), and +/// [`DelaunayTriangulation::simplex_vertices`](crate::DelaunayTriangulation::simplex_vertices). /// /// # Examples /// @@ -104,16 +107,7 @@ pub enum DelaunayValidationError { /// use delaunay::prelude::validation::delaunay_violation_report; /// use delaunay::prelude::*; /// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Validation(#[from] delaunay::DelaunayValidationError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { +/// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -121,7 +115,7 @@ pub enum DelaunayValidationError { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let report = delaunay_violation_report(dt.tds(), None)?; +/// let report = dt.delaunay_violation_report(None)?; /// assert!(report.is_valid()); /// # Ok(()) /// # } @@ -129,15 +123,16 @@ pub enum DelaunayValidationError { #[derive(Clone, Debug, PartialEq, Eq)] #[must_use] pub struct DelaunayViolationReport { - /// Number of vertices in the TDS when the report was generated. + /// Number of vertices in the triangulation when the report was generated. pub number_of_vertices: usize, - /// Number of simplices in the TDS when the report was generated. + /// Number of simplices in the triangulation when the report was generated. pub number_of_simplices: usize, /// Number of requested simplices considered by the report. /// - /// When `simplices_to_check` is `None`, this is the TDS simplex count. When a - /// subset is provided, this is the subset length; missing simplex keys are - /// still counted as requested work and are skipped by the violation scan. + /// When `simplices_to_check` is `None`, this is the triangulation simplex + /// count. When a subset is provided, this is the subset length; missing + /// simplex keys are still counted as requested work and are skipped by the + /// violation scan. pub checked_simplices: usize, /// Simplices that failed the empty-circumsphere property. pub violating_simplices: ViolationBuffer, @@ -182,8 +177,8 @@ impl DelaunayViolationReport { /// /// The detail record keeps the report compact and key-oriented. Use /// [`simplex_key`](Self::simplex_key), [`simplex_vertices`](Self::simplex_vertices), and -/// [`offending_vertex`](Self::offending_vertex) to recover full vertex or simplex -/// records from the source [`Tds`]. +/// [`offending_vertex`](Self::offending_vertex) with the source triangulation's +/// public accessors to recover full vertex or simplex records. /// /// [`neighbor_simplices`](Self::neighbor_simplices) preserves the violating simplex's raw /// [`NeighborSlot`] state for each facet so diagnostics can distinguish @@ -429,18 +424,8 @@ pub fn is_delaunay_property_only( /// /// ``` /// use delaunay::prelude::*; -/// use delaunay::prelude::validation::find_delaunay_violations; /// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Validation(#[from] delaunay::DelaunayValidationError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { +/// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -449,11 +434,9 @@ pub fn is_delaunay_property_only( /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); -/// /// // Find all violating simplices (should be empty for valid Delaunay triangulation) -/// let violations = find_delaunay_violations(tds, None)?; -/// assert!(violations.is_empty()); +/// let report = dt.delaunay_violation_report(None)?; +/// assert!(report.violating_simplices.is_empty()); /// # Ok(()) /// # } /// ``` @@ -540,19 +523,9 @@ pub fn find_delaunay_violations( /// # Examples /// /// ```rust -/// use delaunay::prelude::validation::delaunay_violation_report; /// use delaunay::prelude::*; /// -/// # #[derive(Debug, thiserror::Error)] -/// # enum ExampleError { -/// # #[error(transparent)] -/// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), -/// # #[error(transparent)] -/// # Validation(#[from] delaunay::DelaunayValidationError), -/// # #[error(transparent)] -/// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), -/// # } -/// # fn main() -> Result<(), ExampleError> { +/// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -561,7 +534,7 @@ pub fn find_delaunay_violations( /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let report = delaunay_violation_report(dt.tds(), None)?; +/// let report = dt.delaunay_violation_report(None)?; /// assert!(report.violating_simplices.is_empty()); /// # Ok(()) /// # } @@ -632,7 +605,7 @@ impl From for DelaunayValidationError { /// Debug helper: print detailed information about the first detected Delaunay /// violation (or all vertices if none are found) to aid in debugging. /// -/// This function is intended for use in tests and debug builds only. It uses the +/// This function is intended for diagnostics-enabled debugging workflows. It uses the /// same robust predicates as [`find_delaunay_violations`] (and the crate-private Delaunay-property-only check) /// and prints: /// - A triangulation summary (vertex and simplex counts) @@ -651,7 +624,7 @@ impl From for DelaunayValidationError { /// let tds: Tds<(), (), 3> = Tds::empty(); /// debug_print_first_delaunay_violation(&tds, None); /// ``` -#[cfg(any(test, feature = "diagnostics"))] +#[cfg(feature = "diagnostics")] #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] #[expect( clippy::too_many_lines, @@ -831,23 +804,22 @@ mod tests { ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let tds = &dt.as_triangulation().tds; - // Basic Delaunay helpers should report no violations. + // Public owner-level Delaunay helpers should report no violations. assert!( - is_delaunay_property_only(tds).is_ok(), + dt.is_valid_delaunay().is_ok(), "Simple tetrahedron should satisfy the Delaunay property" ); - let violations = find_delaunay_violations(tds, None).unwrap(); + let violations = dt.delaunay_violation_report(None).unwrap(); assert!( - violations.is_empty(), - "find_delaunay_violations should report no violating simplices for a tetrahedron" + violations.is_valid(), + "owner-level Delaunay report should have no violating simplices for a tetrahedron" ); // Smoke test for the debug helper: it should not panic and should print a // summary indicating that no violations were found. - #[cfg(any(test, feature = "diagnostics"))] - debug_print_first_delaunay_violation(tds, None); + #[cfg(feature = "diagnostics")] + dt.debug_print_first_delaunay_violation(None); } fn init_tracing() { @@ -1037,12 +1009,12 @@ mod tests { assert!(message.contains("Non-finite value")); } + #[cfg(feature = "diagnostics")] #[test] fn debug_print_first_delaunay_violation_handles_violations() { init_tracing(); let (tds, _, _) = build_non_delaunay_quad_2d(); - #[cfg(any(test, feature = "diagnostics"))] debug_print_first_delaunay_violation(&tds, None); } @@ -1057,7 +1029,7 @@ mod tests { ]; let dt = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let report = delaunay_violation_report(dt.tds(), None).unwrap(); + let report = dt.delaunay_violation_report(None).unwrap(); assert!(report.is_valid()); assert_eq!(report.number_of_vertices, 4); diff --git a/src/delaunay/query.rs b/src/delaunay/query.rs index 8480b27d..d4aa687f 100644 --- a/src/delaunay/query.rs +++ b/src/delaunay/query.rs @@ -11,21 +11,36 @@ use crate::core::adjacency::{ EdgeIndex, IncidenceView, SimplexNeighborIndex, TopologyIndexBuildError, TriangulationAdjacency, }; -use crate::core::collections::SimplexSecondaryMap; -use crate::core::edge::{EdgeKey, EdgeKeyError}; -use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter, FacetHandle}; +use crate::core::algorithms::flips::{FlipError, RidgeHandle}; +use crate::core::algorithms::locate::{ConflictError, LocateError, LocateResult, LocateStats}; +use crate::core::collections::{SimplexKeyBuffer, SimplexSecondaryMap, SmallBuffer, Uuid}; +use crate::core::edge::{EdgeKey, EdgeKeyError, EdgeView}; +use crate::core::facet::{ + AllFacetsIter, BoundaryFacetsIter, FacetError, FacetHandle, FacetToSimplicesIndex, FacetView, + SimplexFacetsIter, +}; use crate::core::query::QueryError; use crate::core::simplex::Simplex; -use crate::core::tds::{InvariantError, SimplexKey, Tds, TdsError, TdsMutationError, VertexKey}; +use crate::core::tds::{ + InvariantError, InvariantViolation, SimplexKey, Tds, TdsError, TdsMutationError, + TriangulationValidationReport, VertexKey, +}; use crate::core::traits::data_type::DataCopy; use crate::core::triangulation::Triangulation; use crate::core::validation::{TopologyGuarantee, ValidationConfigurationError, ValidationPolicy}; use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::{CoordinateConversionError, CoordinateValidationError}; use crate::geometry::util::safe_usize_to_scalar; use crate::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; -use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; +use crate::topology::characteristics::{ + euler::{FVector, TopologyClassification}, + validation::TopologyCheckResult, +}; +use crate::topology::manifold::ManifoldError; +use crate::topology::ridge::{RidgeCandidate, RidgeQuery, RidgeView}; +use crate::topology::traits::topological_space::{GlobalTopology, TopologyError, TopologyKind}; use crate::topology::traits::{ GlobalTopologyModelError, global_topology_model::GlobalTopologyModel, }; @@ -167,16 +182,9 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, @@ -201,16 +209,9 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, @@ -230,498 +231,972 @@ impl DelaunayTriangulation { self.tri.number_of_simplices() } - /// Returns the dimension of the triangulation. + /// Returns simplex keys paired with their stable UUIDs. /// - /// Returns the dimension `D` as an `i32`. + /// This is a zero-allocation identity iterator for diagnostics, snapshots, + /// and downstream bookkeeping. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 0.0, 1.0]?, + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert_eq!(dt.dim(), 4); + /// let Some((simplex_key, simplex_uuid)) = dt.simplex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(dt.simplex_uuid_from_key(simplex_key), Some(simplex_uuid)); + /// assert_eq!(dt.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn dim(&self) -> i32 { - self.tri.dim() + pub fn simplex_uuids(&self) -> impl Iterator + '_ { + self.tri.simplex_uuids() } - /// Returns an iterator over all simplices in the triangulation. - /// - /// This method provides access to the simplices stored in the underlying - /// triangulation data structure. The iterator yields `(SimplexKey, &Simplex)` - /// pairs for each simplex in the triangulation. - /// - /// # Returns - /// - /// An iterator over `(SimplexKey, &Simplex)` pairs. + /// Returns a simplex key for a stable simplex UUID. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::prelude::query::*; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use uuid::Uuid; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, simplex_uuid)) = dt.simplex_uuids().next() else { + /// return Ok(()); + /// }; /// - /// for (simplex_key, simplex) in dt.simplices() { - /// println!("Simplex {:?} has {} vertices", simplex_key, simplex.number_of_vertices()); - /// } + /// assert_eq!(dt.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + /// assert_eq!(dt.simplex_key_from_uuid(&Uuid::nil()), None); /// # Ok(()) /// # } /// ``` - pub fn simplices(&self) -> impl Iterator)> { - self.tri.tds.simplices() + #[must_use] + pub fn simplex_key_from_uuid(&self, simplex_uuid: &Uuid) -> Option { + self.tri.simplex_key_from_uuid(simplex_uuid) } - /// Computes a topology-aware barycenter of a live simplex. + /// Returns the stable UUID for a live simplex key. /// - /// For ordinary Euclidean simplices, this is the arithmetic average of the - /// simplex vertex coordinates. For periodic image-point triangulations, the - /// method first lifts each vertex into the simplex-local covering-space - /// frame using the simplex's stored periodic offsets, averages those lifted - /// coordinates, and then canonicalizes the result back into the topology's - /// coordinate domain. For a valid nondegenerate simplex, the lifted - /// barycenter lies in the simplex-local interior; the returned [`Point`] is - /// the canonical coordinate representative to use with - /// [`PachnerMove::K1Insert`](crate::pachner::PachnerMove::K1Insert). + /// # Examples /// - /// # Errors + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::SimplexKey; /// - /// Returns: + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, simplex_uuid)) = dt.simplex_uuids().next() else { + /// return Ok(()); + /// }; /// - /// - [`SimplexBarycenterError::MissingSimplex`] when `simplex_key` is stale. - /// - [`SimplexBarycenterError::InvalidSimplexArity`] when the live simplex - /// does not contain exactly `D + 1` vertices. - /// - [`SimplexBarycenterError::MissingVertex`] when the simplex references a - /// stale vertex key. - /// - [`SimplexBarycenterError::PeriodicOffsetCountMismatch`] when stored - /// periodic offsets are not aligned with simplex vertices. - /// - [`SimplexBarycenterError::DivisorConversion`] when the vertex count - /// cannot be converted to the coordinate scalar. - /// - [`SimplexBarycenterError::VertexLift`] or - /// [`SimplexBarycenterError::BarycenterCanonicalization`] when topology - /// model lifting or wrapping fails. - /// - [`SimplexBarycenterError::PointValidation`] when the averaged - /// coordinates fail [`Point`] validation. + /// assert_eq!(dt.simplex_uuid_from_key(simplex_key), Some(simplex_uuid)); + /// assert_eq!(dt.simplex_uuid_from_key(SimplexKey::default()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn simplex_uuid_from_key(&self, simplex_key: SimplexKey) -> Option { + self.tri.simplex_uuid_from_key(simplex_key) + } + + /// Returns vertex keys paired with their stable UUIDs. + /// + /// This is a zero-allocation identity iterator for diagnostics, snapshots, + /// and downstream bookkeeping. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, + /// DelaunayResult, DelaunayTriangulationBuilder, /// }; - /// use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; /// /// # fn main() -> DelaunayResult<()> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices) - /// .topology_guarantee(TopologyGuarantee::PLManifold) - /// .build()?; - /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, vertex_uuid)) = dt.vertex_uuids().next() else { /// return Ok(()); /// }; /// - /// let barycenter = dt.simplex_barycenter(simplex_key)?; - /// let result = dt - /// .propose_pachner(PachnerMove::K1Insert { - /// simplex_key, - /// vertex: delaunay::vertex!(*barycenter.coords())?, - /// })? - /// .attempt_on(&mut dt)?; - /// assert_eq!(result.inserted_face_vertices.len(), 1); + /// assert_eq!(dt.vertex_uuid_from_key(vertex_key), Some(vertex_uuid)); + /// assert_eq!(dt.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); /// # Ok(()) /// # } /// ``` - pub fn simplex_barycenter( - &self, - simplex_key: SimplexKey, - ) -> Result, SimplexBarycenterError> { - let simplex = self - .tri - .tds - .simplex(simplex_key) - .ok_or(SimplexBarycenterError::MissingSimplex { simplex_key })?; - - let vertex_count = simplex.number_of_vertices(); - let expected = D + 1; - if vertex_count != expected { - return Err(SimplexBarycenterError::InvalidSimplexArity { - simplex_key, - expected, - actual: vertex_count, - }); - } - - let model = self.global_topology().model(); - let periodic_offsets = if model.supports_periodic_orientation_offsets() { - simplex.periodic_vertex_offsets() - } else { - None - }; - if let Some(offsets) = periodic_offsets - && offsets.len() != vertex_count - { - return Err(SimplexBarycenterError::PeriodicOffsetCountMismatch { - simplex_key, - offset_count: offsets.len(), - vertex_count, - }); - } - - let divisor = safe_usize_to_scalar(vertex_count).map_err(|source| { - SimplexBarycenterError::DivisorConversion { - simplex_key, - vertex_count, - source, - } - })?; - let mut barycenter = [0.0_f64; D]; - for (vertex_index, &vertex_key) in simplex.vertices().iter().enumerate() { - let vertex = - self.tri - .tds - .vertex(vertex_key) - .ok_or(SimplexBarycenterError::MissingVertex { - simplex_key, - vertex_key, - })?; - let periodic_offset = periodic_offsets.map(|offsets| offsets[vertex_index]); - let lifted = model - .lift_for_orientation(*vertex.point().coords(), periodic_offset) - .map_err(|source| SimplexBarycenterError::VertexLift { - simplex_key, - vertex_key, - source, - })?; - for axis in 0..D { - barycenter[axis] += lifted[axis] / divisor; - } - } - - model - .canonicalize_point_in_place(&mut barycenter) - .map_err( - |source| SimplexBarycenterError::BarycenterCanonicalization { - simplex_key, - source, - }, - )?; - Point::try_new(barycenter).map_err(|source| SimplexBarycenterError::PointValidation { - simplex_key, - source, - }) + pub fn vertex_uuids(&self) -> impl Iterator + '_ { + self.tri.vertex_uuids() } - /// Returns an iterator over all vertices in the triangulation. + /// Returns a vertex key for a stable vertex UUID. /// - /// This method provides access to the vertices stored in the underlying - /// triangulation data structure. The iterator yields `(VertexKey, &Vertex)` - /// pairs for each vertex in the triangulation. + /// # Examples /// - /// # Returns + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use uuid::Uuid; /// - /// An iterator over `(VertexKey, &Vertex)` pairs. + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, vertex_uuid)) = dt.vertex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(dt.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + /// assert_eq!(dt.vertex_key_from_uuid(&Uuid::nil()), None); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn vertex_key_from_uuid(&self, vertex_uuid: &Uuid) -> Option { + self.tri.vertex_key_from_uuid(vertex_uuid) + } + + /// Returns the stable UUID for a live vertex key. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::prelude::query::*; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::VertexKey; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, vertex_uuid)) = dt.vertex_uuids().next() else { + /// return Ok(()); + /// }; + /// + /// assert_eq!(dt.vertex_uuid_from_key(vertex_key), Some(vertex_uuid)); + /// assert_eq!(dt.vertex_uuid_from_key(VertexKey::default()), None); + /// # Ok(()) /// # } - /// # fn main() -> Result<(), ExampleError> { + /// ``` + #[must_use] + pub fn vertex_uuid_from_key(&self, vertex_key: VertexKey) -> Option { + self.tri.vertex_uuid_from_key(vertex_key) + } + + /// Returns the dimension of the triangulation. + /// + /// Returns the dimension `D` as an `i32`. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 0.0, 1.0]?, /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// - /// for (vertex_key, vertex) in dt.vertices() { - /// println!("Vertex {:?} at {:?}", vertex_key, vertex.point()); - /// } + /// assert_eq!(dt.dim(), 4); /// # Ok(()) /// # } /// ``` - pub fn vertices(&self) -> impl Iterator)> { - self.tri.vertices() + #[must_use] + pub fn dim(&self) -> i32 { + self.tri.dim() } - /// Sets the auxiliary data on a vertex, returning the previous value. + /// Returns the topology generation counter for this triangulation. + #[must_use] + pub fn topology_generation(&self) -> u64 { + self.tri.topology_generation() + } + + /// Returns whether adjacent simplices have coherent opposite facet orientations. + #[must_use] + pub fn is_coherently_oriented(&self) -> bool { + self.tri.is_coherently_oriented() + } + + /// Fast-fail Level 2 structural validation. /// - /// This is a safe O(1) operation that modifies only the user-data field. - /// It does not affect geometry, topology, or Delaunay invariants, so - /// no caches are invalidated. + /// This checks the underlying combinatorial structure without exposing the + /// storage owner. Use [`DelaunayTriangulation::validate`](Self::validate) + /// for cumulative Levels 1-5 validation. /// - /// # Returns + /// # Errors /// - /// The old `Option` value when the key exists. + /// Returns the first [`TdsError`] encountered by structural validation. + pub fn is_valid_structure(&self) -> Result<(), TdsError> { + self.tri.is_valid_structure() + } + + /// Cumulative Levels 1-2 validation for the triangulation structure. /// /// # Errors /// - /// Returns [`TdsMutationError`] if `key` does not identify a vertex in the - /// underlying TDS. + /// Returns the first [`TdsError`] encountered by element or structural + /// validation. + pub fn validate_structure(&self) -> Result<(), TdsError> { + self.tri.validate_structure() + } + + /// Returns the first actionable Level 2 structural diagnostic, if any. + #[must_use] + pub fn structure_diagnostic(&self) -> Option { + self.tri.structure_diagnostic() + } + + /// Runs Level 2 structure checks and returns all checkable failures. /// - /// # Examples + /// # Errors /// - /// ``` - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, - /// }; + /// Returns a [`TriangulationValidationReport`] containing all invariant + /// violations if any structural validation step fails. + pub fn structure_report(&self) -> Result<(), TriangulationValidationReport> { + self.tri.structure_report() + } + + /// Counts simplices by dimension for this triangulation. /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # #[error(transparent)] - /// # TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices: [Vertex; 3] = [ - /// delaunay::vertex![0.0, 0.0; data = 10i32]?, - /// delaunay::vertex![1.0, 0.0; data = 20]?, - /// delaunay::vertex![0.0, 1.0; data = 30]?, - /// ]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some((key, _)) = dt.vertices().next() else { - /// return Ok(()); - /// }; + /// # Errors /// - /// let prev = dt.set_vertex_data(key, Some(99))?; - /// assert!(prev.is_some()); + /// Returns [`TopologyError`] if counts cannot be computed from the current topology. + pub fn simplex_counts(&self) -> Result { + self.tri.simplex_counts() + } + + /// Counts simplices on the topology-approved boundary only. /// - /// // Clear data - /// let prev = dt.set_vertex_data(key, None)?; - /// assert_eq!(prev, Some(99)); - /// assert_eq!(dt.tds().vertex(key).map(|v| v.data()), Some(None)); - /// # Ok(()) - /// # } - /// ``` - #[inline] - pub fn set_vertex_data( - &mut self, - key: VertexKey, - data: Option, - ) -> Result, TdsMutationError> { - self.tri.tds.set_vertex_data(key, data) + /// # Errors + /// + /// Returns [`TopologyError`] if boundary facets cannot be classified or enumerated. + pub fn boundary_simplex_counts(&self) -> Result { + self.tri.boundary_simplex_counts() } - /// Sets the auxiliary data on a simplex, returning the previous value. + /// Classifies this triangulation for Euler-characteristic checks. /// - /// This is a safe O(1) operation that modifies only the user-data field. - /// It does not affect geometry, topology, or Delaunay invariants, so - /// no caches are invalidated. + /// # Errors /// - /// # Returns + /// Returns [`TopologyError`] if facet incidence cannot be classified for the provided topology. + pub fn topology_classification_for( + &self, + global_topology: GlobalTopology, + ) -> Result { + self.tri.topology_classification_for(global_topology) + } + + /// Validates this triangulation's Euler characteristic against its topology metadata. /// - /// The old `Option` value when the key exists. + /// # Errors + /// + /// Returns [`TopologyError`] if simplex counts or boundary classification cannot be computed. + pub fn euler_check(&self) -> Result { + self.tri.euler_check() + } + + /// Validates this triangulation's Euler characteristic against explicit topology metadata. /// /// # Errors /// - /// Returns [`TdsMutationError`] if `key` does not identify a simplex in - /// the underlying TDS. + /// Returns [`TopologyError`] if simplex counts or boundary classification cannot be computed. + pub fn euler_check_for_topology( + &self, + global_topology: GlobalTopology, + ) -> Result { + self.tri.euler_check_for_topology(global_topology) + } + + /// Validates all ridge links for the Level 3 PL-manifold codimension-2 condition. /// - /// # Examples + /// Ridge-link validation checks that every `(D-2)`-simplex has a + /// 1-dimensional link that is a connected path for boundary ridges or a + /// connected cycle for interior ridges. It is a useful local diagnostic, but + /// in dimensions `D >= 3` it is not by itself a complete PL-manifold + /// certificate. /// - /// ``` - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// # Errors /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # #[error(transparent)] - /// # TdsMutation(#[from] delaunay::prelude::tds::TdsMutationError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = [ - /// delaunay::vertex![0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0]?, - /// ]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).simplex_data_type::().build()?; - /// let Some((key, _)) = dt.simplices().next() else { - /// return Ok(()); + /// Returns [`ManifoldError::Tds`] if storage is inconsistent while resolving + /// ridge stars. Returns [`ManifoldError::RidgeLinkNotManifold`] when a + /// ridge link is not a connected path or cycle. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, vertex, /// }; /// - /// let prev = dt.set_simplex_data(key, Some(42))?; - /// assert_eq!(prev, None); + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// // Clear data - /// let prev = dt.set_simplex_data(key, None)?; - /// assert_eq!(prev, Some(42)); - /// assert_eq!(dt.tds().simplex(key).map(|s| s.data()), Some(None)); + /// dt.validate_ridge_links()?; /// # Ok(()) /// # } /// ``` - #[inline] - pub fn set_simplex_data( - &mut self, - key: SimplexKey, - data: Option, - ) -> Result, TdsMutationError> { - self.tri.tds.set_simplex_data(key, data) + pub fn validate_ridge_links(&self) -> Result<(), ManifoldError> { + self.tri.validate_ridge_links() } - /// Fills every existing simplex with data computed from a borrowed simplex view. + /// Validates ridge links incident to a selected set of simplices. /// - /// This is the ergonomic follow-on step for triangulations whose simplex - /// payload type was selected at construction time. It does not change - /// geometry, topology, runtime keys, UUIDs, or validation generation. + /// This localized Level 3 diagnostic checks only ridges touching the + /// supplied simplex keys. Missing simplex keys are ignored because they may + /// have been removed by a preceding local topology edit. + /// + /// # Errors + /// + /// Returns [`ManifoldError::Tds`] if storage is inconsistent while resolving + /// ridge stars. Returns [`ManifoldError::RidgeLinkNotManifold`] when a + /// checked ridge link is not a connected path or cycle. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayResult, DelaunayTriangulationBuilder, vertex, /// }; /// /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0]?, + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let touched = dt.simplices().map(|(key, _)| key); /// - /// let mut dt: DelaunayTriangulation<_, _, usize, 2> = - /// DelaunayTriangulationBuilder::new(&vertices) - /// .simplex_data_type::() - /// .build()?; - /// dt.fill_simplex_data(|_, simplex| simplex.number_of_vertices()); - /// - /// for (_, simplex) in dt.simplices() { - /// assert_eq!(simplex.data(), Some(&3)); - /// } + /// dt.validate_ridge_links_for_simplices(touched)?; /// # Ok(()) /// # } /// ``` - pub fn fill_simplex_data(&mut self, data_for: F) - where - F: FnMut(SimplexKey, &Simplex) -> V, - { - self.tri.tds.fill_simplex_data(data_for); + pub fn validate_ridge_links_for_simplices( + &self, + simplices: impl IntoIterator, + ) -> Result<(), ManifoldError> { + self.tri.validate_ridge_links_for_simplices(simplices) } - /// Fills every existing simplex from a complete secondary map. + /// Validates all vertex links for the canonical Level 3 PL-manifold condition. /// - /// The method preflights the entire map before mutating canonical simplex - /// storage. It returns [`SimplexDataFillError::StaleSimplexData`] if the - /// map contains a key that does not belong to this triangulation, and - /// [`SimplexDataFillError::MissingSimplexData`] if any live simplex has no - /// entry. On either preflight error, no simplex payload is changed. + /// This check verifies that every vertex link is a `(D-1)`-sphere for + /// interior vertices or a `(D-1)`-ball for boundary vertices, using this + /// triangulation's global-topology metadata to distinguish true boundary + /// facets from periodic identifications. /// /// # Errors /// - /// Returns [`SimplexDataFillError`] when the secondary map is stale, - /// incomplete, or when the final TDS payload write fails. - /// - /// Because values are copied out of a borrowed secondary map, this method - /// requires [`DataCopy`]. Use [`Self::fill_simplex_data`] when payloads can - /// be computed by value for each simplex. + /// Returns [`ManifoldError::Tds`] if facet incidence cannot be built from + /// current storage. Returns [`ManifoldError::VertexLinkNotManifold`] when a + /// vertex link fails the expected sphere/ball manifold condition. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::collections::SimplexSecondaryMap; /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayResult, DelaunayTriangulationBuilder, vertex, /// }; /// /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0]?, + /// vertex![0.0, 0.0, 0.0]?, + /// vertex![1.0, 0.0, 0.0]?, + /// vertex![0.0, 1.0, 0.0]?, + /// vertex![0.0, 0.0, 1.0]?, /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let mut dt: DelaunayTriangulation<_, _, usize, 2> = - /// DelaunayTriangulationBuilder::new(&vertices) - /// .simplex_data_type::() - /// .build()?; - /// let mut data = SimplexSecondaryMap::new(); - /// for (simplex_key, simplex) in dt.simplices() { - /// data.insert(simplex_key, simplex.number_of_vertices()); - /// } - /// - /// dt.try_fill_simplex_data_from(&data)?; - /// for (_, simplex) in dt.simplices() { - /// assert_eq!(simplex.data(), Some(&3)); - /// } + /// dt.validate_vertex_links()?; /// # Ok(()) /// # } /// ``` - pub fn try_fill_simplex_data_from( - &mut self, - data: &SimplexSecondaryMap, - ) -> Result<(), SimplexDataFillError> - where - V: DataCopy, - { - for (simplex_key, _) in data { - if !self.tri.tds.contains_simplex(simplex_key) { - return Err(SimplexDataFillError::StaleSimplexData { simplex_key }); - } - } + pub fn validate_vertex_links(&self) -> Result<(), ManifoldError> { + self.tri.validate_vertex_links() + } - let assignments = self - .tri - .tds - .simplex_keys() - .map(|simplex_key| { - data.get(simplex_key) - .copied() + /// Returns an iterator over all simplices in the triangulation. + /// + /// This method provides access to the simplices stored in the underlying + /// triangulation data structure. The iterator yields `(SimplexKey, &Simplex)` + /// pairs for each simplex in the triangulation. + /// + /// # Returns + /// + /// An iterator over `(SimplexKey, &Simplex)` pairs. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::query::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// for (simplex_key, simplex) in dt.simplices() { + /// println!("Simplex {:?} has {} vertices", simplex_key, simplex.number_of_vertices()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn simplices(&self) -> impl Iterator)> { + self.tri.tds.simplices() + } + + /// Returns a read-only simplex view by key. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::SimplexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(dt.simplex(simplex_key).is_some()); + /// assert!(dt.simplex(SimplexKey::default()).is_none()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn simplex(&self, key: SimplexKey) -> Option<&Simplex> { + self.tri.simplex(key) + } + + /// Returns `true` when `key` identifies a live simplex in this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::SimplexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(dt.contains_simplex(simplex_key)); + /// assert!(!dt.contains_simplex(SimplexKey::default())); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn contains_simplex(&self, key: SimplexKey) -> bool { + self.tri.contains_simplex(key) + } + + /// Computes a topology-aware barycenter of a live simplex. + /// + /// For ordinary Euclidean simplices, this is the arithmetic average of the + /// simplex vertex coordinates. For periodic image-point triangulations, the + /// method first lifts each vertex into the simplex-local covering-space + /// frame using the simplex's stored periodic offsets, averages those lifted + /// coordinates, and then canonicalizes the result back into the topology's + /// coordinate domain. For a valid nondegenerate simplex, the lifted + /// barycenter lies in the simplex-local interior; the returned [`Point`] is + /// the canonical coordinate representative to use with + /// [`PachnerMove::K1Insert`](crate::pachner::PachnerMove::K1Insert). + /// + /// # Errors + /// + /// Returns: + /// + /// - [`SimplexBarycenterError::MissingSimplex`] when `simplex_key` is stale. + /// - [`SimplexBarycenterError::InvalidSimplexArity`] when the live simplex + /// does not contain exactly `D + 1` vertices. + /// - [`SimplexBarycenterError::MissingVertex`] when the simplex references a + /// stale vertex key. + /// - [`SimplexBarycenterError::PeriodicOffsetCountMismatch`] when stored + /// periodic offsets are not aligned with simplex vertices. + /// - [`SimplexBarycenterError::DivisorConversion`] when the vertex count + /// cannot be converted to the coordinate scalar. + /// - [`SimplexBarycenterError::VertexLift`] or + /// [`SimplexBarycenterError::BarycenterCanonicalization`] when topology + /// model lifting or wrapping fails. + /// - [`SimplexBarycenterError::PointValidation`] when the averaged + /// coordinates fail [`Point`] validation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, + /// }; + /// use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices) + /// .topology_guarantee(TopologyGuarantee::PLManifold) + /// .build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let barycenter = dt.simplex_barycenter(simplex_key)?; + /// let result = dt + /// .propose_pachner(PachnerMove::K1Insert { + /// simplex_key, + /// vertex: delaunay::vertex!(*barycenter.coords())?, + /// })? + /// .attempt_on(&mut dt)?; + /// assert_eq!(result.inserted_face_vertices.len(), 1); + /// # Ok(()) + /// # } + /// ``` + pub fn simplex_barycenter( + &self, + simplex_key: SimplexKey, + ) -> Result, SimplexBarycenterError> { + let simplex = self + .tri + .tds + .simplex(simplex_key) + .ok_or(SimplexBarycenterError::MissingSimplex { simplex_key })?; + + let vertex_count = simplex.number_of_vertices(); + let expected = D + 1; + if vertex_count != expected { + return Err(SimplexBarycenterError::InvalidSimplexArity { + simplex_key, + expected, + actual: vertex_count, + }); + } + + let model = self.global_topology().model(); + let periodic_offsets = if model.supports_periodic_orientation_offsets() { + simplex.periodic_vertex_offsets() + } else { + None + }; + if let Some(offsets) = periodic_offsets + && offsets.len() != vertex_count + { + return Err(SimplexBarycenterError::PeriodicOffsetCountMismatch { + simplex_key, + offset_count: offsets.len(), + vertex_count, + }); + } + + let divisor = safe_usize_to_scalar(vertex_count).map_err(|source| { + SimplexBarycenterError::DivisorConversion { + simplex_key, + vertex_count, + source, + } + })?; + let mut barycenter = [0.0_f64; D]; + for (vertex_index, &vertex_key) in simplex.vertices().iter().enumerate() { + let vertex = + self.tri + .tds + .vertex(vertex_key) + .ok_or(SimplexBarycenterError::MissingVertex { + simplex_key, + vertex_key, + })?; + let periodic_offset = periodic_offsets.map(|offsets| offsets[vertex_index]); + let lifted = model + .lift_for_orientation(*vertex.point().coords(), periodic_offset) + .map_err(|source| SimplexBarycenterError::VertexLift { + simplex_key, + vertex_key, + source, + })?; + for axis in 0..D { + barycenter[axis] += lifted[axis] / divisor; + } + } + + model + .canonicalize_point_in_place(&mut barycenter) + .map_err( + |source| SimplexBarycenterError::BarycenterCanonicalization { + simplex_key, + source, + }, + )?; + Point::try_new(barycenter).map_err(|source| SimplexBarycenterError::PointValidation { + simplex_key, + source, + }) + } + + /// Returns an iterator over all vertices in the triangulation. + /// + /// This method provides access to the vertices stored in the underlying + /// triangulation data structure. The iterator yields `(VertexKey, &Vertex)` + /// pairs for each vertex in the triangulation. + /// + /// # Returns + /// + /// An iterator over `(VertexKey, &Vertex)` pairs. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::query::*; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// for (vertex_key, vertex) in dt.vertices() { + /// println!("Vertex {:?} at {:?}", vertex_key, vertex.point()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn vertices(&self) -> impl Iterator)> { + self.tri.vertices() + } + + /// Returns a read-only vertex view by key. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::VertexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, _)) = dt.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(dt.vertex(vertex_key).is_some()); + /// assert!(dt.vertex(VertexKey::default()).is_none()); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn vertex(&self, key: VertexKey) -> Option<&Vertex> { + self.tri.vertex(key) + } + + /// Returns `true` when `key` identifies a live vertex in this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// use delaunay::prelude::tds::VertexKey; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, _)) = dt.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// assert!(dt.contains_vertex_key(vertex_key)); + /// assert!(!dt.contains_vertex_key(VertexKey::default())); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn contains_vertex_key(&self, key: VertexKey) -> bool { + self.tri.contains_vertex_key(key) + } + + /// Sets the auxiliary data on a vertex, returning the previous value. + /// + /// This is a safe O(1) operation that modifies only the user-data field. + /// It does not affect geometry, topology, or Delaunay invariants, so + /// no caches are invalidated. + /// + /// # Returns + /// + /// The old `Option` value when the key exists. + /// + /// # Errors + /// + /// Returns [`TdsMutationError`] if `key` does not identify a vertex in the + /// underlying TDS. + /// + /// # Examples + /// + /// ``` + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, Vertex, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices: [Vertex; 3] = [ + /// delaunay::vertex![0.0, 0.0; data = 10i32]?, + /// delaunay::vertex![1.0, 0.0; data = 20]?, + /// delaunay::vertex![0.0, 1.0; data = 30]?, + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((key, _)) = dt.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// let prev = dt.set_vertex_data(key, Some(99))?; + /// assert!(prev.is_some()); + /// + /// // Clear data + /// let prev = dt.set_vertex_data(key, None)?; + /// assert_eq!(prev, Some(99)); + /// assert_eq!(dt.vertex(key).map(|v| v.data()), Some(None)); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn set_vertex_data( + &mut self, + key: VertexKey, + data: Option, + ) -> Result, TdsMutationError> { + self.tri.tds.set_vertex_data(key, data) + } + + /// Sets the auxiliary data on a simplex, returning the previous value. + /// + /// This is a safe O(1) operation that modifies only the user-data field. + /// It does not affect geometry, topology, or Delaunay invariants, so + /// no caches are invalidated. + /// + /// # Returns + /// + /// The old `Option` value when the key exists. + /// + /// # Errors + /// + /// Returns [`TdsMutationError`] if `key` does not identify a simplex in + /// the underlying TDS. + /// + /// # Examples + /// + /// ``` + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).simplex_data_type::().build()?; + /// let Some((key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let prev = dt.set_simplex_data(key, Some(42))?; + /// assert_eq!(prev, None); + /// + /// // Clear data + /// let prev = dt.set_simplex_data(key, None)?; + /// assert_eq!(prev, Some(42)); + /// assert_eq!(dt.simplex(key).map(|s| s.data()), Some(None)); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn set_simplex_data( + &mut self, + key: SimplexKey, + data: Option, + ) -> Result, TdsMutationError> { + self.tri.tds.set_simplex_data(key, data) + } + + /// Fills every existing simplex with data computed from a borrowed simplex view. + /// + /// This is the ergonomic follow-on step for triangulations whose simplex + /// payload type was selected at construction time. It does not change + /// geometry, topology, runtime keys, UUIDs, or validation generation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// + /// let mut dt: DelaunayTriangulation<_, _, usize, 2> = + /// DelaunayTriangulationBuilder::new(&vertices) + /// .simplex_data_type::() + /// .build()?; + /// dt.fill_simplex_data(|_, simplex| simplex.number_of_vertices()); + /// + /// for (_, simplex) in dt.simplices() { + /// assert_eq!(simplex.data(), Some(&3)); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn fill_simplex_data(&mut self, data_for: F) + where + F: FnMut(SimplexKey, &Simplex) -> V, + { + self.tri.tds.fill_simplex_data(data_for); + } + + /// Fills every existing simplex from a complete secondary map. + /// + /// The method preflights the entire map before mutating canonical simplex + /// storage. It returns [`SimplexDataFillError::StaleSimplexData`] if the + /// map contains a key that does not belong to this triangulation, and + /// [`SimplexDataFillError::MissingSimplexData`] if any live simplex has no + /// entry. On either preflight error, no simplex payload is changed. + /// + /// # Errors + /// + /// Returns [`SimplexDataFillError`] when the secondary map is stale, + /// incomplete, or when the final TDS payload write fails. + /// + /// Because values are copied out of a borrowed secondary map, this method + /// requires [`DataCopy`]. Use [`Self::fill_simplex_data`] when payloads can + /// be computed by value for each simplex. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::collections::SimplexSecondaryMap; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// + /// let mut dt: DelaunayTriangulation<_, _, usize, 2> = + /// DelaunayTriangulationBuilder::new(&vertices) + /// .simplex_data_type::() + /// .build()?; + /// let mut data = SimplexSecondaryMap::new(); + /// for (simplex_key, simplex) in dt.simplices() { + /// data.insert(simplex_key, simplex.number_of_vertices()); + /// } + /// + /// dt.try_fill_simplex_data_from(&data)?; + /// for (_, simplex) in dt.simplices() { + /// assert_eq!(simplex.data(), Some(&3)); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn try_fill_simplex_data_from( + &mut self, + data: &SimplexSecondaryMap, + ) -> Result<(), SimplexDataFillError> + where + V: DataCopy, + { + for (simplex_key, _) in data { + if !self.tri.tds.contains_simplex(simplex_key) { + return Err(SimplexDataFillError::StaleSimplexData { simplex_key }); + } + } + + let assignments = self + .tri + .tds + .simplex_keys() + .map(|simplex_key| { + data.get(simplex_key) + .copied() .map(|simplex_data| (simplex_key, simplex_data)) .ok_or(SimplexDataFillError::MissingSimplexData { simplex_key }) }) @@ -739,667 +1214,961 @@ impl DelaunayTriangulation { Ok(()) } - /// Returns a reference to the underlying triangulation data structure. + #[must_use] + pub(crate) const fn tds(&self) -> &Tds { + &self.tri.tds + } + + /// Returns the kernel backing crate-internal repair and rebuild workflows. + /// + /// This keeps internal callers from reaching through + /// [`DelaunayTriangulation::as_triangulation`] just to access storage + /// details on the generic triangulation owner. + #[must_use] + pub(crate) const fn kernel(&self) -> &K { + &self.tri.kernel + } + + pub(crate) const fn invalidate_locate_hint_cache(&mut self) { + self.insertion_state.last_inserted_simplex = None; + } + + pub(crate) fn invalidate_repair_caches(&mut self) { + self.invalidate_locate_hint_cache(); + self.spatial_index = None; + } + + /// Returns mutable TDS access for crate-internal repair algorithms. + /// + /// Repair passes may rewrite topology and invalidate locate hints, so this + /// deliberately clears the ephemeral caches before handing out the borrow. + pub(crate) fn tds_mut_for_repair(&mut self) -> &mut Tds { + self.invalidate_repair_caches(); + &mut self.tri.tds + } + + /// Returns a reference to the underlying `Triangulation` (kernel + tds). + /// + /// This is useful when you need to pass the triangulation to methods that + /// expect a `&Triangulation`, such as + /// [`ConvexHull::try_from_triangulation`](crate::geometry::algorithms::convex_hull::ConvexHull::try_from_triangulation). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::query::ConvexHull; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// let vertices: Vec<_> = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let hull = ConvexHull::try_from_triangulation(dt.as_triangulation())?; + /// assert_eq!(hull.number_of_facets(), 4); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub const fn as_triangulation(&self) -> &Triangulation { + &self.tri + } + + /// Consumes this Delaunay owner and returns the underlying triangulation. + /// + /// Use this when a workflow intentionally leaves the Delaunay-specific + /// owner and continues with generic topology editing. This moves the + /// canonical triangulation instead of cloning a topology snapshot. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let tri = dt.into_triangulation(); + /// assert_eq!(tri.number_of_vertices(), 3); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn into_triangulation(self) -> Triangulation { + self.tri + } + + /// Returns an iterator over boundary (hull) facets in the triangulation. + /// + /// Boundary facets are one-sided facets not identified by closed periodic + /// topology. This method computes the facet-to-simplices index internally + /// for convenience. + /// + /// # Returns + /// + /// An iterator yielding `Result` items for boundary + /// facets only. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let boundary_count = dt + /// .boundary_facets()? + /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; + /// assert_eq!(boundary_count, 4); // All facets are on boundary + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`QueryError::TriangulationCorrupted`] if facet-incidence index + /// construction detects invalid simplex or facet bookkeeping. The variant + /// preserves the lower-level [`TdsError`] for diagnostics. Returns + /// [`QueryError::TopologyInvalid`] when topology-aware boundary + /// classification rejects the declared global topology or detects another + /// manifold-boundary inconsistency. + /// Individual iterator items return [`FacetError`] + /// if a boundary facet handle cannot be reborrowed as a view. + pub fn boundary_facets(&self) -> Result, QueryError> { + self.tri.boundary_facets() + } + + /// Returns the insertion-time global topology validation policy used by the underlying + /// triangulation. + /// + /// This policy controls when Level 3 (`Triangulation::is_valid_topology()`) is run automatically + /// during incremental insertion (as part of the topology safety net). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::validation::ValidationPolicy; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// assert_eq!(dt.validation_policy(), ValidationPolicy::ExplicitOnly); + /// # Ok(()) + /// # } + /// ``` + #[inline] + #[must_use] + pub const fn validation_policy(&self) -> ValidationPolicy { + self.tri.validation_policy + } + + /// Tries to set the insertion-time global topology validation policy used by the underlying + /// triangulation. + /// + /// This affects subsequent incremental insertions. (Construction-time behavior is determined + /// by the policy active during `new()` / `with_kernel()`.) + /// + /// # Errors + /// + /// Returns [`ValidationConfigurationError::IncompatibleTopologyAndValidationPolicy`] when the + /// requested policy is incompatible with the current topology guarantee. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::DelaunayTriangulation; + /// use delaunay::prelude::validation::{ + /// ValidationConfigurationError, ValidationPolicy, + /// }; + /// + /// # fn main() -> Result<(), ValidationConfigurationError> { + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + /// + /// dt.try_set_validation_policy(ValidationPolicy::Always)?; + /// assert_eq!( + /// dt.validation_policy(), + /// ValidationPolicy::Always + /// ); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn try_set_validation_policy( + &mut self, + policy: ValidationPolicy, + ) -> Result<(), ValidationConfigurationError> { + self.tri.try_set_validation_policy(policy) + } + + /// Sets the insertion-time global topology validation policy used by the underlying + /// triangulation. + /// + /// Prefer [`try_set_validation_policy`](Self::try_set_validation_policy) when callers need + /// typed feedback for rejected combinations. This compatibility setter leaves the existing + /// policy unchanged and emits a warning if the requested combination is incoherent. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::DelaunayTriangulation; + /// use delaunay::prelude::validation::ValidationPolicy; + /// + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + /// + /// dt.set_validation_policy(ValidationPolicy::Always); + /// assert_eq!(dt.validation_policy(), ValidationPolicy::Always); + /// ``` + #[inline] + pub fn set_validation_policy(&mut self, policy: ValidationPolicy) { + self.tri.set_validation_policy(policy); + } + + /// Tries to set the topology guarantee used for Level 3 topology validation. + /// + /// # Errors /// - /// This provides access to the purely combinatorial Tds layer for - /// advanced operations and performance testing. + /// Returns [`ValidationConfigurationError::IncompatibleTopologyAndValidationPolicy`] when the + /// requested guarantee cannot be represented with the current validation policy. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + /// use delaunay::prelude::construction::DelaunayTriangulation; + /// use delaunay::prelude::validation::{ + /// TopologyGuarantee, ValidationConfigurationError, /// }; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 0.0, 1.0]?, - /// ]; + /// # fn main() -> Result<(), ValidationConfigurationError> { + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// dt.try_set_topology_guarantee(TopologyGuarantee::Pseudomanifold)?; /// - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let tds = dt.tds(); - /// assert_eq!(tds.number_of_vertices(), 5); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); /// # Ok(()) /// # } /// ``` - #[must_use] - pub const fn tds(&self) -> &Tds { - &self.tri.tds + #[inline] + pub fn try_set_topology_guarantee( + &mut self, + guarantee: TopologyGuarantee, + ) -> Result<(), ValidationConfigurationError> { + self.tri.try_set_topology_guarantee(guarantee) } - /// Returns a mutable reference to the underlying triangulation data structure. + /// Returns the automatic Delaunay repair policy. /// - /// This provides mutable access to the purely combinatorial Tds layer for - /// advanced operations and testing of internal algorithms. + /// # Examples /// - /// # Safety + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::repair::DelaunayRepairPolicy; /// - /// Modifying the Tds directly can break Delaunay invariants. Use this only - /// when you know what you're doing (typically in tests or specialized algorithms). - #[cfg(test)] - pub(crate) fn tds_mut(&mut self) -> &mut Tds { - // Direct mutable access can invalidate performance caches. - self.invalidate_repair_caches(); - &mut self.tri.tds - } - - pub(crate) const fn invalidate_locate_hint_cache(&mut self) { - self.insertion_state.last_inserted_simplex = None; - } - - pub(crate) fn invalidate_repair_caches(&mut self) { - self.invalidate_locate_hint_cache(); - self.spatial_index = None; - } - - /// Returns mutable TDS access for crate-internal repair algorithms. + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// Repair passes may rewrite topology and invalidate locate hints, so this - /// deliberately clears the ephemeral caches before handing out the borrow. - pub(crate) fn tds_mut_for_repair(&mut self) -> &mut Tds { - self.invalidate_repair_caches(); - &mut self.tri.tds + /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::EveryInsertion); + /// # Ok(()) + /// # } + /// ``` + #[inline] + #[must_use] + pub const fn delaunay_repair_policy(&self) -> DelaunayRepairPolicy { + self.insertion_state.delaunay_repair_policy } - /// Returns a reference to the underlying `Triangulation` (kernel + tds). + /// Sets the automatic Delaunay repair policy. /// - /// This is useful when you need to pass the triangulation to methods that - /// expect a `&Triangulation`, such as - /// [`ConvexHull::try_from_triangulation`](crate::geometry::algorithms::convex_hull::ConvexHull::try_from_triangulation). + /// This affects future incremental insertions; it does not rewrite already + /// stored topology. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::prelude::query::ConvexHull; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::repair::DelaunayRepairPolicy; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Hull(#[from] delaunay::query::ConvexHullConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::Never); + /// # Ok(()) /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices: Vec<_> = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, - /// ]; + /// ``` + #[inline] + pub const fn set_delaunay_repair_policy(&mut self, policy: DelaunayRepairPolicy) { + self.insertion_state.delaunay_repair_policy = policy; + } + + /// Returns the automatic global Delaunay validation policy. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::repair::DelaunayCheckPolicy; /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let hull = ConvexHull::try_from_triangulation(dt.as_triangulation())?; - /// assert_eq!(hull.number_of_facets(), 4); + /// + /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EndOnly); /// # Ok(()) /// # } /// ``` + #[inline] #[must_use] - pub const fn as_triangulation(&self) -> &Triangulation { - &self.tri + pub const fn delaunay_check_policy(&self) -> DelaunayCheckPolicy { + self.insertion_state.delaunay_check_policy } - /// Returns an iterator over boundary (hull) facets in the triangulation. - /// - /// Boundary facets are one-sided facets not identified by closed periodic - /// topology. This method computes the facet-to-simplices index internally - /// for convenience. - /// - /// # Returns + /// Sets the automatic global Delaunay validation policy. /// - /// An iterator yielding `Result` items for boundary - /// facets only. + /// This affects future incremental insertions; it does not perform an + /// immediate global Delaunay check. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulationBuilder}; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::repair::DelaunayCheckPolicy; + /// use std::num::NonZeroUsize; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, - /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some(every_two) = NonZeroUsize::new(2) else { + /// return Ok(()); + /// }; /// - /// let boundary_count = dt - /// .boundary_facets()? - /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; - /// assert_eq!(boundary_count, 4); // All facets are on boundary + /// dt.set_delaunay_check_policy(DelaunayCheckPolicy::EveryN(every_two)); + /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EveryN(every_two)); /// # Ok(()) /// # } /// ``` + #[inline] + pub const fn set_delaunay_check_policy(&mut self, policy: DelaunayCheckPolicy) { + self.insertion_state.delaunay_check_policy = policy; + } +} + +// ============================================================================= +// CONFIGURATION & TRAVERSAL (Minimal Bounds, continued) +// ============================================================================= + +impl DelaunayTriangulation { + // ------------------------------------------------------------------------- + // CONFIGURATION + // ------------------------------------------------------------------------- + + /// Returns the topology guarantee used for Level 3 topology validation. /// - /// # Errors + /// # Examples /// - /// Returns [`QueryError::TriangulationCorrupted`] if facet-incidence index - /// construction detects invalid simplex or facet bookkeeping. The variant - /// preserves the lower-level [`TdsError`] for diagnostics. Returns - /// [`QueryError::TopologyInvalid`] when topology-aware boundary - /// classification rejects the declared global topology or detects another - /// manifold-boundary inconsistency. - /// Individual iterator items return [`FacetError`](crate::prelude::tds::FacetError) - /// if a boundary facet handle cannot be reborrowed as a view. - pub fn boundary_facets(&self) -> Result, QueryError> { - self.tri.boundary_facets() + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, TopologyGuarantee, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + /// # Ok(()) + /// # } + /// ``` + #[inline] + #[must_use] + pub const fn topology_guarantee(&self) -> TopologyGuarantee { + self.tri.topology_guarantee() } - /// Returns the insertion-time global topology validation policy used by the underlying - /// triangulation. - /// - /// This policy controls when Level 3 (`Triangulation::is_valid_topology()`) is run automatically - /// during incremental insertion (as part of the topology safety net). + /// Returns runtime global topology metadata associated with this triangulation. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + /// DelaunayResult, DelaunayTriangulationBuilder, GlobalTopology, /// }; - /// use delaunay::prelude::validation::ValidationPolicy; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// assert!(dt.global_topology().is_euclidean()); + /// # Ok(()) /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0]?, - /// ]; + /// ``` + #[inline] + #[must_use] + pub const fn global_topology(&self) -> GlobalTopology { + self.tri.global_topology() + } + + /// Returns the high-level topology kind (`Euclidean`, `Toroidal`, etc.). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, TopologyKind, + /// }; /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// - /// assert_eq!(dt.validation_policy(), ValidationPolicy::ExplicitOnly); + /// assert_eq!(dt.topology_kind(), TopologyKind::Euclidean); /// # Ok(()) /// # } /// ``` #[inline] #[must_use] - pub const fn validation_policy(&self) -> ValidationPolicy { - self.tri.validation_policy + pub const fn topology_kind(&self) -> TopologyKind { + self.tri.topology_kind() } - /// Tries to set the insertion-time global topology validation policy used by the underlying - /// triangulation. + /// Sets runtime global topology metadata after validating it against current topology. /// - /// This affects subsequent incremental insertions. (Construction-time behavior is determined - /// by the policy active during `new()` / `with_kernel()`.) + /// The update is atomic: if the current triangulation does not satisfy the + /// requested global topology, the previous metadata is restored before the + /// error is returned. /// /// # Errors /// - /// Returns [`ValidationConfigurationError::IncompatibleTopologyAndValidationPolicy`] when the - /// requested policy is incompatible with the current topology guarantee. + /// Returns [`DelaunayTriangulationValidationError::Tds`] if lower-level + /// structure is invalid while checking topology, or + /// [`DelaunayTriangulationValidationError::Triangulation`] when Level 3 + /// topology violates the requested metadata, for example when Euclidean + /// boundary facets are relabeled as closed spherical or toroidal topology, + /// [`DelaunayTriangulationValidationError::Embedding`] when Level 4 rejects + /// the requested embedding model, or + /// [`DelaunayTriangulationValidationError::VerificationFailed`] when Level 5 + /// Delaunay validation fails. The previous topology metadata is restored + /// before the error is returned. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulation; - /// use delaunay::prelude::validation::{ - /// ValidationConfigurationError, ValidationPolicy, + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, GlobalTopology, /// }; /// - /// # fn main() -> Result<(), ValidationConfigurationError> { - /// let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); - /// - /// dt.try_set_validation_policy(ValidationPolicy::Always)?; - /// assert_eq!( - /// dt.validation_policy(), - /// ValidationPolicy::Always - /// ); + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// dt.try_set_global_topology(GlobalTopology::Euclidean)?; + /// assert!(dt.global_topology().is_euclidean()); /// # Ok(()) /// # } /// ``` #[inline] - pub fn try_set_validation_policy( + pub fn try_set_global_topology( &mut self, - policy: ValidationPolicy, - ) -> Result<(), ValidationConfigurationError> { - self.tri.try_set_validation_policy(policy) + global_topology: GlobalTopology, + ) -> Result<(), DelaunayTriangulationValidationError> { + match self.tri.try_set_global_topology(global_topology) { + Ok(()) => Ok(()), + Err(InvariantError::Tds(err)) => Err(err.into()), + Err(InvariantError::Triangulation(err)) => Err(err.into()), + Err(InvariantError::Embedding(err)) => Err(err.into()), + Err(InvariantError::Delaunay(err)) => Err(err), + } } - /// Sets the insertion-time global topology validation policy used by the underlying - /// triangulation. + /// Sets the topology guarantee used for Level 3 topology validation. /// - /// Prefer [`try_set_validation_policy`](Self::try_set_validation_policy) when callers need + /// Prefer [`try_set_topology_guarantee`](Self::try_set_topology_guarantee) when callers need /// typed feedback for rejected combinations. This compatibility setter leaves the existing - /// policy unchanged and emits a warning if the requested combination is incoherent. + /// guarantee unchanged and emits a warning if the requested combination is incoherent. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulation; - /// use delaunay::prelude::validation::ValidationPolicy; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, TopologyGuarantee, + /// }; /// - /// let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); /// - /// dt.set_validation_policy(ValidationPolicy::Always); - /// assert_eq!(dt.validation_policy(), ValidationPolicy::Always); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); /// ``` #[inline] - pub fn set_validation_policy(&mut self, policy: ValidationPolicy) { - self.tri.set_validation_policy(policy); + pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { + self.tri.set_topology_guarantee(guarantee); } - /// Tries to set the topology guarantee used for Level 3 topology validation. + /// Returns an iterator over all facets in the triangulation. /// - /// # Errors + /// Delegates to the underlying `Triangulation` layer. This provides + /// efficient access to all facets without pre-allocating a vector. /// - /// Returns [`ValidationConfigurationError::IncompatibleTopologyAndValidationPolicy`] when the - /// requested guarantee cannot be represented with the current validation policy. + /// # Returns + /// + /// An iterator yielding `Result` items for all facets. + /// + /// Individual iterator items return + /// [`FacetError`] if a facet view cannot be + /// constructed from the current TDS state. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulation; - /// use delaunay::prelude::validation::{ - /// TopologyGuarantee, ValidationConfigurationError, + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, /// }; /// - /// # fn main() -> Result<(), ValidationConfigurationError> { - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - /// dt.try_set_topology_guarantee(TopologyGuarantee::Pseudomanifold)?; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = vec![ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// let facet_count = dt + /// .facets() + /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; + /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets /// # Ok(()) /// # } /// ``` - #[inline] - pub fn try_set_topology_guarantee( - &mut self, - guarantee: TopologyGuarantee, - ) -> Result<(), ValidationConfigurationError> { - self.tri.try_set_topology_guarantee(guarantee) + #[must_use] + pub fn facets(&self) -> AllFacetsIter<'_, U, V, D> { + self.tri.facets() } - /// Returns the automatic Delaunay repair policy. + /// Returns an iterator over all facets of one simplex. + /// + /// This is a convenience wrapper around + /// [`Triangulation::simplex_facets`](crate::Triangulation::simplex_facets). + /// + /// # Errors + /// + /// Returns [`FacetError`] if `simplex_key` is missing or this dimension + /// cannot be represented by public facet-index storage. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, - /// }; - /// use delaunay::prelude::repair::DelaunayRepairPolicy; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; /// - /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::EveryInsertion); + /// assert_eq!(dt.simplex_facets(simplex_key)?.count(), 4); /// # Ok(()) /// # } /// ``` - #[inline] - #[must_use] - pub const fn delaunay_repair_policy(&self) -> DelaunayRepairPolicy { - self.insertion_state.delaunay_repair_policy + pub fn simplex_facets( + &self, + simplex_key: SimplexKey, + ) -> Result, FacetError> { + self.tri.simplex_facets(simplex_key) } - /// Sets the automatic Delaunay repair policy. + /// Validates and returns a simplex-local facet handle. /// - /// This affects future incremental insertions; it does not rewrite already - /// stored topology. + /// This is a convenience wrapper around + /// [`Triangulation::facet_handle`](crate::Triangulation::facet_handle). + /// + /// # Errors + /// + /// Returns [`FacetError`] if `simplex_key` is missing or `facet_index` is + /// outside the simplex's local facet range. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, - /// }; - /// use delaunay::prelude::repair::DelaunayRepairPolicy; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; /// - /// dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::Never); + /// let facet = dt.facet_handle(simplex_key, 0)?; + /// assert_eq!(facet.simplex_key(), simplex_key); /// # Ok(()) /// # } /// ``` - #[inline] - pub const fn set_delaunay_repair_policy(&mut self, policy: DelaunayRepairPolicy) { - self.insertion_state.delaunay_repair_policy = policy; + pub fn facet_handle( + &self, + simplex_key: SimplexKey, + facet_index: u8, + ) -> Result { + self.tri.facet_handle(simplex_key, facet_index) } - /// Returns the automatic global Delaunay validation policy. + /// Revalidates a facet handle and returns a borrowed facet view. + /// + /// This is a convenience wrapper around + /// [`Triangulation::facet_view`](crate::Triangulation::facet_view). + /// + /// # Errors + /// + /// Returns [`FacetError`] if the handle is stale or no longer identifies a + /// live simplex-local facet. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, - /// }; - /// use delaunay::prelude::repair::DelaunayCheckPolicy; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// let facet = dt.facet_handle(simplex_key, 0)?; /// - /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EndOnly); + /// let view = dt.facet_view(facet)?; + /// assert_eq!(view.handle(), facet); /// # Ok(()) /// # } /// ``` - #[inline] - #[must_use] - pub const fn delaunay_check_policy(&self) -> DelaunayCheckPolicy { - self.insertion_state.delaunay_check_policy + pub fn facet_view(&self, facet: FacetHandle) -> Result, FacetError> { + self.tri.facet_view(facet) } - /// Sets the automatic global Delaunay validation policy. + /// Builds the owner-bound facet-to-simplices incidence index. /// - /// This affects future incremental insertions; it does not perform an - /// immediate global Delaunay check. + /// This is a convenience wrapper around + /// [`Triangulation::facet_incidence_index`](crate::Triangulation::facet_incidence_index). /// - /// # Examples + /// # Errors /// - /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, - /// }; - /// use delaunay::prelude::repair::DelaunayCheckPolicy; - /// use std::num::NonZeroUsize; + /// Returns [`TdsError`] if the triangulation's facet incidence is + /// structurally inconsistent. /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// let Some(every_two) = NonZeroUsize::new(2) else { - /// return Ok(()); - /// }; + /// # Examples /// - /// dt.set_delaunay_check_policy(DelaunayCheckPolicy::EveryN(every_two)); - /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EveryN(every_two)); + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let index = dt.facet_incidence_index()?; + /// assert_eq!(index.iter().filter(|facet| facet.is_one_sided()).count(), 3); /// # Ok(()) /// # } /// ``` - #[inline] - pub const fn set_delaunay_check_policy(&mut self, policy: DelaunayCheckPolicy) { - self.insertion_state.delaunay_check_policy = policy; + pub fn facet_incidence_index(&self) -> Result, TdsError> { + self.tri.facet_incidence_index() } -} -// ============================================================================= -// CONFIGURATION & TRAVERSAL (Minimal Bounds, continued) -// ============================================================================= - -impl DelaunayTriangulation { - // ------------------------------------------------------------------------- - // CONFIGURATION - // ------------------------------------------------------------------------- - - /// Returns the topology guarantee used for Level 3 topology validation. + /// Returns unique topology ridge candidates in the triangulation. + /// + /// This is a convenience wrapper around + /// [`Triangulation::ridges`](crate::Triangulation::ridges). + /// It yields [`RidgeCandidate`] values: in 2D, ridges are vertices; in 3D, + /// ridges are edges. + /// + /// # Errors + /// + /// Individual iterator items return [`QueryError::InvalidRidgeCandidate`] if + /// stored simplex vertices cannot form a valid codimension-two ridge. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, TopologyGuarantee, - /// }; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + /// + /// assert_eq!(dt.ridges().try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?, 3); /// # Ok(()) /// # } /// ``` - #[inline] - #[must_use] - pub const fn topology_guarantee(&self) -> TopologyGuarantee { - self.tri.topology_guarantee() + pub fn ridges(&self) -> impl Iterator, QueryError>> + '_ { + self.tri.ridges() } - /// Returns runtime global topology metadata associated with this triangulation. + /// Returns simplex-local ridge handles for `K3` Pachner moves. + /// + /// This is a convenience wrapper around + /// [`Triangulation::ridge_handles`](crate::Triangulation::ridge_handles). + /// Dimensions below 3 have no `K3` Pachner ridge candidates and yield an + /// empty iterator. + /// + /// # Errors + /// + /// Individual iterator items return [`QueryError::RidgeIndexCapacityExceeded`] + /// if a simplex-local omitted vertex index cannot fit in the public + /// [`RidgeHandle`] index storage. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, GlobalTopology, - /// }; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert!(dt.global_topology().is_euclidean()); + /// + /// assert_eq!( + /// dt.ridge_handles() + /// .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?, + /// 6, + /// ); /// # Ok(()) /// # } /// ``` - #[inline] - #[must_use] - pub const fn global_topology(&self) -> GlobalTopology { - self.tri.global_topology() + pub fn ridge_handles(&self) -> impl Iterator> + '_ { + self.tri.ridge_handles() } - /// Returns the high-level topology kind (`Euclidean`, `Toroidal`, etc.). + /// Validates and returns a simplex-local ridge handle. + /// + /// This is a convenience wrapper around + /// [`Triangulation::ridge_handle`](crate::Triangulation::ridge_handle). + /// + /// # Errors + /// + /// Returns [`FlipError`] if the dimension does not support ridge flips, the + /// simplex key is missing, or either omitted vertex index is invalid. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, TopologyKind, - /// }; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?]; + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// assert_eq!(dt.topology_kind(), TopologyKind::Euclidean); + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let ridge = dt.ridge_handle(simplex_key, 0, 1)?; + /// assert_eq!(ridge.simplex_key(), simplex_key); /// # Ok(()) /// # } /// ``` - #[inline] - #[must_use] - pub const fn topology_kind(&self) -> TopologyKind { - self.tri.topology_kind() + pub fn ridge_handle( + &self, + simplex_key: SimplexKey, + omit_a: u8, + omit_b: u8, + ) -> Result { + self.tri.ridge_handle(simplex_key, omit_a, omit_b) } - /// Sets runtime global topology metadata after validating it against current topology. + /// Returns the simplex star incident to a ridge candidate. /// - /// The update is atomic: if the current triangulation does not satisfy the - /// requested global topology, the previous metadata is restored before the - /// error is returned. + /// This is a convenience wrapper around + /// [`Triangulation::ridge_star_simplices`](crate::Triangulation::ridge_star_simplices). /// /// # Errors /// - /// Returns [`DelaunayTriangulationValidationError::Tds`] if lower-level - /// structure is invalid while checking topology, or - /// [`DelaunayTriangulationValidationError::Triangulation`] when Level 3 - /// topology violates the requested metadata, for example when Euclidean - /// boundary facets are relabeled as closed spherical or toroidal topology, - /// [`DelaunayTriangulationValidationError::Embedding`] when Level 4 rejects - /// the requested embedding model, or - /// [`DelaunayTriangulationValidationError::VerificationFailed`] when Level 5 - /// Delaunay validation fails. The previous topology metadata is restored - /// before the error is returned. + /// Returns [`ManifoldError`] if any ridge vertex is stale or incidence + /// bookkeeping cannot be traversed. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulationBuilder, GlobalTopology, + /// DelaunayResult, DelaunayTriangulationBuilder, /// }; + /// use delaunay::prelude::query::RidgeCandidate; /// /// # fn main() -> DelaunayResult<()> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0]?, + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// dt.try_set_global_topology(GlobalTopology::Euclidean)?; - /// assert!(dt.global_topology().is_euclidean()); + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Ok(ridge) = RidgeCandidate::<3>::try_from_vertices( + /// dt.vertices().map(|(key, _)| key).take(2), + /// ) else { + /// return Ok(()); + /// }; + /// + /// let star = dt.ridge_star_simplices(&ridge)?; + /// assert!(!star.is_empty()); /// # Ok(()) /// # } /// ``` - #[inline] - pub fn try_set_global_topology( - &mut self, - global_topology: GlobalTopology, - ) -> Result<(), DelaunayTriangulationValidationError> { - match self.tri.try_set_global_topology(global_topology) { - Ok(()) => Ok(()), - Err(InvariantError::Tds(err)) => Err(err.into()), - Err(InvariantError::Triangulation(err)) => Err(err.into()), - Err(InvariantError::Embedding(err)) => Err(err.into()), - Err(InvariantError::Delaunay(err)) => Err(err), - } + pub fn ridge_star_simplices( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + self.tri.ridge_star_simplices(ridge_candidate) } - /// Sets the topology guarantee used for Level 3 topology validation. + /// Revalidates a ridge candidate and returns a borrowed ridge query. /// - /// Prefer [`try_set_topology_guarantee`](Self::try_set_topology_guarantee) when callers need - /// typed feedback for rejected combinations. This compatibility setter leaves the existing - /// guarantee unchanged and emits a warning if the requested combination is incoherent. + /// Unlike [`DelaunayTriangulation::ridge_view`], the query permits an empty + /// incident star. + /// + /// # Errors + /// + /// Returns [`ManifoldError`] if any ridge vertex is stale or incidence + /// bookkeeping cannot be traversed. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayTriangulation, TopologyGuarantee, + /// DelaunayResult, DelaunayTriangulationBuilder, /// }; + /// use delaunay::prelude::query::RidgeCandidate; /// - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - /// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Ok(ridge) = RidgeCandidate::<2>::try_from_vertices( + /// dt.vertices().map(|(key, _)| key).take(1), + /// ) else { + /// return Ok(()); + /// }; /// - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// let query = dt.ridge_query(&ridge)?; + /// assert!(!query.incident_simplices().is_empty()); + /// # Ok(()) + /// # } /// ``` - #[inline] - pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { - self.tri.set_topology_guarantee(guarantee); + pub fn ridge_query( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + self.tri.ridge_query(ridge_candidate) } - /// Returns an iterator over all facets in the triangulation. - /// - /// Delegates to the underlying `Triangulation` layer. This provides - /// efficient access to all facets without pre-allocating a vector. - /// - /// # Returns + /// Revalidates a ridge candidate and returns a borrowed ridge view. /// - /// An iterator yielding `Result` items for all facets. + /// # Errors /// - /// Individual iterator items return - /// [`FacetError`](crate::prelude::tds::FacetError) if a facet view cannot be - /// constructed from the current TDS state. + /// Returns [`ManifoldError`] if any ridge vertex is stale, incidence + /// bookkeeping cannot be traversed, or the ridge has an empty star. /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + /// DelaunayResult, DelaunayTriangulationBuilder, /// }; + /// use delaunay::prelude::query::RidgeCandidate; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Query(#[from] delaunay::query::QueryError), - /// # #[error(transparent)] - /// # Facet(#[from] delaunay::prelude::tds::FacetError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// let vertices = vec![ - /// delaunay::vertex![0.0, 0.0, 0.0]?, - /// delaunay::vertex![1.0, 0.0, 0.0]?, - /// delaunay::vertex![0.0, 1.0, 0.0]?, - /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Ok(ridge) = RidgeCandidate::<2>::try_from_vertices( + /// dt.vertices().map(|(key, _)| key).take(1), + /// ) else { + /// return Ok(()); + /// }; /// - /// let facet_count = dt - /// .facets() - /// .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; - /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets + /// let view = dt.ridge_view(&ridge)?; + /// assert_eq!(view.ridge_candidate(), &ridge); /// # Ok(()) /// # } /// ``` - #[must_use] - pub fn facets(&self) -> AllFacetsIter<'_, U, V, D> { - self.tri.facets() + pub fn ridge_view( + &self, + ridge_candidate: &RidgeCandidate, + ) -> Result, ManifoldError> { + self.tri.ridge_view(ridge_candidate) } /// Builds a lifetime-bound adjacency view for fast repeated topology queries. @@ -1419,19 +2188,10 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Adjacency(#[from] delaunay::query::TopologyIndexBuildError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1459,6 +2219,30 @@ impl DelaunayTriangulation { /// # Errors /// /// Returns an error if the maintained incidence relation is internally inconsistent. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((vertex_key, _)) = dt.vertices().next() else { + /// return Ok(()); + /// }; + /// + /// let incidence = dt.incidence()?; + /// assert_eq!(incidence.number_of_adjacent_simplices(vertex_key), 1); + /// # Ok(()) + /// # } + /// ``` #[inline] pub fn incidence(&self) -> Result, TopologyIndexBuildError> { self.as_triangulation().incidence() @@ -1472,6 +2256,28 @@ impl DelaunayTriangulation { /// # Errors /// /// Returns an error if a simplex references a missing vertex key. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0, 0.0]?, + /// delaunay::vertex![0.0, 0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let edge_index = dt.build_edge_index()?; + /// assert_eq!(edge_index.number_of_edges(), 6); + /// # Ok(()) + /// # } + /// ``` #[inline] pub fn build_edge_index(&self) -> Result, TopologyIndexBuildError> { self.as_triangulation().build_edge_index() @@ -1485,6 +2291,30 @@ impl DelaunayTriangulation { /// # Errors /// /// Returns an error if a simplex references a missing neighbor key. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((simplex_key, _)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let neighbor_index = dt.build_simplex_neighbor_index()?; + /// assert_eq!(neighbor_index.number_of_simplex_neighbors(simplex_key), 0); + /// # Ok(()) + /// # } + /// ``` #[inline] pub fn build_simplex_neighbor_index( &self, @@ -1500,18 +2330,11 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// /// // A single 3D tetrahedron has 6 unique edges. - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1529,6 +2352,81 @@ impl DelaunayTriangulation { self.as_triangulation().edges() } + /// Validates and returns an edge key for two live vertices. + /// + /// This is a convenience wrapper around + /// [`Triangulation::edge_key`](crate::Triangulation::edge_key). + /// + /// # Errors + /// + /// Returns [`EdgeKeyError`] if the vertices are stale, duplicated, or do not + /// share a stored simplex edge. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((_simplex_key, simplex)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let edge = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// assert!(dt.edges().any(|candidate| candidate == edge)); + /// # Ok(()) + /// # } + /// ``` + pub fn edge_key(&self, a: VertexKey, b: VertexKey) -> Result { + self.as_triangulation().edge_key(a, b) + } + + /// Revalidates an edge key and returns a borrowed edge view. + /// + /// This is a convenience wrapper around + /// [`Triangulation::edge_view`](crate::Triangulation::edge_view). + /// + /// # Errors + /// + /// Returns [`EdgeKeyError`] if the key is stale or the maintained incidence + /// relation cannot prove a live edge star. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let Some((_simplex_key, simplex)) = dt.simplices().next() else { + /// return Ok(()); + /// }; + /// + /// let key = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; + /// let view = dt.edge_view(key)?; + /// assert_eq!(view.key(), key); + /// # Ok(()) + /// # } + /// ``` + pub fn edge_view(&self, edge: EdgeKey) -> Result, EdgeKeyError> { + self.as_triangulation().edge_view(edge) + } + /// Returns an iterator over all unique edges incident to a vertex. /// /// This is a convenience wrapper around @@ -1539,17 +2437,10 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1582,18 +2473,11 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// /// // A single tetrahedron has no simplex neighbors. - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0]?, @@ -1613,6 +2497,140 @@ impl DelaunayTriangulation { self.as_triangulation().simplex_neighbors(c) } + /// Locates a point in this triangulation using the triangulation's kernel. + /// + /// This is a convenience wrapper around + /// [`Triangulation::locate`](crate::Triangulation::locate). + /// + /// # Errors + /// + /// Returns [`LocateError`] if structural data or predicates fail during the + /// facet-walking query. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::algorithms::LocateResult; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::geometry::Point; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// let location = dt.locate(&query, None)?; + /// std::assert_matches!(location, LocateResult::InsideSimplex(_)); + /// # Ok(()) + /// # } + /// ``` + pub fn locate( + &self, + point: &Point, + hint: Option, + ) -> Result + where + K: Kernel, + { + self.as_triangulation().locate(point, hint) + } + + /// Locates a point and returns facet-walk traversal statistics. + /// + /// This is a convenience wrapper around + /// [`Triangulation::locate_with_stats`](crate::Triangulation::locate_with_stats). + /// + /// # Errors + /// + /// Returns [`LocateError`] if structural data or predicates fail during the + /// facet-walking query. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::geometry::Point; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// let (_location, stats) = dt.locate_with_stats(&query, None)?; + /// assert!(!stats.fell_back_to_scan()); + /// # Ok(()) + /// # } + /// ``` + pub fn locate_with_stats( + &self, + point: &Point, + hint: Option, + ) -> Result<(LocateResult, LocateStats), LocateError> + where + K: Kernel, + { + self.as_triangulation().locate_with_stats(point, hint) + } + + /// Finds the conflict region for inserting `point` from a known start simplex. + /// + /// This is a convenience wrapper around + /// [`Triangulation::find_conflict_region`](crate::Triangulation::find_conflict_region). + /// + /// # Errors + /// + /// Returns [`ConflictError`] if the start simplex is stale, structural data is + /// inconsistent, or the conflict boundary cannot be classified. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::algorithms::LocateResult; + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// use delaunay::prelude::geometry::Point; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// let query = Point::try_from([0.2, 0.2])?; + /// + /// if let LocateResult::InsideSimplex(start) = dt.locate(&query, None)? { + /// let conflict = dt.find_conflict_region(&query, start)?; + /// assert!(!conflict.is_empty()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn find_conflict_region( + &self, + point: &Point, + start_simplex: SimplexKey, + ) -> Result + where + K: Kernel, + { + self.as_triangulation() + .find_conflict_region(point, start_simplex) + } + /// Returns a slice view of a simplex's vertex keys. /// /// This is a zero-allocation accessor that validates the simplex key and @@ -1629,19 +2647,10 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] delaunay::prelude::tds::TdsError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1671,17 +2680,10 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// use delaunay::prelude::query::*; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Source(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = vec![ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1737,19 +2739,9 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::prelude::tds::EdgeKey; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Edge(#[from] delaunay::prelude::tds::EdgeKeyError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1760,7 +2752,7 @@ impl DelaunayTriangulation { /// return Ok(()); /// }; /// - /// let boundary_edge = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; + /// let boundary_edge = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; /// assert!(dt.try_interior_facet_for_edge_2d(boundary_edge)?.is_none()); /// # Ok(()) /// # } @@ -1793,19 +2785,9 @@ impl DelaunayTriangulation { /// # Examples /// /// ```rust - /// use delaunay::prelude::construction::DelaunayTriangulationBuilder; - /// use delaunay::prelude::tds::EdgeKey; + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] delaunay::DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Edge(#[from] delaunay::prelude::tds::EdgeKeyError), - /// # #[error(transparent)] - /// # Coordinate(#[from] delaunay::prelude::geometry::CoordinateConversionError), - /// # } - /// # fn main() -> Result<(), ExampleError> { + /// # fn main() -> DelaunayResult<()> { /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, @@ -1816,7 +2798,7 @@ impl DelaunayTriangulation { /// return Ok(()); /// }; /// - /// let boundary_edge = EdgeKey::try_new(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; + /// let boundary_edge = dt.edge_key(simplex.vertices()[0], simplex.vertices()[1])?; /// assert_eq!(dt.try_incident_facets_to_edge_2d(boundary_edge)?.count(), 1); /// # Ok(()) /// # } @@ -2379,14 +3361,8 @@ mod tests { .build() .unwrap(); assert!(dt.number_of_simplices() > 1); - let simplex_identity_before: HashSet<_> = dt - .simplices() - .map(|(simplex_key, simplex)| (simplex_key, simplex.uuid())) - .collect(); - let vertex_identity_before: HashSet<_> = dt - .vertices() - .map(|(vertex_key, vertex)| (vertex_key, vertex.uuid())) - .collect(); + let simplex_identity_before: HashSet<_> = dt.simplex_uuids().collect(); + let vertex_identity_before: HashSet<_> = dt.vertex_uuids().collect(); let mut data = SimplexSecondaryMap::new(); for (simplex_key, simplex) in dt.simplices() { @@ -2395,14 +3371,8 @@ mod tests { dt.try_fill_simplex_data_from(&data).unwrap(); - let simplex_identity_after: HashSet<_> = dt - .simplices() - .map(|(simplex_key, simplex)| (simplex_key, simplex.uuid())) - .collect(); - let vertex_identity_after: HashSet<_> = dt - .vertices() - .map(|(vertex_key, vertex)| (vertex_key, vertex.uuid())) - .collect(); + let simplex_identity_after: HashSet<_> = dt.simplex_uuids().collect(); + let vertex_identity_after: HashSet<_> = dt.vertex_uuids().collect(); assert_eq!(simplex_identity_after, simplex_identity_before); assert_eq!(vertex_identity_after, vertex_identity_before); @@ -2423,25 +3393,13 @@ mod tests { .simplex_data_type::() .build() .unwrap(); - let simplex_identity_before: HashSet<_> = dt - .simplices() - .map(|(simplex_key, simplex)| (simplex_key, simplex.uuid())) - .collect(); - let vertex_identity_before: HashSet<_> = dt - .vertices() - .map(|(vertex_key, vertex)| (vertex_key, vertex.uuid())) - .collect(); + let simplex_identity_before: HashSet<_> = dt.simplex_uuids().collect(); + let vertex_identity_before: HashSet<_> = dt.vertex_uuids().collect(); dt.fill_simplex_data(|_, simplex| simplex.number_of_vertices()); - let simplex_identity_after: HashSet<_> = dt - .simplices() - .map(|(simplex_key, simplex)| (simplex_key, simplex.uuid())) - .collect(); - let vertex_identity_after: HashSet<_> = dt - .vertices() - .map(|(vertex_key, vertex)| (vertex_key, vertex.uuid())) - .collect(); + let simplex_identity_after: HashSet<_> = dt.simplex_uuids().collect(); + let vertex_identity_after: HashSet<_> = dt.vertex_uuids().collect(); assert_eq!(simplex_identity_after, simplex_identity_before); assert_eq!(vertex_identity_after, vertex_identity_before); diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs index 1b4976cc..4364500c 100644 --- a/src/delaunay/repair.rs +++ b/src/delaunay/repair.rs @@ -10,8 +10,6 @@ #![forbid(unsafe_code)] -#[cfg(test)] -use crate::construction::test_hooks; use crate::core::algorithms::flips::{ DelaunayRepairError, DelaunayRepairHeuristicRebuildFailure, DelaunayRepairHeuristicVertexContext, DelaunayRepairOrientationCanonicalizationFailure, @@ -28,11 +26,10 @@ use crate::core::vertex::Vertex; use crate::geometry::kernel::{ExactPredicates, Kernel, RobustKernel}; use crate::geometry::traits::coordinate::CoordinateValues; use crate::triangulation::DelaunayTriangulation; -#[cfg(test)] -use crate::validation::DelaunayTriangulationCandidate; use rand::SeedableRng; use rand::seq::SliceRandom; use std::{ + cell::Cell, fmt, hash::{Hash, Hasher}, num::NonZeroUsize, @@ -44,7 +41,7 @@ const HEURISTIC_REBUILD_ATTEMPTS: usize = 6; const MAX_HEURISTIC_REBUILD_DEPTH: usize = 1; thread_local! { - static HEURISTIC_REBUILD_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + static HEURISTIC_REBUILD_DEPTH: Cell = const { Cell::new(0) }; } struct HeuristicRebuildRecursionGuard { @@ -405,8 +402,8 @@ where V: DataType, { #[cfg(test)] - if test_hooks::force_repair_nonconvergent_enabled() { - return Err(test_hooks::synthetic_nonconvergent_error()); + if tests::force_repair_nonconvergent_enabled() { + return Err(tests::synthetic_nonconvergent_error()); } let operation = TopologicalOperation::FacetFlip; let topology = self.tri.topology_guarantee(); @@ -477,6 +474,12 @@ where let (tds, kernel) = (&mut self.tri.tds, &kernel); repair_delaunay_with_flips_k2_k3_run(tds, kernel, seed_simplices, topology, max_flips) } +} + +impl DelaunayTriangulation { + // ============================================================================= + // REPAIR POLICY GATES (No Kernel Bounds) + // ============================================================================= /// Applies the repair policy only when the dimension and topology can /// support bistellar flips. @@ -535,7 +538,7 @@ where fn force_heuristic_rebuild_enabled() -> bool { #[cfg(test)] { - test_hooks::force_heuristic_rebuild_enabled() + tests::force_heuristic_rebuild_enabled() } #[cfg(not(test))] { @@ -923,10 +926,8 @@ where #[cfg(test)] mod tests { use super::*; - use crate::construction::test_hooks; use crate::core::algorithms::flips::{ DelaunayRepairDiagnostics, DelaunayRepairPostconditionFailure, FlipError, RepairQueueOrder, - verify_delaunay_via_flip_predicates, }; use crate::core::simplex::Simplex; use crate::core::tds::{Tds, TriangulationConstructionState}; @@ -935,9 +936,73 @@ mod tests { use crate::geometry::kernel::{AdaptiveKernel, RobustKernel}; use crate::topology::traits::topological_space::GlobalTopology; use crate::triangulation::DelaunayTriangulation; + use crate::validation::DelaunayTriangulationCandidate; use crate::vertex; use std::{assert_matches, num::NonZeroUsize, sync::Once}; + // Last-resort fault injection for rollback branches that are hard to + // trigger deterministically; thread-local state avoids cross-test leakage. + // Remove this once a cleaner harness can reach the branch directly. + thread_local! { + static FORCE_HEURISTIC_REBUILD: Cell = const { Cell::new(false) }; + static FORCE_REPAIR_NONCONVERGENT: Cell = const { Cell::new(false) }; + } + + #[must_use] + pub(super) fn force_heuristic_rebuild_enabled() -> bool { + FORCE_HEURISTIC_REBUILD.with(Cell::get) + } + + #[must_use] + pub(super) fn force_repair_nonconvergent_enabled() -> bool { + FORCE_REPAIR_NONCONVERGENT.with(Cell::get) + } + + #[must_use] + pub(super) fn synthetic_nonconvergent_error() -> DelaunayRepairError { + DelaunayRepairError::NonConvergent { + max_flips: 0, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 0, + flips_performed: 0, + max_queue_len: 0, + ambiguous_predicates: 0, + ambiguous_predicate_samples: Vec::new(), + predicate_failures: 0, + cycle_detections: 0, + cycle_signature_samples: Vec::new(), + attempt: 0, + queue_order: RepairQueueOrder::Fifo, + }), + } + } + + #[must_use] + fn set_force_heuristic_rebuild(enabled: bool) -> bool { + FORCE_HEURISTIC_REBUILD.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + + fn restore_force_heuristic_rebuild(prior: bool) { + FORCE_HEURISTIC_REBUILD.with(|flag| flag.set(prior)); + } + + #[must_use] + fn set_force_repair_nonconvergent(enabled: bool) -> bool { + FORCE_REPAIR_NONCONVERGENT.with(|flag| { + let prior = flag.get(); + flag.set(enabled); + prior + }) + } + + fn restore_force_repair_nonconvergent(prior: bool) { + FORCE_REPAIR_NONCONVERGENT.with(|flag| flag.set(prior)); + } + fn init_tracing() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -956,14 +1021,14 @@ mod tests { impl ForceHeuristicRebuildGuard { fn enable() -> Self { - let prior = test_hooks::set_force_heuristic_rebuild(true); + let prior = set_force_heuristic_rebuild(true); Self { prior } } } impl Drop for ForceHeuristicRebuildGuard { fn drop(&mut self) { - test_hooks::restore_force_heuristic_rebuild(self.prior); + restore_force_heuristic_rebuild(self.prior); } } @@ -973,14 +1038,14 @@ mod tests { impl ForceRepairNonconvergentGuard { fn enable() -> Self { - let prior = test_hooks::set_force_repair_nonconvergent(true); + let prior = set_force_repair_nonconvergent(true); Self { prior } } } impl Drop for ForceRepairNonconvergentGuard { fn drop(&mut self) { - test_hooks::restore_force_repair_nonconvergent(self.prior); + restore_force_repair_nonconvergent(self.prior); } } @@ -1323,18 +1388,26 @@ mod tests { // and robust fallback kernels both see a real flip-repair site. let kernel = AdaptiveKernel::::new(); let robust_kernel = RobustKernel::::new(); - let tds = non_delaunay_quad_tds(); - assert!(verify_delaunay_via_flip_predicates(&tds, &kernel).is_err()); - assert!(verify_delaunay_via_flip_predicates(&tds, &robust_kernel).is_err()); let mut dt: DelaunayTriangulation, (), (), 2> = DelaunayTriangulationCandidate::assemble( - tds, + non_delaunay_quad_tds(), kernel, TopologyGuarantee::PLManifold, GlobalTopology::DEFAULT, ) .into_repairable_delaunay_for_test(); dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + assert!(dt.verify_via_flip_predicates().is_err()); + + let robust_dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulationCandidate::assemble( + non_delaunay_quad_tds(), + robust_kernel, + TopologyGuarantee::PLManifold, + GlobalTopology::DEFAULT, + ) + .into_repairable_delaunay_for_test(); + assert!(robust_dt.verify_via_flip_predicates().is_err()); // max_flips=0 should fail (flips are needed but budget is zero). let config_zero = DelaunayRepairHeuristicConfig { diff --git a/src/delaunay/triangulation.rs b/src/delaunay/triangulation.rs index 70f8caa2..1205d2a8 100644 --- a/src/delaunay/triangulation.rs +++ b/src/delaunay/triangulation.rs @@ -37,17 +37,14 @@ use crate::core::triangulation::Triangulation; /// Uses efficient incremental cavity-based insertion algorithm: /// - ✅ Point location (facet walking) - [`locate`] /// - ✅ Conflict region computation (local BFS) - [`find_conflict_region`] -/// - ✅ Cavity extraction and filling - [`extract_cavity_boundary`], [`fill_cavity`] -/// - ✅ Local neighbor wiring - [`wire_cavity_neighbors`] -/// - ✅ Hull extension for outside points - [`extend_hull`] +/// - ✅ Cavity extraction and filling - [`extract_cavity_boundary`] plus internal cavity replacement +/// - ✅ Local neighbor wiring after cavity replacement +/// - ✅ Hull extension for outside points /// - ✅ Flip-based Delaunay repair (k=2/k=3 bistellar flips) /// /// [`locate`]: crate::algorithms::locate /// [`find_conflict_region`]: crate::algorithms::find_conflict_region /// [`extract_cavity_boundary`]: crate::algorithms::extract_cavity_boundary -/// [`fill_cavity`]: crate::fill_cavity -/// [`wire_cavity_neighbors`]: crate::wire_cavity_neighbors -/// [`extend_hull`]: crate::extend_hull /// /// # Examples /// diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index 9c3ab074..c44aafac 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -8,18 +8,24 @@ #![forbid(unsafe_code)] -use crate::core::algorithms::flips::{DelaunayRepairError, verify_delaunay_for_triangulation}; +use crate::core::algorithms::flips::{ + DelaunayRepairError, verify_triangulation_via_flip_predicates, +}; use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::embedding::TriangulationEmbeddingValidationError; use crate::core::operations::DelaunayInsertionState; use crate::core::tds::{ - InvariantError, InvariantKind, InvariantViolation, Tds, TdsError, TriangulationValidationReport, + InvariantError, InvariantKind, InvariantViolation, SimplexKey, Tds, TdsError, + TriangulationValidationReport, }; use crate::core::traits::data_type::DataType; use crate::core::triangulation::Triangulation; use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; +#[cfg(feature = "diagnostics")] +use crate::delaunay_property_validation::debug_print_first_delaunay_violation as debug_print_first_tds_delaunay_violation; use crate::delaunay_property_validation::{ - DelaunayValidationError, delaunay_violation_report, is_delaunay_property_only, + DelaunayValidationError, DelaunayViolationReport, + delaunay_violation_report as tds_delaunay_violation_report, is_delaunay_property_only, }; use crate::geometry::kernel::Kernel; use crate::repair::DelaunayRepairOperation; @@ -197,8 +203,9 @@ where /// Typed source for Level 5 Delaunay verification failures. /// /// Passive validation has two implementation paths: -/// - flip-predicate verification via [`verify_delaunay_for_triangulation`], used by -/// [`DelaunayTriangulation::is_valid_delaunay`](crate::DelaunayTriangulation::is_valid_delaunay) +/// - flip-predicate verification via +/// [`DelaunayTriangulation::verify_via_flip_predicates`](crate::DelaunayTriangulation::verify_via_flip_predicates), +/// used by [`DelaunayTriangulation::is_valid_delaunay`](crate::DelaunayTriangulation::is_valid_delaunay) /// - empty-circumsphere validation via `is_delaunay_property_only`, used when /// reconstructing Euclidean triangulations from raw [`Tds`] /// @@ -558,7 +565,7 @@ where /// ``` pub fn is_valid_delaunay(&self) -> Result<(), DelaunayTriangulationValidationError> { // Use fast flip-based verification (O(simplices) instead of O(simplices × vertices)) - self.is_delaunay_via_flips().map_err(|source| { + self.verify_via_flip_predicates().map_err(|source| { DelaunayTriangulationValidationError::VerificationFailed { source: Box::new(DelaunayVerificationError::from(source)), } @@ -627,7 +634,7 @@ where /// ``` pub fn delaunay_report(&self) -> Result<(), TriangulationValidationReport> { if self.global_topology().is_euclidean() { - return match delaunay_violation_report(self.tds(), None) { + return match tds_delaunay_violation_report(self.tds(), None) { Ok(report) if report.is_valid() => Ok(()), Ok(report) => Err(TriangulationValidationReport { violations: report @@ -667,6 +674,74 @@ where }) } + /// Builds a detailed Delaunay empty-circumsphere violation report. + /// + /// This is the high-level owner-bound counterpart to the TDS-level + /// [`delaunay_violation_report`](crate::delaunay_violation_report) helper. + /// It keeps callers on the `DelaunayTriangulation` API while returning the + /// same typed, key-oriented diagnostics. + /// + /// # Errors + /// + /// Returns [`DelaunayValidationError`] if the scan encounters invalid + /// simplex structure, missing vertex references, or robust predicate + /// conversion failures. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayResult, DelaunayTriangulationBuilder, + /// }; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// let report = dt.delaunay_violation_report(None)?; + /// assert!(report.is_valid()); + /// # Ok(()) + /// # } + /// ``` + pub fn delaunay_violation_report( + &self, + simplices_to_check: Option<&[SimplexKey]>, + ) -> Result { + tds_delaunay_violation_report(self.tds(), simplices_to_check) + } + + /// Logs detailed information for the first Delaunay violation, when present. + /// + /// This diagnostics-only method keeps debug workflows on the high-level + /// triangulation owner instead of requiring public TDS access. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayResult, DelaunayTriangulationBuilder}; + /// + /// # fn main() -> DelaunayResult<()> { + /// let vertices = [ + /// delaunay::vertex![0.0, 0.0]?, + /// delaunay::vertex![1.0, 0.0]?, + /// delaunay::vertex![0.0, 1.0]?, + /// ]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; + /// + /// dt.debug_print_first_delaunay_violation(None); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "diagnostics")] + #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] + pub fn debug_print_first_delaunay_violation(&self, simplices_subset: Option<&[SimplexKey]>) { + debug_print_first_tds_delaunay_violation(self.tds(), simplices_subset); + } + /// Verify the Delaunay property via fast O(simplices) flip predicates. /// /// This checks the Delaunay property by testing all possible flip configurations @@ -696,12 +771,12 @@ where /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// // Fast O(N) verification - /// assert!(dt.is_delaunay_via_flips().is_ok()); + /// assert!(dt.verify_via_flip_predicates().is_ok()); /// # Ok(()) /// # } /// ``` - pub fn is_delaunay_via_flips(&self) -> Result<(), DelaunayRepairError> { - verify_delaunay_for_triangulation(&self.tri) + pub fn verify_via_flip_predicates(&self) -> Result<(), DelaunayRepairError> { + verify_triangulation_via_flip_predicates(&self.tri) } /// Performs cumulative validation for Levels 1–5. @@ -893,24 +968,23 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::tds::Tds; /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, + /// DelaunayResult, DelaunayTriangulation, /// }; + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::triangulation::Triangulation; /// /// # fn main() -> DelaunayResult<()> { - /// let vertices = vec![ + /// // Reconstruct DelaunayTriangulation from imported low-level storage. + /// let vertices = [ /// delaunay::vertex![0.0, 0.0, 0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0, 0.0, 0.0]?, /// delaunay::vertex![0.0, 1.0, 0.0, 0.0]?, /// delaunay::vertex![0.0, 0.0, 1.0, 0.0]?, /// delaunay::vertex![0.0, 0.0, 0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// - /// // Reconstruct DelaunayTriangulation from a Tds snapshot. - /// let tds: Tds<(), (), 4> = dt.tds().clone(); + /// let tds = + /// Triangulation::, (), (), 4>::build_initial_simplex(&vertices)?; /// let reconstructed = DelaunayTriangulation::try_from_tds(tds, FastKernel::new())?; /// assert_eq!(reconstructed.number_of_vertices(), 5); /// # Ok(()) @@ -941,21 +1015,22 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::geometry::FastKernel; /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, TopologyGuarantee, + /// DelaunayResult, DelaunayTriangulation, TopologyGuarantee, /// }; + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::triangulation::Triangulation; /// /// # fn main() -> DelaunayResult<()> { - /// let vertices = vec![ + /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// + /// let tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_guarantee( - /// dt.tds().clone(), + /// tds, /// FastKernel::new(), /// TopologyGuarantee::PLManifoldStrict, /// )?; @@ -994,22 +1069,22 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::geometry::FastKernel; /// use delaunay::prelude::construction::{ - /// DelaunayResult, DelaunayTriangulation, DelaunayTriangulationBuilder, GlobalTopology, - /// TopologyGuarantee, + /// DelaunayResult, DelaunayTriangulation, GlobalTopology, TopologyGuarantee, /// }; + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::triangulation::Triangulation; /// /// # fn main() -> DelaunayResult<()> { - /// let vertices = vec![ + /// let vertices = [ /// delaunay::vertex![0.0, 0.0]?, /// delaunay::vertex![1.0, 0.0]?, /// delaunay::vertex![0.0, 1.0]?, /// ]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; - /// + /// let tds = + /// Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_context( - /// dt.tds().clone(), + /// tds, /// FastKernel::new(), /// TopologyGuarantee::PLManifoldStrict, /// GlobalTopology::Euclidean, @@ -1496,7 +1571,7 @@ mod tests { // Break vertex mapping so Level 2 structural validation fails. let vk = dt.tds().vertex_keys().next().unwrap(); let uuid = dt.tds().vertex(vk).unwrap().uuid(); - dt.tds_mut().uuid_to_vertex_key.remove(&uuid); + dt.tds_mut_for_repair().uuid_to_vertex_key.remove(&uuid); match dt.validate() { Err(DelaunayTriangulationValidationError::Tds(source)) @@ -1521,7 +1596,7 @@ mod tests { // Add an isolated vertex so Level 3 (topology) fails. let _ = dt - .tds_mut() + .tds_mut_for_repair() .insert_vertex_with_mapping(test_vertex([0.5, 0.5, 0.5])) .unwrap(); diff --git a/src/geometry/algorithms/convex_hull.rs b/src/geometry/algorithms/convex_hull.rs index 1030525b..86bfa209 100644 --- a/src/geometry/algorithms/convex_hull.rs +++ b/src/geometry/algorithms/convex_hull.rs @@ -74,8 +74,6 @@ use thiserror::Error; // Import Orientation for predicates use crate::geometry::predicates::Orientation; -#[cfg(test)] -use num_traits::NumCast; // ============================================================================= // ERROR TYPES @@ -2103,6 +2101,7 @@ mod tests { }; use crate::triangulation::DelaunayTriangulation; use crate::vertex; + use num_traits::NumCast; use std::assert_matches; use std::error::Error; use std::sync::atomic::Ordering; @@ -2183,15 +2182,6 @@ mod tests { /// - Inside point (centroid) is correctly identified as not outside /// - Outside point is correctly identified as outside /// - /// # Usage - /// - /// ```ignore - /// test_hull_dimensions! { - /// hull_2d => 2 => "triangle" => 3 => - /// vec![delaunay::vertex![0.0, 0.0]?, delaunay::vertex![1.0, 0.0]?, delaunay::vertex![0.0, 1.0]?], - /// } - /// ``` - /// /// # Arguments /// /// * `$test_name` - Base name for generated tests @@ -3051,12 +3041,7 @@ mod tests { ); // Validate vertices through FacetView for (i, facet_handle) in hull_2d.hull_facets.iter().enumerate() { - let facet_view = FacetView::try_new( - &dt_2d.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt_2d.facet_view(*facet_handle).unwrap(); let vertices = facet_view.vertices().count(); assert_eq!(vertices, 2, "2D facet {i} should have exactly 2 vertices"); } @@ -3088,12 +3073,7 @@ mod tests { ); // Validate vertices through FacetView for (i, facet_handle) in hull_3d.hull_facets.iter().enumerate() { - let facet_view = FacetView::try_new( - &dt_3d.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt_3d.facet_view(*facet_handle).unwrap(); let vertices = facet_view.vertices().count(); assert_eq!(vertices, 3, "3D facet {i} should have exactly 3 vertices"); } @@ -3126,12 +3106,7 @@ mod tests { ); // Validate vertices through FacetView for (i, facet_handle) in hull_4d.hull_facets.iter().enumerate() { - let facet_view = FacetView::try_new( - &dt_4d.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt_4d.facet_view(*facet_handle).unwrap(); let vertices = facet_view.vertices().count(); assert_eq!(vertices, 4, "4D facet {i} should have exactly 4 vertices"); } @@ -3165,12 +3140,7 @@ mod tests { ); // Validate vertices through FacetView for (i, facet_handle) in hull_5d.hull_facets.iter().enumerate() { - let facet_view = FacetView::try_new( - &dt_5d.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt_5d.facet_view(*facet_handle).unwrap(); let vertices = facet_view.vertices().count(); assert_eq!(vertices, 5, "5D facet {i} should have exactly 5 vertices"); } @@ -3845,12 +3815,7 @@ mod tests { // Test fallback with various points if let Some(facet_handle) = hull.facet(0) { // Create FacetView to get vertices - let facet_view = FacetView::try_new( - &dt.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt.facet_view(*facet_handle).unwrap(); let facet_vertices = facet_view_to_vertices(&facet_view); // Test with a point very close to the facet (should not be visible) @@ -3974,7 +3939,7 @@ mod tests { // Test FacetCacheProvider trait implementation let _facet_cache = hull.facet_cache(); let cached_gen = hull.cached_generation(); - let tds_gen = dt.as_triangulation().tds.generation(); + let tds_gen = dt.topology_generation(); assert_eq!( cached_gen.load(std::sync::atomic::Ordering::Acquire), tds_gen, @@ -4060,16 +4025,7 @@ mod tests { let vertex_counts: Vec = hull .hull_facets .iter() - .map(|facet_handle| { - FacetView::try_new( - &dt.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap() - .vertices() - .count() - }) + .map(|facet_handle| dt.facet_view(*facet_handle).unwrap().vertices().count()) .collect(); // All facets should have the same number of vertices (dimension) @@ -4345,7 +4301,7 @@ mod tests { let hull = ConvexHull::try_from_triangulation(dt.as_triangulation()).unwrap(); // Get initial generation values - let initial_tds_generation = dt.as_triangulation().tds.generation(); + let initial_tds_generation = dt.topology_generation(); let initial_hull_generation = hull.cached_generation().load(Ordering::Acquire); test_debug!(" Initial TDS generation: {initial_tds_generation}"); @@ -4370,7 +4326,7 @@ mod tests { assert!(result1.is_ok(), "Initial visibility test should succeed"); // Cache should now be built, generations should still match - let post_cache_tds_gen = dt.as_triangulation().tds.generation(); + let post_cache_tds_gen = dt.topology_generation(); let post_cache_hull_gen = hull.cached_generation().load(Ordering::Acquire); test_debug!( @@ -4399,7 +4355,7 @@ mod tests { // Test TDS modification by adding a new vertex test_debug!(" Testing TDS modification and hull invalidation..."); - let old_generation = dt.as_triangulation().tds.generation(); + let old_generation = dt.topology_generation(); let stale_hull_gen = hull.cached_generation().load(Ordering::Acquire); // Add a new vertex - this will bump the generation @@ -4407,7 +4363,7 @@ mod tests { dt.insert_vertex(new_vertex) .expect("Failed to insert vertex into DelaunayTriangulation"); - let modified_tds_gen = dt.as_triangulation().tds.generation(); + let modified_tds_gen = dt.topology_generation(); test_debug!(" After TDS modification (added vertex):"); test_debug!(" TDS generation: {modified_tds_gen}"); test_debug!(" Hull cached generation: {stale_hull_gen}"); @@ -4511,7 +4467,7 @@ mod tests { // Generation should be updated to current TDS generation let final_hull_gen = new_hull.cached_generation().load(Ordering::Acquire); - let final_tds_gen = dt.as_triangulation().tds.generation(); + let final_tds_gen = dt.topology_generation(); assert_eq!( final_hull_gen, final_tds_gen, "Hull generation should match TDS generation after cache rebuild" @@ -4591,7 +4547,7 @@ mod tests { // First call should build the cache test_debug!(" Testing initial cache building..."); let cache1 = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) + .try_get_or_build_facet_cache(dt.tds()) .expect("Failed to build cache"); assert!( !cache1.is_empty(), @@ -4608,7 +4564,7 @@ mod tests { // Second call with same generation should reuse cache test_debug!(" Testing cache reuse with same generation..."); let cache2 = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) + .try_get_or_build_facet_cache(dt.tds()) .expect("Failed to reuse cache"); assert_eq!( cache1.len(), @@ -4632,14 +4588,14 @@ mod tests { // Modify triangulation by adding a vertex to trigger generation change test_debug!(" Testing cache invalidation with generation change..."); - let old_generation = dt.as_triangulation().tds.generation(); + let old_generation = dt.topology_generation(); // Add a new vertex to trigger generation bump let new_vertex = vertex!([0.5, 0.5, 0.5]).unwrap(); // Interior point dt.insert_vertex(new_vertex) .expect("Failed to insert vertex"); - let new_generation = dt.as_triangulation().tds.generation(); + let new_generation = dt.topology_generation(); assert!( new_generation > old_generation, "Generation should increase after adding vertex" @@ -4647,7 +4603,7 @@ mod tests { // Next call should rebuild cache due to generation change let cache3 = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) + .try_get_or_build_facet_cache(dt.tds()) .expect("Failed to rebuild cache"); // The cache content might be different since we added a vertex @@ -4688,7 +4644,7 @@ mod tests { // Test that cache contains keys derivable by the key derivation method test_debug!(" Testing cache-key derivation consistency..."); let cache = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) + .try_get_or_build_facet_cache(dt.tds()) .expect("Failed to build cache"); // For each facet in the hull, derive its key and check it exists in cache @@ -4696,18 +4652,13 @@ mod tests { for i in 0..hull.number_of_facets() { let facet_handle = hull.facet(i).unwrap(); // Create FacetView to get vertices - let facet_view = FacetView::try_new( - &dt.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt.facet_view(*facet_handle).unwrap(); let facet_vertices = facet_view_to_vertices(&facet_view); // Get vertex keys from vertices via TDS let facet_vertex_keys: Vec<_> = facet_vertices .iter() - .filter_map(|v| dt.as_triangulation().tds.vertex_key_from_uuid(&v.uuid())) + .filter_map(|v| dt.vertex_key_from_uuid(&v.uuid())) .collect(); let derived_key_result = checked_facet_key_from_vertex_keys::<3>(&facet_vertex_keys); @@ -5001,9 +4952,7 @@ mod tests { test_debug!(" Testing cache consistency after concurrent access..."); // Verify cache is in a consistent state after concurrent access - let cache = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) - .unwrap(); + let cache = hull.try_get_or_build_facet_cache(dt.tds()).unwrap(); assert!( !cache.is_empty(), "Cache should be populated after concurrent access" @@ -5039,9 +4988,7 @@ mod tests { // Build initial cache test_debug!(" Building initial cache..."); - let initial_cache = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) - .unwrap(); + let initial_cache = hull.try_get_or_build_facet_cache(dt.tds()).unwrap(); assert!( !initial_cache.is_empty(), "Initial cache should not be empty" @@ -5053,7 +5000,7 @@ mod tests { // Check initial generation let initial_gen = hull.cached_generation().load(Ordering::Acquire); - let expected_gen = dt.as_triangulation().tds.generation(); + let expected_gen = dt.topology_generation(); assert_eq!( initial_gen, expected_gen, "Initial cached generation should match TDS generation" @@ -5079,9 +5026,7 @@ mod tests { // Rebuild cache after invalidation test_debug!(" Rebuilding cache after invalidation..."); - let rebuilt_cache = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) - .unwrap(); + let rebuilt_cache = hull.try_get_or_build_facet_cache(dt.tds()).unwrap(); assert!( !rebuilt_cache.is_empty(), "Rebuilt cache should not be empty" @@ -5096,7 +5041,7 @@ mod tests { // Generation should be updated to TDS generation let final_gen = hull.cached_generation().load(Ordering::Acquire); - let tds_gen = dt.as_triangulation().tds.generation(); + let tds_gen = dt.topology_generation(); assert_eq!( final_gen, tds_gen, "Generation should match TDS generation after rebuild" @@ -5748,12 +5693,7 @@ mod tests { test_debug!(" Testing fallback with points at various distances..."); let facet_handle = hull.facet(0).unwrap(); - let facet_view = FacetView::try_new( - &dt.as_triangulation().tds, - facet_handle.simplex_key(), - facet_handle.facet_index(), - ) - .unwrap(); + let facet_view = dt.facet_view(*facet_handle).unwrap(); let test_facet_vertices = facet_view_to_vertices(&facet_view); // Test points at different distance scales @@ -6799,7 +6739,7 @@ mod tests { test_debug!(" Testing rapid generation changes..."); // Record initial generation - let initial_generation = dt.as_triangulation().tds.generation(); + let initial_generation = dt.topology_generation(); let initial_hull_generation = hull.cached_generation().load(Ordering::Acquire); test_debug!(" Initial TDS generation: {initial_generation}"); @@ -6810,7 +6750,7 @@ mod tests { let new_vertex = vertex!([>::from(i).mul_add(0.01, 0.1), 0.1, 0.1]).unwrap(); if dt.insert_vertex(new_vertex).is_ok() { - let current_gen = dt.as_triangulation().tds.generation(); + let current_gen = dt.topology_generation(); test_debug!(" After modification {i}: TDS generation = {current_gen}"); // Test that hull detects staleness @@ -6827,7 +6767,7 @@ mod tests { test_debug!(" Testing cache rebuild with high generation values..."); - let final_generation = dt.as_triangulation().tds.generation(); + let final_generation = dt.topology_generation(); test_debug!(" Final TDS generation: {final_generation}"); // Force cache rebuild @@ -6954,7 +6894,7 @@ mod tests { assert!(final_cache.is_some(), "Cache should exist at end of cycle"); let final_generation = hull.cached_generation().load(Ordering::Acquire); - let tds_generation = dt.as_triangulation().tds.generation(); + let tds_generation = dt.topology_generation(); assert_eq!( final_generation, tds_generation, "Hull generation should match TDS at end of cycle" @@ -7134,7 +7074,7 @@ mod tests { ]) .build() .unwrap(); - let initial_gen = dt.as_triangulation().tds.generation(); + let initial_gen = dt.topology_generation(); let hull = ConvexHull::try_from_triangulation(dt.as_triangulation()).unwrap(); // Verify hull is valid initially @@ -7146,7 +7086,7 @@ mod tests { // Step 2: Mutate triangulation (increases generation) dt.insert_vertex(vertex![0.5, 0.5, 0.5].unwrap()).unwrap(); - let new_gen = dt.as_triangulation().tds.generation(); + let new_gen = dt.topology_generation(); assert_ne!( initial_gen, new_gen, "Triangulation generation should increase after mutation" @@ -7235,8 +7175,8 @@ mod tests { let dt2: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices).build().unwrap(); assert_eq!( - dt1.as_triangulation().tds.generation(), - dt2.as_triangulation().tds.generation(), + dt1.topology_generation(), + dt2.topology_generation(), "regression setup needs matching generation counters" ); @@ -7275,8 +7215,8 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); let dt2 = dt1.clone(); assert_eq!( - dt1.as_triangulation().tds.generation(), - dt2.as_triangulation().tds.generation(), + dt1.topology_generation(), + dt2.topology_generation(), "regression setup needs matching generation counters" ); @@ -7315,9 +7255,7 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); let hull = ConvexHull::try_from_triangulation(dt.as_triangulation()).unwrap(); - let cache_before = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) - .unwrap(); + let cache_before = hull.try_get_or_build_facet_cache(dt.tds()).unwrap(); assert!(!cache_before.is_empty()); let creation_generation = hull @@ -7329,16 +7267,11 @@ mod tests { .creation_identity .get() .expect("constructed hull should capture TDS identity"); - assert_eq!(creation_generation, dt.as_triangulation().tds.generation()); - assert!(Arc::ptr_eq( - creation_identity, - dt.as_triangulation().tds.identity() - )); + assert_eq!(creation_generation, dt.topology_generation()); + assert!(Arc::ptr_eq(creation_identity, dt.tds().identity())); assert!(hull.is_valid_for_triangulation(dt.as_triangulation())); let duplicate_uuid = dt - .as_triangulation() - .tds .vertices() .next() .expect("tetrahedron should have vertices") @@ -7357,12 +7290,12 @@ mod tests { ); assert_eq!( - dt.as_triangulation().tds.generation(), + dt.topology_generation(), creation_generation, "failed insert rollback should restore the generation captured by the hull" ); assert!( - Arc::ptr_eq(creation_identity, dt.as_triangulation().tds.identity()), + Arc::ptr_eq(creation_identity, dt.tds().identity()), "failed insert rollback must preserve TDS identity for cache provenance" ); assert!( @@ -7370,9 +7303,7 @@ mod tests { "hull cache key should remain valid after failed insert rollback" ); - let cache_after = hull - .try_get_or_build_facet_cache(&dt.as_triangulation().tds) - .unwrap(); + let cache_after = hull.try_get_or_build_facet_cache(dt.tds()).unwrap(); assert!( Arc::ptr_eq(&cache_before, &cache_after), "facet cache should be reused when rollback restores the same generation" diff --git a/src/geometry/kernel.rs b/src/geometry/kernel.rs index d15095b3..07175a07 100644 --- a/src/geometry/kernel.rs +++ b/src/geometry/kernel.rs @@ -306,8 +306,10 @@ pub trait Kernel: Clone { /// flip-based Delaunay repair — should bound their kernel parameter with this /// trait: /// -/// ```rust,ignore -/// fn repair(kernel: &K) +/// ```rust +/// use delaunay::prelude::geometry::ExactPredicates; +/// +/// fn repair(_kernel: &K) /// where /// K: ExactPredicates, /// { /* ... */ } diff --git a/src/geometry/robust_predicates.rs b/src/geometry/robust_predicates.rs index 8dbd9824..bd243bf5 100644 --- a/src/geometry/robust_predicates.rs +++ b/src/geometry/robust_predicates.rs @@ -17,8 +17,6 @@ use crate::geometry::point::Point; use crate::geometry::sos::{sos_insphere_sign, sos_orientation_sign}; use crate::geometry::traits::coordinate::CoordinateConversionError; use core::{cmp::Ordering, hint::cold_path}; -#[cfg(test)] -use std::cell::Cell; use std::sync::LazyLock; static PROCESS_WIDE_STRICT_INSPHERE_CONSISTENCY: LazyLock = @@ -26,8 +24,8 @@ static PROCESS_WIDE_STRICT_INSPHERE_CONSISTENCY: LazyLock = #[cfg(test)] thread_local! { - static STRICT_INSPHERE_CONSISTENCY_TEST_OVERRIDE: Cell> = - const { Cell::new(None) }; + static STRICT_INSPHERE_CONSISTENCY_TEST_OVERRIDE: std::cell::Cell> = + const { std::cell::Cell::new(None) }; } #[cfg(test)] @@ -51,7 +49,7 @@ impl Drop for StrictInsphereConsistencyOverrideGuard { /// branch coverage does not depend on process-wide environment mutation. fn strict_insphere_consistency_enabled() -> bool { #[cfg(test)] - if let Some(enabled) = STRICT_INSPHERE_CONSISTENCY_TEST_OVERRIDE.with(Cell::get) { + if let Some(enabled) = STRICT_INSPHERE_CONSISTENCY_TEST_OVERRIDE.with(std::cell::Cell::get) { return enabled; } diff --git a/src/geometry/util/measures.rs b/src/geometry/util/measures.rs index 2a9cd690..1d7e4d30 100644 --- a/src/geometry/util/measures.rs +++ b/src/geometry/util/measures.rs @@ -789,6 +789,8 @@ fn facet_measure_gram_matrix( /// # #[error(transparent)] /// # Tds(#[from] delaunay::prelude::tds::TdsError), /// # #[error(transparent)] +/// # Query(#[from] delaunay::prelude::query::QueryError), +/// # #[error(transparent)] /// # Facet(#[from] delaunay::prelude::tds::FacetError), /// # #[error(transparent)] /// # SurfaceMeasure(#[from] delaunay::prelude::geometry::SurfaceMeasureError), @@ -804,10 +806,9 @@ fn facet_measure_gram_matrix( /// delaunay::vertex![0.0, 0.0, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let tds = dt.tds(); /// /// // Get boundary facets as FacetViews -/// let boundary_facets = tds.one_sided_facets()?.collect::, _>>()?; +/// let boundary_facets = dt.boundary_facets()?.collect::, _>>()?; /// /// // Calculate surface area /// let surface_area = surface_measure(&boundary_facets)?; diff --git a/src/geometry/util/triangulation_generation.rs b/src/geometry/util/triangulation_generation.rs index e5887282..581c698d 100644 --- a/src/geometry/util/triangulation_generation.rs +++ b/src/geometry/util/triangulation_generation.rs @@ -522,9 +522,9 @@ where /// Some(789) /// ); /// -/// // Access the underlying Tds if needed +/// // Access triangulation-level counts /// let dt = triangulation_3d?; -/// let vertex_count = dt.tds().number_of_vertices(); +/// let vertex_count = dt.number_of_vertices(); /// # Ok(()) /// # } /// ``` diff --git a/src/lib.rs b/src/lib.rs index ba77dc46..8f4f2a59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,9 +51,9 @@ //! |---|---| //! | Construct/configure a Delaunay triangulation | `use delaunay::prelude::construction::*` | //! | Build/validate/repair generic triangulations | `use delaunay::prelude::triangulation::*` | -//! | Low-level incremental insertion building blocks | `use delaunay::prelude::insertion::*` | +//! | Incremental insertion diagnostics and result types | `use delaunay::prelude::insertion::*` | //! | Post-construction vertex deletion errors and keys | `use delaunay::prelude::deletion::*` | -//! | Read-only queries, traversal, simplex barycenters, convex hull | `use delaunay::prelude::query::*` | +//! | Read-only queries, traversal, ridge views, simplex barycenters, convex hull | `use delaunay::prelude::query::*` | //! | Point location and conflict-region algorithms | `use delaunay::prelude::algorithms::*` | //! | Geometry helpers, simplex embeddings, coordinate ranges, predicates, points | `use delaunay::prelude::geometry::*` | //! | Random points / triangulations for examples and tests | `use delaunay::prelude::generators::*` | @@ -62,7 +62,7 @@ //! | Delaunay repair and flip-based Level 5 validation | `use delaunay::prelude::repair::*` | //! | Delaunayize workflow (repair + flip) | `use delaunay::prelude::delaunayize::*` | //! | Construction telemetry diagnostics | `use delaunay::prelude::diagnostics::*` | -//! | Validation policies, errors, reports, and Level 5 diagnostics | `use delaunay::prelude::validation::*` | +//! | Validation policies, errors, reports, PL-manifold link errors, and Level 5 diagnostics | `use delaunay::prelude::validation::*` | //! | Topology validation, Euler characteristic, ridge queries | `use delaunay::prelude::topology::validation::*` | //! | Topological spaces, topology traits, lifted toroidal IDs | `use delaunay::prelude::topology::spaces::*` | //! | Low-level TDS simplices, facets, keys | `use delaunay::prelude::tds::*` | @@ -113,8 +113,8 @@ //! ]; //! let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; //! -//! // Levels 1–2: elements + structural (TDS) -//! assert!(dt.tds().validate().is_ok()); +//! // Levels 1–2: elements + structural +//! assert!(dt.validate_structure().is_ok()); //! //! // Levels 1–3: elements + structural + topology //! assert!(dt.as_triangulation().validate().is_ok()); @@ -266,8 +266,8 @@ //! - Level 1 (elements / `Vertex` + `Simplex`): `Vertex::is_valid()` / //! `Simplex::is_valid()` for fast checks, or `vertex_report()` / //! `simplex_report()` for element-local diagnostics. -//! - Level 2 (structural / `Tds`): `dt.tds().is_valid()` for a quick check, or `dt.tds().validate()` for -//! Levels 1–2. +//! - Level 2 (structural / `Tds`): `dt.is_valid_structure()` for a quick check, or +//! `dt.validate_structure()` for Levels 1–2. //! - Level 3 (topology / `Triangulation`): `dt.as_triangulation().is_valid_topology()` for topology-only checks, or //! `dt.as_triangulation().validate()` for Levels 1–3. //! - Level 4 (embedding / `Triangulation`): `dt.as_triangulation().validate_embedding()` for cumulative @@ -801,8 +801,7 @@ pub use crate::core::algorithms::incremental_insertion::{ HullExtensionReason, InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionTopologyValidationContext, NeighborRebuildError, NeighborWiringError, - SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, extend_hull, - fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, wire_cavity_neighbors, + SpatialIndexConstructionFailure, TdsConstructionFailure, TdsValidationFailure, }; pub use crate::core::algorithms::pl_manifold_repair::{ PlManifoldRepairError, PlManifoldRepairStage, PlManifoldRepairStats, @@ -943,7 +942,7 @@ pub fn try_vertices_from_points( /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology())?; +/// let result = dt.euler_check()?; /// assert_eq!(result.chi, 1); // Tetrahedron has χ = 1 /// assert!(result.is_valid()); /// # Ok(()) @@ -1112,7 +1111,7 @@ pub mod tds { /// # } /// ``` pub mod algorithms { - #[cfg(any(feature = "diagnostics", all(test, debug_assertions)))] + #[cfg(feature = "diagnostics")] pub use crate::core::algorithms::locate::verify_conflict_region_completeness; pub use crate::core::algorithms::locate::{ ConflictError, InternalInconsistencySite, LocateError, LocateFallback, @@ -1158,6 +1157,7 @@ pub mod query { extract_hull_facet_set, extract_vertex_coordinate_set, format_jaccard_report, jaccard_distance, jaccard_index, measure_with_result, }; + pub use crate::flips::RidgeHandle; pub use crate::geometry::Point; pub use crate::geometry::algorithms::convex_hull::{ ConvexHull, ConvexHullConstructionError, ConvexHullValidationError, @@ -1173,6 +1173,9 @@ pub mod query { Simplex, SimplexFacetsIter, SimplexKey, SimplexNeighborIndex, TopologyIndexBuildError, TriangulationAdjacency, Vertex, VertexKey, }; + pub use crate::topology::ridge::{ + RidgeCandidate, RidgeCandidateError, RidgeLinkView, RidgeQuery, RidgeView, + }; pub use crate::{DelaunayTriangulation, Triangulation}; pub use crate::{SimplexBarycenterError, SimplexDataFillError}; } @@ -1183,7 +1186,8 @@ pub mod prelude { // Re-export the public low-level facades. pub use crate::query::{ DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, - FacetIncidenceAnalysis, QueryError, SimplexBarycenterError, SimplexDataFillError, + FacetIncidenceAnalysis, QueryError, RidgeCandidate, RidgeCandidateError, RidgeHandle, + RidgeLinkView, RidgeQuery, RidgeView, SimplexBarycenterError, SimplexDataFillError, }; pub use crate::tds::*; pub use crate::vertex; @@ -1401,14 +1405,16 @@ pub mod prelude { AllFacetsIter, BoundaryFacetsIter, DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, EdgeIndex, EdgeKey, EdgeKeyError, EdgeView, FacetIncidenceAnalysis, FacetIncidenceView, FacetToSimplicesIndex, FacetView, - IncidenceView, OneSidedFacetsIter, QueryError, SimplexFacetsIter, SimplexNeighborIndex, - TopologyIndexBuildError, TriangulationAdjacency, + IncidenceView, OneSidedFacetsIter, QueryError, RidgeCandidate, RidgeCandidateError, + RidgeHandle, RidgeLinkView, RidgeQuery, RidgeView, SimplexFacetsIter, + SimplexNeighborIndex, TopologyIndexBuildError, TriangulationAdjacency, }; pub use crate::tds::{ FacetHandle, InvariantError, NeighborSlot, Simplex, SimplexKey, Tds, TdsConstructionError, TdsError, TdsErrorKind, TdsMutationError, TriangulationValidationErrorKind, Vertex, VertexKey, }; + pub use crate::topology::manifold::ManifoldError; pub use crate::vertex; pub use crate::{ InsertionError, PeriodicDomainPeriodError, SpatialIndexConstructionFailure, @@ -1490,7 +1496,7 @@ pub mod prelude { /// let Some((simplex_key, _)) = dt.simplices().next() else { /// return Ok(()); /// }; - /// let Ok(facet) = FacetHandle::try_new(dt.tds(), simplex_key, 0) else { + /// let Ok(facet) = dt.facet_handle(simplex_key, 0) else { /// return Ok(()); /// }; /// if dt.flip_k2(facet).is_err() { @@ -1514,19 +1520,15 @@ pub mod prelude { pub use crate::vertex; } - /// Incremental insertion building blocks and diagnostics. + /// Incremental insertion diagnostics and result types. pub mod insertion { - pub use crate::collections::SimplexKeyBuffer; - pub use crate::tds::FacetHandle; - pub use crate::tds::{SimplexKey, Tds, TdsMutationError, VertexKey}; pub use crate::{ CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionTopologyValidationContext, NeighborRebuildError, NeighborWiringError, SpatialIndexConstructionFailure, TdsConstructionFailure, - TdsValidationFailure, extend_hull, fill_cavity, repair_neighbor_pointers, - repair_neighbor_pointers_local, wire_cavity_neighbors, + TdsValidationFailure, }; pub use crate::{InsertionOutcome, InsertionResult, InsertionStatistics}; } @@ -1591,8 +1593,7 @@ pub mod prelude { FlipNeighborRepairDiagnostics, FlipNeighborRepairFailure, FlipNeighborWiringError, FlipOrientationCheckStage, FlipPredicateError, FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, - TriangleHandleError, verify_delaunay_for_triangulation, - verify_delaunay_via_flip_predicates, + TriangleHandleError, }; pub use crate::repair::{ DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicSeeds, @@ -1636,6 +1637,7 @@ pub mod prelude { /// assert!(cadence.should_validate(32)); /// ``` pub mod validation { + pub use crate::topology::manifold::ManifoldError; pub use crate::validation::*; pub use crate::{ DelaunayTriangulationValidationError, DelaunayVerificationError, @@ -1859,13 +1861,14 @@ pub mod prelude { } /// Convenience re-exports for common **read-only** workflows (topology traversal, adjacency, - /// simplex barycenters, convex-hull extraction, and common input types). + /// ridge views, simplex barycenters, convex-hull extraction, and common input types). /// /// This is useful if you want a smaller import surface than `delaunay::prelude::*`, /// while still having access to the key public APIs typically used in docs/tests/examples/benches. /// /// Includes: - /// - Topology traversal: [`DelaunayTriangulation::edges`], [`DelaunayTriangulation::incident_edges`], + /// - Topology traversal: [`DelaunayTriangulation::facets`], [`DelaunayTriangulation::ridges`], + /// [`DelaunayTriangulation::edges`], [`DelaunayTriangulation::incident_edges`], /// [`DelaunayTriangulation::simplex_neighbors`] /// - Fast repeated queries: [`DelaunayTriangulation::incidence`], [`DelaunayTriangulation::build_edge_index`], /// [`DelaunayTriangulation::build_simplex_neighbor_index`], and composite @@ -1910,8 +1913,9 @@ pub mod prelude { pub use crate::query::{ AllFacetsIter, BoundaryFacetsIter, DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, FacetIncidenceAnalysis, FacetView, - OneSidedFacetsIter, QueryError, Simplex, SimplexBarycenterError, SimplexDataFillError, - SimplexFacetsIter, Vertex, + OneSidedFacetsIter, QueryError, RidgeCandidate, RidgeCandidateError, RidgeHandle, + RidgeLinkView, RidgeQuery, RidgeView, Simplex, SimplexBarycenterError, + SimplexDataFillError, SimplexFacetsIter, Vertex, }; // Read-only predicates (useful in benchmarks / lightweight geometry checks) @@ -2062,8 +2066,7 @@ mod tests { triangulation::Triangulation, vertex::Vertex, }, geometry::{ - Point, algorithms::convex_hull::ConvexHull, kernel::AdaptiveKernel, kernel::FastKernel, - util::CircumcenterError, + Point, algorithms::convex_hull::ConvexHull, kernel::FastKernel, util::CircumcenterError, }, is_normal, prelude::delaunayize::{ @@ -2075,7 +2078,6 @@ mod tests { DelaunayCheckPolicy, DelaunayRepairError, DelaunayRepairOutcome, DelaunayRepairPolicy, DelaunayRepairStats, DelaunayTriangulation as RepairDelaunayTriangulation, FlipContextError, FlipError, RepairQueueOrder, TopologyGuarantee, - verify_delaunay_for_triangulation, verify_delaunay_via_flip_predicates, }, prelude::*, vertex, @@ -2195,10 +2197,9 @@ mod tests { RepairDelaunayTriangulation::builder(&vertices) .build() .unwrap(); - let kernel = AdaptiveKernel::::new(); - assert!(verify_delaunay_for_triangulation(dt.as_triangulation()).is_ok()); - assert!(verify_delaunay_via_flip_predicates(dt.tds(), &kernel).is_ok()); + assert!(dt.verify_via_flip_predicates().is_ok()); + assert!(dt.is_valid_delaunay().is_ok()); let stats = DelaunayRepairStats::default(); let outcome = DelaunayRepairOutcome { @@ -2319,16 +2320,14 @@ mod tests { assert_eq!(dt.number_of_vertices(), 4); assert_eq!(dt.number_of_simplices(), 1); - // Access Triangulation, Tds, Simplex types + // Access Triangulation and simplex query types let tri = dt.as_triangulation(); assert_eq!(tri.number_of_vertices(), 4); - - let tds = &tri.tds; - assert_eq!(tds.number_of_simplices(), 1); + assert_eq!(tri.number_of_simplices(), 1); // Iterate over simplices for (simplex_key, _simplex) in tri.simplices() { - assert!(tds.simplex(simplex_key).is_some()); + assert!(tri.simplex(simplex_key).is_some()); } } @@ -2343,10 +2342,9 @@ mod tests { let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - // Test locate function with kernel - let kernel = FastKernel::::new(); + // Test locate via the owning triangulation. let query_point = Point::try_new([0.3, 0.3]).expect("finite point coordinates"); - let result = locate(dt.tds(), &kernel, &query_point, None); + let result = dt.locate(&query_point, None); assert!(result.is_ok()); // Result should be a LocateResult @@ -2360,7 +2358,7 @@ mod tests { // Test outside point let outside_point = Point::try_new([10.0, 10.0]).expect("finite point coordinates"); - let result = locate(dt.tds(), &kernel, &outside_point, None); + let result = dt.locate(&outside_point, None); assert!(result.is_ok()); } diff --git a/src/topology/characteristics/euler.rs b/src/topology/characteristics/euler.rs index 841bccc6..714e15b8 100644 --- a/src/topology/characteristics/euler.rs +++ b/src/topology/characteristics/euler.rs @@ -7,7 +7,6 @@ //! //! ```rust //! use delaunay::prelude::*; -//! use delaunay::prelude::topology::validation::euler; //! //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { @@ -27,9 +26,8 @@ //! ]; //! let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; //! -//! let counts = euler::count_simplices(dt.tds())?; -//! let chi = euler::euler_characteristic(&counts); -//! assert_eq!(chi, 1); // Single tetrahedron has χ = 1 +//! let result = dt.euler_check()?; +//! assert_eq!(result.chi, 1); // Single tetrahedron has χ = 1 //! # Ok(()) //! # } //! ``` @@ -207,7 +205,6 @@ pub enum TopologyClassification { /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::topology::validation::euler; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -226,7 +223,7 @@ pub enum TopologyClassification { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let counts = euler::count_simplices(dt.tds())?; +/// let counts = dt.simplex_counts()?; /// assert_eq!(counts.count(0), 3); // 3 vertices /// assert_eq!(counts.count(1), 3); // 3 edges /// assert_eq!(counts.count(2), 1); // 1 face @@ -479,7 +476,7 @@ fn insert_simplices_of_size( /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let boundary_counts = euler::count_boundary_simplices(dt.tds(), dt.global_topology())?; +/// let boundary_counts = dt.boundary_simplex_counts()?; /// let boundary_chi = euler::euler_characteristic(&boundary_counts); /// assert_eq!(boundary_chi, 2); // S² has χ = 2 /// # Ok(()) @@ -750,7 +747,7 @@ pub(crate) fn triangulated_surface_boundary_component_count( /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::topology::validation::{classify_triangulation, TopologyClassification}; +/// use delaunay::prelude::topology::validation::TopologyClassification; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -770,7 +767,7 @@ pub(crate) fn triangulated_surface_boundary_component_count( /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let classification = classify_triangulation(dt.tds(), dt.global_topology())?; +/// let classification = dt.topology_classification_for(dt.global_topology())?; /// assert_eq!(classification, TopologyClassification::SingleSimplex(3)); /// # Ok(()) /// # } diff --git a/src/topology/characteristics/validation.rs b/src/topology/characteristics/validation.rs index 77381b73..4389003c 100644 --- a/src/topology/characteristics/validation.rs +++ b/src/topology/characteristics/validation.rs @@ -24,7 +24,6 @@ use crate::topology::{ /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::topology::validation; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -44,7 +43,7 @@ use crate::topology::{ /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology())?; +/// let result = dt.euler_check()?; /// assert_eq!(result.chi, 1); /// assert!(result.is_valid()); /// # Ok(()) @@ -121,7 +120,6 @@ impl TopologyCheckResult { /// /// ```rust /// use delaunay::prelude::*; -/// use delaunay::prelude::topology::validation; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -140,7 +138,7 @@ impl TopologyCheckResult { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology())?; +/// let result = dt.euler_check()?; /// assert_eq!(result.chi, 1); /// assert_eq!(result.counts.count(0), 3); // 3 vertices /// assert_eq!(result.counts.count(1), 3); // 3 edges diff --git a/src/topology/manifold.rs b/src/topology/manifold.rs index 33a72791..f02760d6 100644 --- a/src/topology/manifold.rs +++ b/src/topology/manifold.rs @@ -341,7 +341,7 @@ pub enum BoundaryFacetClassification { /// delaunay::vertex![0.5, 1.0]?, /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; -/// let facet_index = dt.tds().build_facet_to_simplices_index()?; +/// let facet_index = dt.facet_incidence_index()?; /// /// let Some(incidence) = facet_index.iter().find(|incidence| incidence.is_one_sided()) /// else { diff --git a/src/topology/ridge.rs b/src/topology/ridge.rs index 2c93380d..1bbbfa40 100644 --- a/src/topology/ridge.rs +++ b/src/topology/ridge.rs @@ -77,7 +77,7 @@ pub enum RidgeCandidateError { /// ```rust /// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::{ -/// ManifoldError, RidgeCandidate, RidgeCandidateError, ridge_star_simplices, +/// ManifoldError, RidgeCandidate, RidgeCandidateError, /// }; /// /// # #[derive(Debug, thiserror::Error)] @@ -100,9 +100,9 @@ pub enum RidgeCandidateError { /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// // In 2D, a ridge is a vertex because it has arity D - 1. -/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; -/// let star = ridge_star_simplices(dt.tds(), &ridge)?; -/// let view = ridge.view(dt.tds())?; +/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; +/// let star = dt.ridge_star_simplices(&ridge)?; +/// let view = dt.ridge_view(&ridge)?; /// /// assert_eq!(ridge.as_slice(), view.vertex_keys()); /// assert_eq!(star.as_slice(), view.incident_simplices()); @@ -110,7 +110,7 @@ pub enum RidgeCandidateError { /// # } /// ``` #[must_use] -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RidgeCandidate { vertices: VertexKeyBuffer, } @@ -152,7 +152,7 @@ impl RidgeCandidate { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.tds().vertex_keys().take(2))?; + /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(2))?; /// assert_eq!(ridge.as_slice().len(), 2); /// # Ok(()) /// # } @@ -214,7 +214,7 @@ impl RidgeCandidate { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.tds().vertex_keys().take(2))?; + /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(2))?; /// assert_eq!(ridge.as_slice().len(), 2); /// # Ok(()) /// # } @@ -252,7 +252,7 @@ impl RidgeCandidate { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.tds().vertex_keys().take(2))?; + /// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(2))?; /// assert_eq!(ridge.iter().count(), ridge.as_slice().len()); /// # Ok(()) /// # } @@ -297,8 +297,8 @@ impl RidgeCandidate { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let query = ridge.query(dt.tds())?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let query = dt.ridge_query(&ridge)?; /// assert_eq!(query.vertex_keys(), ridge.as_slice()); /// # Ok(()) /// # } @@ -348,8 +348,8 @@ impl RidgeCandidate { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let view = ridge.view(dt.tds())?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let view = dt.ridge_view(&ridge)?; /// assert!(!view.incident_simplices().is_empty()); /// # Ok(()) /// # } @@ -415,8 +415,8 @@ impl<'tds, U, V, const D: usize> RidgeQuery<'tds, U, V, D> { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let query = RidgeQuery::try_new(dt.tds(), ridge)?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let query = dt.ridge_query(&ridge)?; /// assert_eq!(query.vertices().len(), 1); /// # Ok(()) /// # } @@ -436,13 +436,6 @@ impl<'tds, U, V, const D: usize> RidgeQuery<'tds, U, V, D> { }) } - /// Returns the borrowed TDS backing this query. - #[inline] - #[must_use] - pub const fn tds(&self) -> &'tds Tds { - self.tds - } - /// Returns the validated ridge candidate represented by this query. #[inline] pub const fn ridge_candidate(&self) -> &RidgeCandidate { @@ -508,8 +501,8 @@ impl<'tds, U, V, const D: usize> RidgeQuery<'tds, U, V, D> { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let query = ridge.query(dt.tds())?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let query = dt.ridge_query(&ridge)?; /// let links = query.links()?; /// assert!(!links.is_empty()); /// # Ok(()) @@ -601,8 +594,8 @@ impl<'tds, U, V, const D: usize> RidgeView<'tds, U, V, D> { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let view = RidgeView::try_new(dt.tds(), ridge)?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let view = dt.ridge_view(&ridge)?; /// assert_eq!(view.vertices().len(), 1); /// # Ok(()) /// # } @@ -626,13 +619,6 @@ impl<'tds, U, V, const D: usize> RidgeView<'tds, U, V, D> { }) } - /// Returns the borrowed TDS backing this view. - #[inline] - #[must_use] - pub const fn tds(&self) -> &'tds Tds { - self.tds - } - /// Returns the validated ridge candidate represented by this view. #[inline] pub const fn ridge_candidate(&self) -> &RidgeCandidate { @@ -698,8 +684,8 @@ impl<'tds, U, V, const D: usize> RidgeView<'tds, U, V, D> { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// - /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; - /// let view = ridge.view(dt.tds())?; + /// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; + /// let view = dt.ridge_view(&ridge)?; /// let links = view.links()?; /// assert_eq!(links[0].quotient_ridge_candidate(), view.ridge_candidate()); /// # Ok(()) @@ -779,15 +765,14 @@ impl Eq for RidgeView<'_, U, V, D> {} /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; -/// let view = ridge.view(dt.tds())?; +/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; +/// let view = dt.ridge_view(&ridge)?; /// let links = view.links()?; /// let link = &links[0]; /// -/// assert_eq!(link.tds().number_of_vertices(), dt.tds().number_of_vertices()); +/// assert_eq!(link.incident_simplices(), view.incident_simplices()); /// assert_eq!(link.quotient_ridge_candidate(), view.ridge_candidate()); /// assert_eq!(link.lifted_ridge_vertices().len(), ridge.as_slice().len()); -/// assert_eq!(link.incident_simplices(), view.incident_simplices()); /// assert!(!link.edges().is_empty()); /// # Ok(()) /// # } @@ -801,14 +786,7 @@ pub struct RidgeLinkView<'tds, U, V, const D: usize> { link_edges: SmallBuffer, } -impl<'tds, U, V, const D: usize> RidgeLinkView<'tds, U, V, D> { - /// Returns the borrowed TDS backing this link view. - #[inline] - #[must_use] - pub const fn tds(&self) -> &'tds Tds { - self.tds - } - +impl RidgeLinkView<'_, U, V, D> { /// Returns the quotient-space ridge candidate that produced this link. #[inline] pub const fn quotient_ridge_candidate(&self) -> &RidgeCandidate { @@ -1052,8 +1030,8 @@ pub(crate) fn simplex_star_simplices( /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// /// // In 3D, a ridge is an edge because it has arity D - 1. -/// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.tds().vertex_keys().take(2))?; -/// let star = ridge_star_simplices(dt.tds(), &ridge)?; +/// let ridge = RidgeCandidate::<3>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(2))?; +/// let star = dt.ridge_star_simplices(&ridge)?; /// assert!(!star.is_empty()); /// # Ok(()) /// # } @@ -2133,20 +2111,16 @@ mod tests { }; let expected: SimplexKeySet = [c0123, c0134, c0142].into_iter().collect(); - let tds_ptr = ptr::from_ref(&tds); - assert!(ptr::eq(ptr::from_ref(ridge_query.tds()), tds_ptr)); assert_eq!(ridge_query.ridge_candidate(), &ridge_candidate); assert_eq!(ridge_query.vertex_keys(), ridge_candidate.as_slice()); assert_eq!(star_set, expected); assert_eq!(query_star_set, expected); assert_eq!(view_star_set, expected); - assert!(ptr::eq(ptr::from_ref(ridge_view.tds()), tds_ptr)); assert_eq!(ridge_view.ridge_candidate(), &ridge_candidate); assert_eq!(ridge_view.vertex_keys(), ridge_candidate.as_slice()); assert_eq!(query_vertex_uuids, ridge_vertex_uuids); assert_eq!(ridge_vertex_uuids.len(), 2); assert_eq!(ridge_links.len(), 1); - assert!(ptr::eq(ptr::from_ref(ridge_link.tds()), tds_ptr)); assert_eq!(ridge_link.quotient_ridge_candidate(), &ridge_candidate); assert_eq!(ridge_link.lifted_ridge_vertices().len(), 2); assert_eq!( diff --git a/src/topology/spaces/toroidal.rs b/src/topology/spaces/toroidal.rs index 887ec58a..1d4d1fe7 100644 --- a/src/topology/spaces/toroidal.rs +++ b/src/topology/spaces/toroidal.rs @@ -65,8 +65,8 @@ use std::{ /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; -/// let view = ridge.view(dt.tds())?; +/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; +/// let view = dt.ridge_view(&ridge)?; /// let links = view.links()?; /// let Some(edge) = links.first().and_then(|link| link.edges().first()) else { /// return Ok(()); @@ -159,8 +159,8 @@ impl LiftedVertexId { /// ]; /// let dt = DelaunayTriangulationBuilder::new(&vertices).build()?; /// -/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.tds().vertex_keys().take(1))?; -/// let view = ridge.view(dt.tds())?; +/// let ridge = RidgeCandidate::<2>::try_from_vertices(dt.vertices().map(|(key, _)| key).take(1))?; +/// let view = dt.ridge_view(&ridge)?; /// let links = view.links()?; /// let Some(edge) = links.first().and_then(|link| link.edges().first()) else { /// return Ok(()); diff --git a/tests/README.md b/tests/README.md index 7059dd97..8cf879fb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -610,8 +610,8 @@ use delaunay::prelude::query::{ ```rust use delaunay::assert_jaccard_gte; -let before = extract_vertex_coordinate_set(&tds_before); -let after = extract_vertex_coordinate_set(&tds_after); +let before = extract_vertex_coordinate_set(tri_before); +let after = extract_vertex_coordinate_set(tri_after); // With custom label (4-arg form) assert_jaccard_gte!( @@ -662,9 +662,9 @@ println!("{}", report); use delaunay::assert_jaccard_gte; use delaunay::prelude::query::extract_vertex_coordinate_set; -let original_coords = extract_vertex_coordinate_set(&tds); +let original_coords = extract_vertex_coordinate_set(tri); // ... perform operation (serialization, transformation, etc.) ... -let result_coords = extract_vertex_coordinate_set(&tds_after); +let result_coords = extract_vertex_coordinate_set(tri_after); assert_jaccard_gte!( &original_coords, @@ -679,8 +679,8 @@ assert_jaccard_gte!( ```rust use delaunay::prelude::query::extract_edge_set; -let edges_a = extract_edge_set(&tds_a); -let edges_b = extract_edge_set(&tds_b); +let edges_a = extract_edge_set(tri_a); +let edges_b = extract_edge_set(tri_b); assert_jaccard_gte!( &edges_a, diff --git a/tests/benchmark_flip_fixtures.rs b/tests/benchmark_flip_fixtures.rs index e836a7fc..30c4fae6 100644 --- a/tests/benchmark_flip_fixtures.rs +++ b/tests/benchmark_flip_fixtures.rs @@ -19,7 +19,7 @@ mod flip_workflows; use std::assert_matches; -use delaunay::flips::{FacetHandle, FlipError, RidgeHandle}; +use delaunay::flips::FlipError; use delaunay::prelude::construction::{ DelaunayConstructionFailure, DelaunayConstructionRetryFailure, DelaunayTriangulationConstructionError, @@ -41,11 +41,12 @@ use flip_fixtures::{ #[cfg(feature = "slow-tests")] use flip_workflows::verify_k3_roundtrip; use flip_workflows::{ - CandidateFilter, FlipTriangulation, FlipWorkflowError, assert_same_topology, build_flip_dt, - facet_support_touches_adversarial_feature, flippable_k2_facet, flippable_k3_ridge, forward_k2, - forward_k3, largest_volume_simplex, ridge_support_touches_adversarial_feature, roundtrip_k1, - simplex_touches_adversarial_feature, snapshot_topology, verify_k1_roundtrip, - verify_k2_roundtrip, + CandidateFilter, FlipMoveKind, FlipTriangulation, FlipWorkflowContext, FlipWorkflowError, + assert_same_topology, build_flip_dt, facet_support_touches_adversarial_feature, + flippable_k2_facet, flippable_k3_ridge, forward_k2, largest_volume_simplex, + ridge_support_touches_adversarial_feature, roundtrip_k1, simplex_touches_adversarial_feature, + snapshot_topology, verify_k1_roundtrip, verify_k2_forward, verify_k2_roundtrip, + verify_k3_forward, }; #[cfg(feature = "slow-tests")] @@ -282,7 +283,8 @@ fn invalid_facet_support_returns_specific_error() { .simplices() .next() .expect("stable 2D fixture should contain a simplex"); - let err = FacetHandle::try_new(base_dt.tds(), simplex_key, u8::MAX) + let err = base_dt + .facet_handle(simplex_key, u8::MAX) .expect_err("invalid facet index should be rejected at construction"); assert_eq!( err, @@ -301,7 +303,8 @@ fn invalid_ridge_support_returns_specific_error() { .simplices() .next() .expect("stable 3D fixture should contain a simplex"); - let err = RidgeHandle::try_new(base_dt.tds(), simplex_key, u8::MAX, u8::MAX) + let err = base_dt + .ridge_handle(simplex_key, u8::MAX, u8::MAX) .expect_err("invalid ridge indices should be rejected at construction"); assert_matches!( err, @@ -313,7 +316,8 @@ fn invalid_ridge_support_returns_specific_error() { } if observed_simplex == simplex_key ); - let err = RidgeHandle::try_new(base_dt.tds(), simplex_key, 0, 0) + let err = base_dt + .ridge_handle(simplex_key, 0, 0) .expect_err("duplicate ridge indices should be rejected at construction"); assert_matches!( err, @@ -334,7 +338,8 @@ fn forward_flip_failure_preserves_typed_source() { .simplices() .next() .expect("stable 2D fixture should contain a simplex"); - let err = FacetHandle::try_new(base_dt.tds(), simplex_key, u8::MAX) + let err = base_dt + .facet_handle(simplex_key, u8::MAX) .expect_err("invalid k=2 facet should fail before flip execution"); assert_eq!( err, @@ -355,8 +360,12 @@ fn topology_mismatch_reports_jaccard_diagnostics() { let mut flipped = base_dt; forward_k2(&mut flipped, facet).expect("2D k=2 forward flip should succeed"); - let err = assert_same_topology(&flipped, &before, "2D k=2 forward mismatch") - .expect_err("forward-only k=2 flip should not match the original topology"); + let err = assert_same_topology( + &flipped, + &before, + FlipWorkflowContext::forward_only::<2>(FlipMoveKind::K2), + ) + .expect_err("forward-only k=2 flip should not match the original topology"); match err { FlipWorkflowError::TopologyMismatch { context, @@ -364,7 +373,10 @@ fn topology_mismatch_reports_jaccard_diagnostics() { simplex_report, .. } => { - assert_eq!(context, "2D k=2 forward mismatch"); + assert_eq!( + context, + FlipWorkflowContext::forward_only::<2>(FlipMoveKind::K2) + ); assert!( vertex_report.contains("Jaccard Similarity Report"), "missing vertex Jaccard report in topology mismatch diagnostics: {vertex_report}" @@ -392,7 +404,7 @@ fn verify_2d_fixture(points: &[[f64; 2]], filter: CandidateFilter) { "2D adversarial k=1 support should touch an adversarial fixture feature" ); } - verify_k1_roundtrip(&base_dt, simplex_key, "2D k=1 n=1 ergodicity roundtrip") + verify_k1_roundtrip(&base_dt, simplex_key) .expect("2D k=1 roundtrip should recover the same triangulation"); let facet = flippable_k2_facet(&base_dt, false, filter) @@ -404,10 +416,7 @@ fn verify_2d_fixture(points: &[[f64; 2]], filter: CandidateFilter) { "2D adversarial k=2 support should touch an adversarial fixture feature" ); } - let mut k2 = base_dt; - forward_k2(&mut k2, facet).expect("2D benchmark k=2 forward flip should succeed"); - k2.as_triangulation() - .validate() + verify_k2_forward(&base_dt, facet) .expect("2D benchmark k=2 forward flip should preserve topology"); } @@ -425,7 +434,7 @@ fn verify_3d_fixture(points: &[[f64; 3]], filter: CandidateFilter) { "3D adversarial k=1 support should touch an adversarial fixture feature" ); } - verify_k1_roundtrip(&base_dt, simplex_key, "3D k=1 n=1 ergodicity roundtrip") + verify_k1_roundtrip(&base_dt, simplex_key) .expect("3D k=1 roundtrip should recover the same triangulation"); let facet = flippable_k2_facet(&base_dt, true, filter) @@ -437,7 +446,7 @@ fn verify_3d_fixture(points: &[[f64; 3]], filter: CandidateFilter) { "3D adversarial k=2 support should touch an adversarial fixture feature" ); } - verify_k2_roundtrip(&base_dt, facet, "3D k=2 n=1 ergodicity roundtrip") + verify_k2_roundtrip(&base_dt, facet) .expect("3D k=2 roundtrip should recover the same triangulation"); let ridge = flippable_k3_ridge(&base_dt, false, filter) @@ -449,10 +458,7 @@ fn verify_3d_fixture(points: &[[f64; 3]], filter: CandidateFilter) { "3D adversarial k=3 support should touch an adversarial fixture feature" ); } - let mut k3 = base_dt; - forward_k3(&mut k3, ridge).expect("3D benchmark k=3 forward flip should succeed"); - k3.as_triangulation() - .validate() + verify_k3_forward(&base_dt, ridge) .expect("3D benchmark k=3 forward flip should preserve topology"); } @@ -477,7 +483,7 @@ fn verify_roundtrip_fixture_move( "adversarial k=1 support should touch an adversarial fixture feature" ); } - verify_k1_roundtrip(&base_dt, simplex_key, "k=1 n=1 ergodicity roundtrip") + verify_k1_roundtrip(&base_dt, simplex_key) .expect("k=1 roundtrip should recover the same triangulation"); } RoundtripMove::K2 => { @@ -490,7 +496,7 @@ fn verify_roundtrip_fixture_move( "adversarial k=2 support should touch an adversarial fixture feature" ); } - verify_k2_roundtrip(&base_dt, facet, "k=2 n=1 ergodicity roundtrip") + verify_k2_roundtrip(&base_dt, facet) .expect("k=2 roundtrip should recover the same triangulation"); } RoundtripMove::K3 => { @@ -503,7 +509,7 @@ fn verify_roundtrip_fixture_move( "adversarial k=3 support should touch an adversarial fixture feature" ); } - verify_k3_roundtrip(&base_dt, ridge, "k=3 n=1 ergodicity roundtrip") + verify_k3_roundtrip(&base_dt, ridge) .expect("k=3 roundtrip should recover the same triangulation"); } } diff --git a/tests/delaunay_edge_cases.rs b/tests/delaunay_edge_cases.rs index 76445ae2..203318d0 100644 --- a/tests/delaunay_edge_cases.rs +++ b/tests/delaunay_edge_cases.rs @@ -13,8 +13,6 @@ use delaunay::prelude::construction::{ DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::diagnostics::debug_print_first_delaunay_violation; use delaunay::prelude::generators::{ generate_random_points_in_ball_seeded, try_generate_random_triangulation_with_topology_guarantee, @@ -275,12 +273,10 @@ fn debug_issue_120_empty_circumsphere_5d() { } } let mut dt_robust: DelaunayTriangulation, (), (), 5> = - DelaunayTriangulation::try_from_tds_with_topology_guarantee( - dt.tds().clone(), - RobustKernel::new(), - TopologyGuarantee::PLManifold, - ) - .unwrap_or_else(|err| panic!("5D robust TDS should validate: {err}")); + DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build_with_kernel(&RobustKernel::new()) + .unwrap_or_else(|err| panic!("5D robust fixture should validate: {err}")); match dt_robust.repair_delaunay_with_flips() { Ok(stats) => { test_debug_info!( @@ -320,7 +316,7 @@ fn debug_issue_120_empty_circumsphere_5d() { for (simplex_key, simplex) in dt.simplices() { test_debug_info!("[Issue #120 debug] simplex {simplex_key:?}:"); for &vkey in simplex.vertices() { - let vertex = dt.tds().vertex(vkey).expect("vertex key should exist"); + let vertex = dt.vertex(vkey).expect("vertex key should exist"); test_debug_info!( " vkey={vkey:?}, uuid={}, point={:?}", vertex.uuid(), @@ -331,9 +327,7 @@ fn debug_issue_120_empty_circumsphere_5d() { if let Err(err) = dt.is_valid_delaunay() { #[cfg(feature = "diagnostics")] - { - debug_print_first_delaunay_violation(dt.tds(), None); - } + dt.debug_print_first_delaunay_violation(None); panic!("5D debug configuration violates Delaunay property: {err:?}"); } } @@ -949,7 +943,7 @@ fn regression_issue_228_exact_predicate_paths_3d_fast() { .expect("3D 16-point construction must not fail (#228 fast regression)"); assert!( - dt.is_delaunay_via_flips().is_ok(), + dt.verify_via_flip_predicates().is_ok(), "Delaunay property must hold (#228 fast regression, seed=0x{seed:X})" ); assert!(dt.number_of_vertices() > 0); diff --git a/tests/delaunay_incremental_insertion.rs b/tests/delaunay_incremental_insertion.rs index 77b362e1..b53a5cc8 100644 --- a/tests/delaunay_incremental_insertion.rs +++ b/tests/delaunay_incremental_insertion.rs @@ -11,7 +11,7 @@ use approx::assert_relative_eq; use delaunay::geometry::kernel::RobustKernel; -use delaunay::prelude::algorithms::{LocateResult, find_conflict_region, locate}; +use delaunay::prelude::algorithms::LocateResult; use delaunay::prelude::collections::MAX_PRACTICAL_DIMENSION_SIZE; use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayTriangulation, DelaunayTriangulationBuilder, @@ -46,7 +46,7 @@ fn assert_neighbors_valid_and_symmetric( let Some(neighbor_key) = neighbor_opt else { continue; }; - let neighbor_simplex = dt.tds().simplex(neighbor_key).unwrap_or_else(|| { + let neighbor_simplex = dt.simplex(neighbor_key).unwrap_or_else(|| { panic!("neighbor {neighbor_key:?} for {simplex_key:?} is missing") }); let facet_key = facet_key_for_simplex(simplex, facet_idx); @@ -77,10 +77,7 @@ fn centroid_of_first_simplex( .expect("test triangulation should contain at least one simplex"); let mut coords = [0.0; D]; for &vertex_key in simplex.vertices() { - let vertex = dt - .tds() - .vertex(vertex_key) - .expect("simplex vertex should exist"); + let vertex = dt.vertex(vertex_key).expect("simplex vertex should exist"); for (coord, &value) in coords.iter_mut().zip(vertex.point().coords()) { *coord += value; } @@ -103,12 +100,12 @@ fn centroid_of_first_simplex( fn assert_locate_and_conflict_traversal( dt: &DelaunayTriangulation, (), (), D>, ) { - let kernel = AdaptiveKernel::::new(); let (hint_simplex, query) = centroid_of_first_simplex(dt); - let no_hint = locate(dt.tds(), &kernel, &query, None).expect("locate without hint failed"); - let with_hint = - locate(dt.tds(), &kernel, &query, Some(hint_simplex)).expect("locate with hint failed"); + let no_hint = dt.locate(&query, None).expect("locate without hint failed"); + let with_hint = dt + .locate(&query, Some(hint_simplex)) + .expect("locate with hint failed"); let start_simplex = match with_hint { LocateResult::InsideSimplex(simplex_key) => simplex_key, @@ -119,11 +116,12 @@ fn assert_locate_and_conflict_traversal( "centroid should locate inside a simplex without hint, got {no_hint:?}" ); - let conflict_simplices = find_conflict_region(dt.tds(), &kernel, &query, start_simplex) + let conflict_simplices = dt + .find_conflict_region(&query, start_simplex) .expect("conflict traversal failed"); assert!(!conflict_simplices.is_empty()); for &simplex_key in &conflict_simplices { - assert!(dt.tds().contains_simplex(simplex_key)); + assert!(dt.contains_simplex(simplex_key)); } } @@ -687,7 +685,7 @@ macro_rules! test_bootstrap_key_stability { // Verify all keys remain valid after simplex creation for (i, &key) in keys.iter().enumerate() { - let vertex = dt.tds().vertex(key); + let vertex = dt.vertex(key); assert!(vertex.is_some(), "Key {} should remain valid after simplex creation", i); @@ -783,7 +781,7 @@ fn test_bootstrap_returns_valid_key_after_tds_rebuild() { assert_eq!(dt.number_of_simplices(), 1); // Verify all returned keys are valid in the final TDS - let vertex1 = dt.tds().vertex(key1); + let vertex1 = dt.vertex(key1); assert!( vertex1.is_some(), "First key should be valid after simplex creation" @@ -794,7 +792,7 @@ fn test_bootstrap_returns_valid_key_after_tds_rebuild() { "First key should map to correct vertex UUID" ); - let vertex2 = dt.tds().vertex(key2); + let vertex2 = dt.vertex(key2); assert!( vertex2.is_some(), "Second key should be valid after simplex creation" @@ -805,7 +803,7 @@ fn test_bootstrap_returns_valid_key_after_tds_rebuild() { "Second key should map to correct vertex UUID" ); - let vertex3 = dt.tds().vertex(key3); + let vertex3 = dt.vertex(key3); assert!( vertex3.is_some(), "Third key (D+1) should be valid after simplex creation" diff --git a/tests/delaunay_repair_fallback.rs b/tests/delaunay_repair_fallback.rs index d37a3087..a1e8b46f 100644 --- a/tests/delaunay_repair_fallback.rs +++ b/tests/delaunay_repair_fallback.rs @@ -4,11 +4,10 @@ //! Delaunay violations, the deterministic rebuild heuristic is triggered and //! successfully produces a valid Delaunay triangulation. -use delaunay::flips::BistellarFlips; -use delaunay::flips::FacetHandle; use delaunay::prelude::construction::{ DelaunayRepairPolicy, DelaunayTriangulation, TopologyGuarantee, }; +use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; use delaunay::vertex; @@ -58,23 +57,28 @@ fn repair_fallback_produces_valid_triangulation() { .expect("fixture construction should succeed"); let mut candidate_facets = Vec::new(); - for (simplex_key, simplex) in dt.simplices() { - if let Some(neighbors) = simplex.neighbors() { - for (index, neighbor) in neighbors.enumerate() { - if neighbor.is_some() { - let facet_index = u8::try_from(index).expect("2D facet index fits in u8"); - candidate_facets.push( - FacetHandle::try_new(dt.tds(), simplex_key, facet_index) - .expect("interior facet index should be valid"), - ); - } - } + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + if facet + .simplex() + .neighbor_key(usize::from(facet.facet_index())) + .flatten() + .is_some() + { + candidate_facets.push(facet.handle()); } } - let flipped = candidate_facets - .into_iter() - .any(|facet| dt.flip_k2(facet).is_ok()); + let mut flipped = false; + for facet in candidate_facets { + let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { + continue; + }; + if proposal.attempt_on(&mut dt).is_ok() { + flipped = true; + break; + } + } assert!(flipped, "fixture should contain a flippable interior facet"); let mut config = DelaunayRepairHeuristicConfig::default(); diff --git a/tests/delaunayize_workflow.rs b/tests/delaunayize_workflow.rs index 60969b83..5d9c92ea 100644 --- a/tests/delaunayize_workflow.rs +++ b/tests/delaunayize_workflow.rs @@ -8,10 +8,9 @@ //! - Repeat-run determinism for outcome stats //! - Multi-dimensional coverage (2D–3D) -use delaunay::flips::BistellarFlips; -use delaunay::flips::FacetHandle; use delaunay::prelude::construction::{DelaunayTriangulation, TriangulationConstructionError}; use delaunay::prelude::delaunayize::*; +use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; use delaunay::vertex; use std::{error::Error, mem::size_of}; @@ -261,8 +260,9 @@ fn test_vertex_count_preserved_after_delaunayize() { // NON-DELAUNAY REPAIR VIA FLIPS TEST // ============================================================================= -/// Build a valid Delaunay triangulation, apply a k=2 flip to intentionally -/// break the Delaunay property, then verify `delaunayize_by_flips` restores it. +/// Build a valid Delaunay triangulation, apply a k=2 Pachner move to +/// intentionally break the Delaunay property, then verify +/// `delaunayize_by_flips` restores it. #[test] fn test_flip_breaks_delaunay_then_delaunayize_restores() { init_tracing(); @@ -280,22 +280,24 @@ fn test_flip_breaks_delaunay_then_delaunayize_restores() { // Collect candidate interior facets (immutable borrow ends before mutation). let mut candidate_facets = Vec::new(); - for (ck, simplex) in dt.simplices() { - if let Some(neighbors) = simplex.neighbors() { - for (i, n) in neighbors.enumerate() { - if let (Some(_), Ok(idx)) = (n, u8::try_from(i)) { - candidate_facets.push( - FacetHandle::try_new(dt.tds(), ck, idx) - .expect("interior facet index should be valid"), - ); - } - } + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + if facet + .simplex() + .neighbor_key(usize::from(facet.facet_index())) + .flatten() + .is_some() + { + candidate_facets.push(facet.handle()); } } let mut flipped = false; for facet in candidate_facets { - if dt.flip_k2(facet).is_ok() { + let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { + continue; + }; + if proposal.attempt_on(&mut dt).is_ok() { flipped = true; break; } @@ -522,7 +524,7 @@ fn test_delaunayize_with_flip_budget_and_fallback_2d() { assert!(dt.validate().is_ok()); } -/// Apply a k=2 flip to break the Delaunay property, then verify +/// Apply a k=2 Pachner move to break the Delaunay property, then verify /// `delaunayize_by_flips` with an explicit flip budget restores it. #[test] fn test_flip_breaks_then_delaunayize_with_budget_restores_3d() { @@ -540,22 +542,24 @@ fn test_flip_breaks_then_delaunayize_with_budget_restores_3d() { // Collect candidate interior facets. let mut candidate_facets = Vec::new(); - for (ck, simplex) in dt.simplices() { - if let Some(neighbors) = simplex.neighbors() { - for (i, n) in neighbors.enumerate() { - if let (Some(_), Ok(idx)) = (n, u8::try_from(i)) { - candidate_facets.push( - FacetHandle::try_new(dt.tds(), ck, idx) - .expect("interior facet index should be valid"), - ); - } - } + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + if facet + .simplex() + .neighbor_key(usize::from(facet.facet_index())) + .flatten() + .is_some() + { + candidate_facets.push(facet.handle()); } } let mut flipped = false; for facet in candidate_facets { - if dt.flip_k2(facet).is_ok() { + let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { + continue; + }; + if proposal.attempt_on(&mut dt).is_ok() { flipped = true; break; } diff --git a/tests/euler_characteristic.rs b/tests/euler_characteristic.rs index 44215292..a7ae9bc7 100644 --- a/tests/euler_characteristic.rs +++ b/tests/euler_characteristic.rs @@ -23,10 +23,9 @@ use delaunay::prelude::construction::{ TopologyGuarantee, }; use delaunay::prelude::geometry::AdaptiveKernel; -use delaunay::prelude::query::FacetIncidenceAnalysis; use delaunay::prelude::tds::Tds; use delaunay::prelude::topology::validation::ManifoldError; -use delaunay::topology::characteristics::{euler, validation}; +use delaunay::topology::characteristics::euler; use delaunay::topology::traits::topological_space::{ GlobalTopology, TopologyError, TopologyKind, ToroidalConstructionMode, }; @@ -67,7 +66,7 @@ fn test_2d_single_triangle() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 3, "Should have 3 vertices"); assert_eq!(result.counts.count(1), 3, "Should have 3 edges"); @@ -93,8 +92,9 @@ fn test_euler_rejects_open_single_simplex_in_closed_topology() { .build() .unwrap(); - let classify_err = - euler::classify_triangulation(dt.tds(), GlobalTopology::Spherical).unwrap_err(); + let classify_err = dt + .topology_classification_for(GlobalTopology::Spherical) + .unwrap_err(); assert_matches!( classify_err, TopologyError::BoundaryClassification { source } @@ -107,8 +107,9 @@ fn test_euler_rejects_open_single_simplex_in_closed_topology() { ) ); - let validation_err = - validation::validate_triangulation_euler(dt.tds(), GlobalTopology::Spherical).unwrap_err(); + let validation_err = dt + .euler_check_for_topology(GlobalTopology::Spherical) + .unwrap_err(); assert_matches!( validation_err, TopologyError::BoundaryClassification { source } @@ -136,7 +137,7 @@ fn test_2d_multiple_triangles() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 4, "Should have 4 vertices"); assert_eq!( @@ -164,7 +165,7 @@ fn test_3d_single_tetrahedron() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 4, "Should have 4 vertices"); assert_eq!(result.counts.count(1), 6, "Should have 6 edges"); @@ -193,7 +194,7 @@ fn test_3d_with_interior_vertex() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 5, "Should have 5 vertices"); assert_eq!( @@ -223,7 +224,7 @@ fn test_4d_single_simplex() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 5, "Should have 5 vertices"); assert_eq!(result.counts.count(1), 10, "Should have 10 edges"); @@ -254,7 +255,7 @@ fn test_5d_single_simplex() { .topology_guarantee(TopologyGuarantee::PLManifold) .build() .unwrap(); - let result = validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let result = dt.euler_check().unwrap(); assert_eq!(result.counts.count(0), 6, "Should have 6 vertices"); assert_eq!(result.chi, 1, "Single 5-simplex should have χ = 1"); @@ -427,8 +428,7 @@ macro_rules! test_complex_with_interior { .unwrap(); // Full complex should have χ = 1 (D-ball) - let full_result = - validation::validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let full_result = dt.euler_check().unwrap(); assert_eq!( full_result.chi, 1, "Full {}-dimensional complex should have χ = 1 (D-ball)", @@ -441,7 +441,11 @@ macro_rules! test_complex_with_interior { ); // Verify we have boundary facets - let boundary_facet_count = dt.tds().number_of_one_sided_facets().unwrap(); + let boundary_facet_count = dt + .boundary_facets() + .unwrap() + .try_fold(0_usize, |count, facet| facet.map(|_| count + 1)) + .unwrap(); assert!( boundary_facet_count > 0, "Should have boundary facets in dimension {}", @@ -449,7 +453,7 @@ macro_rules! test_complex_with_interior { ); // Verify we have more than one simplex (ensuring interior point) - let simplex_count = dt.tds().number_of_simplices(); + let simplex_count = dt.number_of_simplices(); assert!( simplex_count > 1, "Should have multiple simplices (>1) to ensure interior point in dimension {}", @@ -463,8 +467,7 @@ macro_rules! test_complex_with_interior { // - 4D: boundary is S³ (3-sphere) → χ = 0 // - 5D: boundary is S⁴ (4-sphere) → χ = 2 // Generally: χ(S^k) = 1 + (-1)^k - let boundary_counts = - euler::count_boundary_simplices(dt.tds(), dt.global_topology()).unwrap(); + let boundary_counts = dt.boundary_simplex_counts().unwrap(); let boundary_chi = euler::euler_characteristic(&boundary_counts); let expected_boundary_chi = $expected_boundary_chi; diff --git a/tests/insert_with_statistics.rs b/tests/insert_with_statistics.rs index 4fccbc98..968a7259 100644 --- a/tests/insert_with_statistics.rs +++ b/tests/insert_with_statistics.rs @@ -162,7 +162,7 @@ fn delaunay_insert_with_statistics_handles_degenerate_k2_flips_4d() { } assert_eq!(dt.number_of_vertices(), 7); - assert!(dt.tds().validate().is_ok()); + assert!(dt.validate_structure().is_ok()); } #[test] @@ -207,7 +207,7 @@ fn delaunay_insert_with_statistics_duplicate_coordinates_2d() { } // Still in bootstrap (no simplices yet), so validate only Levels 1–2 (elements + structure). - assert!(dt.tds().validate().is_ok()); + assert!(dt.validate_structure().is_ok()); assert_eq!(dt.number_of_vertices(), 1); } diff --git a/tests/large_scale_debug.rs b/tests/large_scale_debug.rs index 9b71b71e..9915faa9 100644 --- a/tests/large_scale_debug.rs +++ b/tests/large_scale_debug.rs @@ -1786,7 +1786,7 @@ fn regression_issue_228_3d_1000_flip_repair_convergence() { "Topology validation (L1-L3) must pass (#228 regression, seed=0x{seed:X})" ); assert!( - dt.is_delaunay_via_flips().is_ok(), + dt.verify_via_flip_predicates().is_ok(), "Delaunay property must hold (#228 regression, seed=0x{seed:X})" ); } diff --git a/tests/pachner_roundtrip.rs b/tests/pachner_roundtrip.rs index dd065912..21b50820 100644 --- a/tests/pachner_roundtrip.rs +++ b/tests/pachner_roundtrip.rs @@ -11,10 +11,12 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, InsertionOrderStrategy, TopologyGuarantee, Vertex, }; use delaunay::prelude::geometry::RobustKernel; +#[cfg(feature = "slow-tests")] +use delaunay::prelude::pachner::RidgeHandle; use delaunay::prelude::pachner::{ BistellarFlipKind, EdgeKey, EdgeKeyError, FacetHandle, FlipDirection, FlipError, PachnerMove, - PachnerMoveFeasibility, PachnerMoveResult, PachnerMoves, PachnerProposal, RidgeHandle, - SimplexKey, TopologyOwner, TriangleHandle, VertexKey, + PachnerMoveFeasibility, PachnerMoveResult, PachnerMoves, PachnerProposal, SimplexKey, + TopologyOwner, TriangleHandle, VertexKey, }; use uuid::Uuid; @@ -32,6 +34,33 @@ const MINIMAL_POINTS_4D: &[[f64; 4]] = &[ [0.0, 0.0, 0.0, 1.0], ]; +fn vertex_key_by_uuid(dt: &Dt, uuid: Uuid) -> Option { + dt.vertices() + .find_map(|(vertex_key, vertex)| (vertex.uuid() == uuid).then_some(vertex_key)) +} + +fn find_live_edge( + dt: &Dt, + a: VertexKey, + b: VertexKey, +) -> Result { + if a == b { + return Err(EdgeKeyError::DuplicateEndpoint { endpoint: a }); + } + if !dt.contains_vertex_key(a) { + return Err(EdgeKeyError::MissingEndpoint { endpoint: a }); + } + if !dt.contains_vertex_key(b) { + return Err(EdgeKeyError::MissingEndpoint { endpoint: b }); + } + dt.edges() + .find(|edge| { + let (first, second) = edge.endpoints(); + (first == a && second == b) || (first == b && second == a) + }) + .ok_or(EdgeKeyError::EdgeNotFound { v0: a, v1: b }) +} + #[cfg(feature = "slow-tests")] const STABLE_POINTS_4D: &[[f64; 4]] = &[ [0.0, 0.0, 0.0, 0.0], @@ -210,9 +239,7 @@ fn stale_pachner_error_propagates_through_delaunay_result() { }, ) .expect("initial k=1 insert should make the simplex key stale"); - let inserted_vertex = dt - .tds() - .vertex_key_from_uuid(&vertex_uuid) + let inserted_vertex = vertex_key_by_uuid(&dt, vertex_uuid) .expect("initial k=1 insert should create the requested vertex"); assert_k1_insert_result(&inserted, inserted_vertex); let before_failed_attempt = snapshot_topology(&dt); @@ -228,8 +255,7 @@ fn stale_pachner_error_propagates_through_delaunay_result() { ), "unexpected DelaunayResult error for stale Pachner move: {err:?}" ); - dt.tds() - .is_valid() + dt.is_valid_structure() .expect("failed Pachner attempt should preserve TDS validity"); assert_eq!(snapshot_topology(&dt), before_failed_attempt); } @@ -348,12 +374,13 @@ fn pachner_feasibility_rejects_unsupported_2d_k2_inverse_without_mutating() { #[test] fn pachner_feasibility_rejects_boundary_facet_like_attempt_2d() { let dt = build_single_triangle_dt_2d(); - let (simplex_key, _) = dt - .simplices() + let facet = dt + .boundary_facets() + .expect("single-triangle fixture should classify boundary facets") .next() - .expect("single-triangle fixture should contain one simplex"); - let facet = FacetHandle::try_new(dt.tds(), simplex_key, 0) - .expect("single-triangle boundary facet should be a live handle"); + .expect("single-triangle fixture should expose a boundary facet") + .expect("boundary facet should reborrow as a live view") + .handle(); let pachner_move = PachnerMove::K2 { facet }; let feasibility = dt.propose_pachner(pachner_move); @@ -418,9 +445,7 @@ fn pachner_feasibility_agrees_with_toroidal_2d_k1_insert() { let result = proposal .attempt_on(&mut trial) .expect("toroidal k=1 attempt should agree with feasibility"); - let inserted_vertex = trial - .tds() - .vertex_key_from_uuid(&vertex_uuid) + let inserted_vertex = vertex_key_by_uuid(&trial, vertex_uuid) .expect("successful k=1 attempt should allocate the inserted vertex key"); assert_eq!(result.kind, feasibility.kind); assert_eq!(result.direction, feasibility.direction); @@ -441,7 +466,6 @@ fn pachner_feasibility_rejects_duplicate_k1_insert_uuid_without_mutating() { let dt = build_single_triangle_dt_2d(); let simplex_key = first_simplex_generic(&dt); let duplicate_vertex = dt - .tds() .vertices() .next() .map(|(_, vertex)| *vertex) @@ -471,7 +495,7 @@ fn pachner_feasibility_rejects_invalid_3d_inverse_k2_without_mutating() { let [a, b, ..] = simplex.vertices() else { panic!("3D simplex should contain at least two vertices"); }; - let edge = EdgeKey::try_new(dt.tds(), *a, *b).expect("simplex vertices should form an edge"); + let edge = find_live_edge(&dt, *a, *b).expect("simplex vertices should form an edge"); let pachner_move = PachnerMove::K2Inverse { edge }; assert_pachner_rejection_preserves_topology(dt, pachner_move, |err| { @@ -488,8 +512,10 @@ fn pachner_feasibility_rejects_invalid_3d_inverse_k2_without_mutating() { #[test] fn pachner_feasibility_rejects_invalid_3d_k3_without_mutating() { let dt = build_minimal_simplex_dt::<3>(); - let simplex_key = first_simplex_generic(&dt); - let ridge = RidgeHandle::try_new(dt.tds(), simplex_key, 0, 1) + let ridge = dt + .ridge_handles() + .next() + .expect("minimal 3D fixture should expose a ridge handle") .expect("minimal 3D fixture should expose a ridge handle"); let pachner_move = PachnerMove::K3 { ridge }; @@ -567,9 +593,7 @@ fn try_stale_k1_insert( vertex, }, )?; - let inserted_vertex = dt - .tds() - .vertex_key_from_uuid(&vertex_uuid) + let inserted_vertex = vertex_key_by_uuid(dt, vertex_uuid) .expect("unexpected successful stale insert should create the requested vertex"); assert_k1_insert_result(&inserted, inserted_vertex); Ok(()) @@ -675,26 +699,14 @@ fn build_canonicalized_toroidal_dt_2d() -> Dt2 { /// Searches the 2D fixture for an edge facet whose public k=2 move succeeds. fn flippable_k2_facet_2d(dt: &Dt2) -> FacetHandle { - for (simplex_key, simplex) in dt.simplices() { - let Some(neighbors) = simplex.neighbors() else { - continue; - }; - for (facet_index, neighbor) in neighbors.enumerate() { - if neighbor.is_none() { - continue; - } - let facet = FacetHandle::try_new( - dt.tds(), - simplex_key, - u8::try_from(facet_index).expect("2D facet index should fit in u8"), - ) - .expect("interior 2D facet index should be valid"); - let mut trial = dt.clone(); - if attempt_pachner_move(&mut trial, PachnerMove::K2 { facet }).is_ok() - && topology_and_delaunay_valid(&trial) - { - return facet; - } + for facet in dt.facets() { + let facet = facet.expect("2D fixture facets should reborrow as live views"); + let facet = facet.handle(); + let mut trial = dt.clone(); + if attempt_pachner_move(&mut trial, PachnerMove::K2 { facet }).is_ok() + && topology_and_delaunay_valid(&trial) + { + return facet; } } panic!("stable 2D fixture should contain a public k=2 candidate"); @@ -725,21 +737,20 @@ fn assert_flip_and_pachner_feasibility_match( /// Captures topology by stable UUIDs for any deterministic test fixture. fn snapshot_topology_generic(dt: &Dt) -> TopologySnapshot { - let tds = dt.tds(); - let mut vertex_uuids = tds + let mut vertex_uuids = dt .vertices() .map(|(_, vertex)| vertex.uuid()) .collect::>(); vertex_uuids.sort(); - let mut simplex_vertex_uuids = tds + let mut simplex_vertex_uuids = dt .simplices() .map(|(_, simplex)| { let mut uuids = simplex .vertices() .iter() .map(|vertex_key| { - tds.vertex(*vertex_key) + dt.vertex(*vertex_key) .expect("simplex should reference live vertices") .uuid() }) @@ -838,9 +849,7 @@ fn assert_public_k1_insert_feasibility_smoke() { let result = proposal .attempt_on(&mut trial) .unwrap_or_else(|err| panic!("{D}D public k=1 mutation should succeed: {err:?}")); - let inserted_vertex = trial - .tds() - .vertex_key_from_uuid(&vertex_uuid) + let inserted_vertex = vertex_key_by_uuid(&trial, vertex_uuid) .expect("successful k=1 mutation should allocate the requested vertex"); assert_eq!(feasibility.kind, result.kind); assert_eq!(feasibility.direction, result.direction); @@ -891,45 +900,31 @@ fn first_simplex_generic(dt: &Dt) -> SimplexKey { /// Computes a simplex centroid for generic dimension smoke tests. fn simplex_centroid_generic(dt: &Dt, simplex_key: SimplexKey) -> [f64; D] { - let simplex = dt - .tds() - .simplex(simplex_key) - .expect("simplex key should exist"); - let mut coords = [0.0; D]; - for &vkey in simplex.vertices() { - let vertex = dt.tds().vertex(vkey).expect("vertex key should exist"); - for (coord, value) in coords.iter_mut().zip(vertex.point().coords()) { - *coord += *value; - } - } - - let vertex_count = u32::try_from(simplex.vertices().len()) - .map(f64::from) - .expect("simplex vertex count should fit in u32"); - for coord in &mut coords { - *coord /= vertex_count; - } - coords + *dt.simplex_barycenter(simplex_key) + .expect("simplex key should have a finite barycenter") + .coords() } /// Converts a 2D facet handle into the edge key represented by that facet. fn edge_for_facet_2d(dt: &Dt2, facet: FacetHandle) -> EdgeKey { - let view = facet - .view(dt.tds()) - .expect("facet handle should still be live"); - let endpoints = view - .simplex() - .vertices() - .iter() - .enumerate() - .filter_map(|(index, &vertex_key)| { - (index != usize::from(view.facet_index())).then_some(vertex_key) + let view = dt + .facets() + .find_map(|candidate| { + let candidate = candidate.expect("2D fixture facets should reborrow as live views"); + (candidate.handle() == facet).then_some(candidate) }) - .collect::>(); - let [a, b] = endpoints.as_slice() else { - panic!("2D facet should contain exactly two edge endpoints"); + .expect("facet handle should still be live"); + let vertices = view.simplex().vertices(); + let endpoints = match usize::from(view.facet_index()) { + 0 => [vertices[1], vertices[2]], + 1 => [vertices[0], vertices[2]], + 2 => [vertices[0], vertices[1]], + index => { + panic!("invalid 2D facet index {index}"); + } }; - EdgeKey::try_new(dt.tds(), *a, *b).expect("facet endpoints should form a live edge") + let [a, b] = endpoints; + find_live_edge(dt, a, b).expect("facet endpoints should form a live edge") } /// Parses the inserted edge reported by a 2D k=2 move. @@ -940,7 +935,7 @@ fn inserted_edge_2d(dt: &Dt2, vertices: &[VertexKey]) -> EdgeKey { vertices.len() ); }; - EdgeKey::try_new(dt.tds(), *a, *b).expect("reported inserted vertices should form a live edge") + find_live_edge(dt, *a, *b).expect("reported inserted vertices should form a live edge") } /// Checks that a rejected detached proposal leaves the live topology byte-for-byte equivalent. @@ -951,7 +946,7 @@ fn assert_failed_attempt_preserves_topology( ) { let before = snapshot_topology(dt); let proposal_generation = proposal.topology_generation(); - let current_generation = dt.tds().generation(); + let current_generation = dt.topology_generation(); let feasibility_err = proposal .can_attempt_on(dt) @@ -965,8 +960,7 @@ fn assert_failed_attempt_preserves_topology( .expect_err("stale Pachner proposal should fail"); assert_error(&err); assert_stale_proposal_generation(&err, proposal_generation, current_generation); - dt.tds() - .is_valid() + dt.is_valid_structure() .expect("failed Pachner attempt should preserve TDS validity"); assert_eq!(snapshot_topology(dt), before); } @@ -1027,9 +1021,7 @@ fn assert_stale_k1_insert_preserves_topology(mut dt: Dt4) { .clone() .attempt_on(&mut dt) .expect("initial k=1 insert should make the proposal stale"); - let inserted_vertex = dt - .tds() - .vertex_key_from_uuid(&vertex_uuid) + let inserted_vertex = vertex_key_by_uuid(&dt, vertex_uuid) .expect("initial k=1 insert should create the requested vertex"); assert_k1_insert_result(&inserted, inserted_vertex); assert_failed_attempt_preserves_topology(&mut dt, stale_proposal, |err| { @@ -1055,10 +1047,8 @@ fn assert_stale_k1_remove_preserves_topology(mut dt: Dt4) { let inserted = insert_proposal .attempt_on(&mut dt) .expect("k=1 insert should create a removable vertex"); - let vertex_key = dt - .tds() - .vertex_key_from_uuid(&vertex_uuid) - .expect("inserted k=1 vertex should be present"); + let vertex_key = + vertex_key_by_uuid(&dt, vertex_uuid).expect("inserted k=1 vertex should be present"); assert_k1_insert_result(&inserted, vertex_key); let stale_proposal = dt .propose_pachner(PachnerMove::K1Remove { vertex_key }) @@ -1189,25 +1179,9 @@ fn first_simplex(dt: &Dt4) -> SimplexKey { /// Computes an interior-ish point for k=1 insertion into a known simplex. fn simplex_centroid(dt: &Dt4, simplex_key: SimplexKey) -> [f64; 4] { - let simplex = dt - .tds() - .simplex(simplex_key) - .expect("simplex key should exist"); - let mut coords = [0.0; 4]; - for &vkey in simplex.vertices() { - let vertex = dt.tds().vertex(vkey).expect("vertex key should exist"); - for (coord, value) in coords.iter_mut().zip(vertex.point().coords()) { - *coord += *value; - } - } - - let vertex_count = u32::try_from(simplex.vertices().len()) - .map(f64::from) - .expect("simplex vertex count should fit in u32"); - for coord in &mut coords { - *coord /= vertex_count; - } - coords + *dt.simplex_barycenter(simplex_key) + .expect("simplex key should have a finite barycenter") + .coords() } /// Applies a k=1 insert/remove pair and checks the reported move metadata. @@ -1226,10 +1200,8 @@ fn roundtrip_k1(dt: &mut Dt4) { .expect("k=1 insert should succeed on stable 4D fixture"); assert_eq!(inserted.inserted_face_vertices.len(), 1); - let inserted_key = dt - .tds() - .vertex_key_from_uuid(&new_uuid) - .expect("inserted k=1 vertex should be present"); + let inserted_key = + vertex_key_by_uuid(dt, new_uuid).expect("inserted k=1 vertex should be present"); let removed = attempt_pachner_move( dt, PachnerMove::K1Remove { @@ -1247,30 +1219,18 @@ fn roundtrip_k1(dt: &mut Dt4) { /// Searches the fixture for a k=2 facet that also supports the public inverse API. #[cfg(feature = "slow-tests")] fn flippable_k2_facet(dt: &Dt4) -> FacetHandle { - for (simplex_key, simplex) in dt.simplices() { - let Some(neighbors) = simplex.neighbors() else { + for facet in dt.facets() { + let facet = facet.expect("4D fixture facets should reborrow as live views"); + let facet = facet.handle(); + let mut trial = dt.clone(); + let Ok(info) = attempt_pachner_move(&mut trial, PachnerMove::K2 { facet }) else { continue; }; - for (facet_index, neighbor) in neighbors.enumerate() { - if neighbor.is_none() { - continue; - } - let facet = FacetHandle::try_new( - dt.tds(), - simplex_key, - u8::try_from(facet_index).expect("facet index should fit in u8"), - ) - .expect("interior facet index should be valid"); - let mut trial = dt.clone(); - let Ok(info) = attempt_pachner_move(&mut trial, PachnerMove::K2 { facet }) else { - continue; - }; - let edge = inserted_edge(&trial, &info.inserted_face_vertices); - if attempt_pachner_move(&mut trial, PachnerMove::K2Inverse { edge }).is_ok() - && topology_and_delaunay_valid(&trial) - { - return facet; - } + let edge = inserted_edge(&trial, &info.inserted_face_vertices); + if attempt_pachner_move(&mut trial, PachnerMove::K2Inverse { edge }).is_ok() + && topology_and_delaunay_valid(&trial) + { + return facet; } } panic!("stable 4D fixture should contain a public k=2 roundtrip candidate"); @@ -1301,33 +1261,23 @@ fn inserted_edge(dt: &Dt4, vertices: &[VertexKey]) -> EdgeKey { vertices.len() ); }; - EdgeKey::try_new(dt.tds(), *a, *b).expect("k=2 flip should report a real inserted edge") + find_live_edge(dt, *a, *b).expect("k=2 flip should report a real inserted edge") } /// Searches the fixture for a k=3 ridge that also supports the public inverse API. #[cfg(feature = "slow-tests")] fn flippable_k3_ridge(dt: &Dt4) -> RidgeHandle { - for (simplex_key, simplex) in dt.simplices() { - for i in 0..simplex.number_of_vertices() { - for j in (i + 1)..simplex.number_of_vertices() { - let ridge = RidgeHandle::try_new( - dt.tds(), - simplex_key, - u8::try_from(i).expect("ridge index should fit in u8"), - u8::try_from(j).expect("ridge index should fit in u8"), - ) - .expect("ridge indices should be valid"); - let mut trial = dt.clone(); - let Ok(info) = attempt_pachner_move(&mut trial, PachnerMove::K3 { ridge }) else { - continue; - }; - let triangle = inserted_triangle(&info.inserted_face_vertices); - if attempt_pachner_move(&mut trial, PachnerMove::K3Inverse { triangle }).is_ok() - && topology_and_delaunay_valid(&trial) - { - return ridge; - } - } + for ridge in dt.ridge_handles() { + let ridge = ridge.expect("4D fixture ridges should produce live handles"); + let mut trial = dt.clone(); + let Ok(info) = attempt_pachner_move(&mut trial, PachnerMove::K3 { ridge }) else { + continue; + }; + let triangle = inserted_triangle(&info.inserted_face_vertices); + if attempt_pachner_move(&mut trial, PachnerMove::K3Inverse { triangle }).is_ok() + && topology_and_delaunay_valid(&trial) + { + return ridge; } } panic!("stable 4D fixture should contain a public k=3 roundtrip candidate"); diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index 256ed329..dda8dfef 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -113,7 +113,7 @@ use delaunay::prelude::geometry::{ use delaunay::prelude::insertion::{ InitialSimplexConstructionError, InitialSimplexUnexpectedInsertionStage, InsertionError, InsertionErrorKind as FocusedInsertionErrorKind, InsertionTopologyValidationContext, - NeighborRebuildError, Tds as InsertionTds, TdsMutationError, repair_neighbor_pointers_local, + NeighborRebuildError, }; use delaunay::prelude::ordering::{ HilbertBitDepth, HilbertError, HilbertQuantizedBatch, MAX_HILBERT_BITS, hilbert_index_in_range, @@ -140,6 +140,9 @@ use delaunay::prelude::query::{ FacetIncidenceAnalysis as QueryFacetIncidenceAnalysis, FacetIncidenceView as QueryFacetIncidenceView, IncidenceView as QueryIncidenceView, OneSidedFacetsIter as QueryOneSidedFacetsIter, QueryError, + RidgeCandidate as QueryRidgeCandidate, RidgeCandidateError as QueryRidgeCandidateError, + RidgeHandle as QueryRidgeHandle, RidgeLinkView as QueryRidgeLinkView, + RidgeQuery as QueryRidgeQuery, RidgeView as QueryRidgeView, SimplexBarycenterError as QuerySimplexBarycenterError, SimplexDataFillError as QuerySimplexDataFillError, SimplexFacetsIter as QuerySimplexFacetsIter, SimplexNeighborIndex as QuerySimplexNeighborIndex, TopologyIndexBuildError, @@ -154,14 +157,15 @@ use delaunay::prelude::repair::{ DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, DelaunayTriangulationValidationError, FlipEdgeAdjacencyError, FlipError, FlipFailureKind, FlipOrientationCheckStage as RepairFlipOrientationCheckStage, FlipTriangleAdjacencyError, - FlipVertexAdjacencyError, RepairQueueOrder, verify_delaunay_for_triangulation, + FlipVertexAdjacencyError, RepairQueueOrder, }; use delaunay::prelude::tds::{ - AllFacetsIter as TdsAllFacetsIter, BoundaryFacetsIter as TdsBoundaryFacetsIter, EdgeKey, - EdgeKeyError, EdgeView, FacetError, FacetHandle, FacetIncidenceView as TdsFacetIncidenceView, - FacetView, InvariantError, NeighborSlot, OneSidedFacetsIter as TdsOneSidedFacetsIter, + AllFacetsIter as TdsAllFacetsIter, BoundaryFacetsIter as TdsBoundaryFacetsIter, EdgeKeyError, + EdgeView, FacetError, FacetIncidenceView as TdsFacetIncidenceView, FacetView, InvariantError, + NeighborSlot, OneSidedFacetsIter as TdsOneSidedFacetsIter, SimplexFacetsIter as TdsSimplexFacetsIter, SimplexKey, Tds, TdsConstructionError, TdsError, - TopologyOwner as TdsTopologyOwner, TopologyOwnerId as TdsTopologyOwnerId, VertexKey, + TdsMutationError, TopologyOwner as TdsTopologyOwner, TopologyOwnerId as TdsTopologyOwnerId, + VertexKey, }; use delaunay::prelude::topology::spaces::{ GlobalTopology, GlobalTopologyModelError, LiftedLinkEdge, LiftedVertexId, TopologyKind, @@ -169,14 +173,19 @@ use delaunay::prelude::topology::spaces::{ }; use delaunay::prelude::topology::validation::{ GlobalTopology as TopologyValidationGlobalTopology, ManifoldError, RidgeCandidate, - RidgeCandidateError, RidgeLinkView, RidgeQuery, RidgeView, ridge_star_simplices, + RidgeCandidateError, RidgeLinkView, RidgeQuery, RidgeView, }; use delaunay::prelude::triangulation::{ AllFacetsIter as TriangulationAllFacetsIter, BoundaryFacetsIter as TriangulationBoundaryFacetsIter, EdgeIndex as GenericEdgeIndex, FacetIssuesMap as TriangulationFacetIssuesMap, FastKernel as TriangulationFastKernel, IncidenceView as GenericIncidenceView, InsertionError as TriangulationInsertionError, + ManifoldError as TriangulationManifoldError, OneSidedFacetsIter as TriangulationOneSidedFacetsIter, QueryError as TriangulationQueryError, + RidgeCandidate as TriangulationRidgeCandidate, + RidgeCandidateError as TriangulationRidgeCandidateError, + RidgeHandle as TriangulationRidgeHandle, RidgeLinkView as TriangulationRidgeLinkView, + RidgeQuery as TriangulationRidgeQuery, RidgeView as TriangulationRidgeView, SimplexFacetsIter as GenericSimplexFacetsIter, SimplexNeighborIndex as GenericSimplexNeighborIndex, SpatialIndexConstructionFailure as GenericSpatialIndexConstructionFailure, @@ -191,6 +200,7 @@ use delaunay::prelude::validation::{ DelaunayValidationError as FocusedDelaunayValidationError, DelaunayViolationDetail as FocusedDelaunayViolationDetail, DelaunayViolationReport as FocusedDelaunayViolationReport, + ManifoldError as FocusedValidationManifoldError, PeriodicDomainPeriodError as FocusedPeriodicDomainPeriodError, TopologyGuarantee as FocusedValidationTopologyGuarantee, TriangulationValidationReport as FocusedValidationReport, ValidationCadence, @@ -213,7 +223,10 @@ use delaunay::prelude::{ IncidenceView as RootIncidenceView, InitialSimplexUnexpectedInsertionStage as RootInitialSimplexUnexpectedInsertionStage, PeriodicDomainPeriodError as RootPeriodicDomainPeriodError, - PlManifoldRepairStage as RootPreludePlManifoldRepairStage, SecureHashMap, SecureHashSet, + PlManifoldRepairStage as RootPreludePlManifoldRepairStage, + RidgeCandidate as RootRidgeCandidate, RidgeCandidateError as RootRidgeCandidateError, + RidgeHandle as RootRidgeHandle, RidgeLinkView as RootRidgeLinkView, + RidgeQuery as RootRidgeQuery, RidgeView as RootRidgeView, SecureHashMap, SecureHashSet, SimplexBarycenterError as RootPreludeSimplexBarycenterError, SimplexDataFillError as RootPreludeSimplexDataFillError, SimplexNeighborIndex as RootSimplexNeighborIndex, TopologyError as RootTopologyError, @@ -230,7 +243,10 @@ use delaunay::query::{ AllFacetsIter as QueryFacadeAllFacetsIter, BoundaryFacetsIter as QueryFacadeBoundaryFacetsIter, EdgeIndex as QueryFacadeEdgeIndex, FacetHandle as QueryFacadeFacetHandle, IncidenceView as QueryFacadeIncidenceView, OneSidedFacetsIter as QueryFacadeOneSidedFacetsIter, - SimplexBarycenterError as QueryFacadeSimplexBarycenterError, + RidgeCandidate as QueryFacadeRidgeCandidate, + RidgeCandidateError as QueryFacadeRidgeCandidateError, RidgeHandle as QueryFacadeRidgeHandle, + RidgeLinkView as QueryFacadeRidgeLinkView, RidgeQuery as QueryFacadeRidgeQuery, + RidgeView as QueryFacadeRidgeView, SimplexBarycenterError as QueryFacadeSimplexBarycenterError, SimplexDataFillError as QueryFacadeSimplexDataFillError, SimplexFacetsIter as QueryFacadeSimplexFacetsIter, SimplexNeighborIndex as QueryFacadeSimplexNeighborIndex, @@ -305,6 +321,8 @@ enum PreludeExportTestError { #[error(transparent)] DelaunayValidation(#[from] DelaunayValidationError), #[error(transparent)] + DelaunayTriangulationValidation(#[from] DelaunayTriangulationValidationError), + #[error(transparent)] DelaunayRepair(#[from] DelaunayRepairError), #[error(transparent)] Delaunayize(#[from] DelaunayizeError), @@ -350,10 +368,8 @@ const fn assert_bistellar_flips(_: &impl BistellarFlips<3, VertexData = ()>) {} /// Proves the root flips module exports the same public trait bound. const fn assert_root_bistellar_flips(_: &impl BistellarFlips<3, VertexData = ()>) {} -struct NonKernelMarker; - -/// Proves explicit topology edits do not require kernel-backed predicates. -const fn assert_bistellar_flips_without_kernel>() {} +/// Proves explicit topology edits are available on kernel-backed triangulations. +const fn assert_bistellar_flips_for_kernel>() {} /// Proves the focused Pachner prelude exports the unified workflow trait. const fn assert_pachner_moves(_: &impl PachnerMoves<3, VertexData = ()>) {} @@ -361,8 +377,8 @@ const fn assert_pachner_moves(_: &impl PachnerMoves<3, VertexData = ()>) {} /// Proves the root Pachner module exports the same unified workflow trait. const fn assert_root_pachner_moves(_: &impl DirectPachnerMoves<3, VertexData = ()>) {} -/// Proves unified Pachner dispatch inherits the kernel-free explicit flip contract. -const fn assert_pachner_moves_without_kernel>() {} +/// Proves unified Pachner dispatch inherits the kernel-backed explicit flip contract. +const fn assert_pachner_moves_for_kernel>() {} /// Proves the focused Pachner prelude exposes topology-owner provenance. const fn assert_pachner_topology_owner(_: &impl PachnerTopologyOwner) {} @@ -554,6 +570,68 @@ fn construction_prelude_exports_common_delaunay_error_aliases() { ); } +#[test] +fn construction_prelude_exports_low_level_delaunay_error_aliases() { + let generic_construction = GenericTriangulationConstructionError::FailedToCreateSimplex { + message: "prelude smoke test".to_owned(), + }; + assert_matches!( + DelaunayError::from(generic_construction.clone()), + DelaunayError::TriangulationConstruction { source: err } + if err.as_ref() == &generic_construction + ); + + let tds_construction = + TdsConstructionError::ValidationError(TdsError::InconsistentDataStructure { + message: "prelude smoke test".to_owned(), + }); + assert_matches!( + DelaunayError::from(tds_construction.clone()), + DelaunayError::TdsConstruction { source: err } if err.as_ref() == &tds_construction + ); + + let tds = TdsError::SimplexNotFound { + simplex_key: SimplexKey::from(KeyData::from_ffi(4)), + context: "prelude smoke test".to_owned(), + }; + assert_matches!( + DelaunayError::from(tds.clone()), + DelaunayError::Tds { source: err } if err.as_ref() == &tds + ); + + let facet = FacetError::FacetNotFoundInTriangulation; + assert_matches!( + DelaunayError::from(facet.clone()), + DelaunayError::Facet { source: err } if err.as_ref() == &facet + ); + + let simplex = SimplexValidationError::DuplicateVertices; + assert_matches!( + DelaunayError::from(simplex.clone()), + DelaunayError::SimplexValidation { source: err } if err.as_ref() == &simplex + ); + + let query = QueryError::from(TdsError::InconsistentDataStructure { + message: "prelude smoke test".to_owned(), + }); + assert_matches!( + DelaunayError::from(query.clone()), + DelaunayError::Query { source: err } if err.as_ref() == &query + ); + + let local_delaunay = DelaunayValidationError::DelaunayViolation { + simplex_key: SimplexKey::from(KeyData::from_ffi(5)), + simplex_vertices: Box::default(), + offending_vertex: None, + neighbor_simplices: Box::default(), + }; + assert_matches!( + DelaunayError::from(local_delaunay.clone()), + DelaunayError::DelaunayPropertyValidation { source: err } + if err.as_ref() == &local_delaunay + ); +} + #[test] fn construction_prelude_exports_builder_statistics_terminal() -> Result<(), PreludeExportTestError> { @@ -989,8 +1067,11 @@ fn root_exports_cover_flattened_public_api() -> Result<(), RootApiExportTestErro #[test] fn flip_exports_cover_orientation_check_stage() { - assert_bistellar_flips_without_kernel::>(); - assert_pachner_moves_without_kernel::>(); + assert_bistellar_flips_for_kernel::< + GenericTriangulation, (), (), 2>, + >(); + assert_pachner_moves_for_kernel::, (), (), 2>>( + ); assert_pachner_moves_for_unsized_trait_objects::>(); assert_matches!( DirectFlipOrientationCheckStage::BeforeMutation, @@ -1010,15 +1091,15 @@ fn flip_exports_cover_orientation_check_stage() { ); } -fn assert_edge_view_exports( - tds: &Tds<(), (), 3>, +fn assert_edge_view_exports( + dt: &DelaunayTriangulation, a: VertexKey, b: VertexKey, ) -> Result<(), PreludeExportTestError> { - let edge_key = EdgeKey::try_new(tds, a, b)?; + let edge_key = dt.edge_key(a, b)?; let query_edge_key: QueryEdgeKey = edge_key; - let edge_view: EdgeView<'_, (), (), 3> = edge_key.view(tds)?; - let query_edge_view: QueryEdgeView<'_, (), (), 3> = edge_key.view(tds)?; + let edge_view: EdgeView<'_, (), (), 3> = dt.edge_view(edge_key)?; + let query_edge_view: QueryEdgeView<'_, (), (), 3> = dt.edge_view(edge_key)?; assert_eq!(query_edge_key, edge_key); assert_eq!(edge_view.key(), edge_key); @@ -1027,34 +1108,34 @@ fn assert_edge_view_exports( Ok(()) } -fn assert_simplex_facet_iter_exports( - tds: &Tds<(), (), 3>, +fn assert_simplex_facet_iter_exports( + dt: &DelaunayTriangulation, simplex_key: SimplexKey, ) -> Result<(), FacetError> { let _query_facade_simplex_facets: QueryFacadeSimplexFacetsIter<'_, (), (), 3> = - tds.try_simplex_facets(simplex_key)?; + dt.simplex_facets(simplex_key)?; let _query_simplex_facets: QuerySimplexFacetsIter<'_, (), (), 3> = - tds.try_simplex_facets(simplex_key)?; + dt.simplex_facets(simplex_key)?; let _tds_simplex_facets: TdsSimplexFacetsIter<'_, (), (), 3> = - tds.try_simplex_facets(simplex_key)?; + dt.simplex_facets(simplex_key)?; let _triangulation_simplex_facets: GenericSimplexFacetsIter<'_, (), (), 3> = - tds.try_simplex_facets(simplex_key)?; + dt.simplex_facets(simplex_key)?; Ok(()) } -fn assert_facet_incidence_exports( - tds: &Tds<(), (), 3>, +fn assert_facet_incidence_exports( + dt: &DelaunayTriangulation, simplex_key: SimplexKey, ) -> Result<(), PreludeExportTestError> { - let facet_handle = FacetHandle::try_new(tds, simplex_key, 0)?; + let facet_handle = dt.facet_handle(simplex_key, 0)?; let query_facet_handle: QueryFacetHandle = facet_handle; let query_facade_facet_handle: QueryFacadeFacetHandle = facet_handle; - let facet_view: FacetView<'_, (), (), 3> = facet_handle.view(tds)?; + let facet_view: FacetView<'_, (), (), 3> = dt.facet_view(facet_handle)?; assert_eq!(facet_view.handle(), facet_handle); assert_eq!(query_facet_handle, facet_handle); assert_eq!(query_facade_facet_handle, facet_handle); - let facet_index = tds.build_facet_to_simplices_index()?; + let facet_index = dt.facet_incidence_index()?; let incidence = facet_index .get(&facet_view.key()) .expect("fresh index should contain the facet view key"); @@ -1065,7 +1146,8 @@ fn assert_facet_incidence_exports( assert_eq!(root_incidence.facet_key(), query_incidence.facet_key()); assert_eq!(query_incidence.facet_key(), tds_incidence.facet_key()); assert!(tds_incidence.is_one_sided()); - assert_query_facet_incidence_trait_export(tds); + let empty_tds: Tds<(), (), 3> = Tds::empty(); + assert_query_facet_incidence_trait_export(&empty_tds); let query_facade_one_sided_facets: Option> = None; let query_one_sided_facets: Option> = None; @@ -1076,8 +1158,8 @@ fn assert_facet_incidence_exports( assert!(query_one_sided_facets.is_none()); assert!(tds_one_sided_facets.is_none()); assert!(triangulation_one_sided_facets.is_none()); - let one_sided_count = tds - .one_sided_facets()? + let one_sided_count = dt + .boundary_facets()? .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; assert!(one_sided_count > 0); @@ -1090,6 +1172,31 @@ fn assert_facet_incidence_exports( Ok(()) } +fn assert_ridge_handle_exports( + dt: &DelaunayTriangulation, + simplex_key: SimplexKey, +) -> Result<(), PreludeExportTestError> { + let ridge = dt.ridge_handle(simplex_key, 0, 1)?; + let query_ridge: QueryRidgeHandle = ridge; + let query_facade_ridge: QueryFacadeRidgeHandle = ridge; + let root_ridge: RootRidgeHandle = ridge; + let triangulation_ridge: TriangulationRidgeHandle = ridge; + + assert_eq!(query_ridge, ridge); + assert_eq!(query_facade_ridge, ridge); + assert_eq!(root_ridge, ridge); + assert_eq!(triangulation_ridge, ridge); + + let iter_ridge = dt + .ridge_handles() + .next() + .transpose()? + .expect("constructed tetrahedron should expose at least one ridge"); + let query_iter_ridge: QueryRidgeHandle = iter_ridge; + assert_eq!(query_iter_ridge.simplex_key(), iter_ridge.simplex_key()); + Ok(()) +} + fn assert_export_prelude_exports( dt: &DelaunayTriangulation, ) -> Result<(), PreludeExportTestError> { @@ -1128,15 +1235,11 @@ fn assert_export_prelude_exports( Ok(()) } -fn assert_insertion_prelude_empty_tds_exports() -> Result<(), PreludeExportTestError> { - let mut empty_tds: InsertionTds<(), (), 2> = InsertionTds::empty(); +fn assert_insertion_prelude_empty_tds_exports() { + let empty_tds: Tds<(), (), 2> = Tds::empty(); let _tds_all_facets: TdsAllFacetsIter<'_, (), (), 2> = empty_tds.facets(); let tds_boundary_facets: Option> = None; assert!(tds_boundary_facets.is_none()); - assert_eq!( - repair_neighbor_pointers_local(&mut empty_tds, &[], None)?, - 0 - ); assert_eq!( CavityRepairStage::PrimaryInsertion.to_string(), "primary insertion" @@ -1146,7 +1249,54 @@ fn assert_insertion_prelude_empty_tds_exports() -> Result<(), PreludeExportTestE ValidationCadence::from_optional_every(Some(128)), ValidationCadence::EveryN(every) if every.get() == 128 ); - Ok(()) +} + +fn assert_misc_bench_prelude_exports() { + assert_insertion_prelude_empty_tds_exports(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert!(NeighborSlot::Boundary.is_boundary()); + assert_eq!( + DegenerateSimplexReason::ZeroOrientation.to_string(), + "zero orientation" + ); + assert_matches!( + MatrixError::OutOfBounds { + row: 1, + column: 2, + dimension: 3 + }, + MatrixError::OutOfBounds { .. } + ); + + let mut root_secure_map: SecureHashMap<[u64; 2], usize> = SecureHashMap::default(); + root_secure_map.insert([1, 2], 3); + assert_eq!(root_secure_map.get(&[1, 2]), Some(&3)); + + let mut root_secure_set: SecureHashSet<[u64; 2]> = SecureHashSet::default(); + root_secure_set.insert([1, 2]); + assert!(root_secure_set.contains(&[1, 2])); + + let mut scoped_secure_map: ScopedSecureHashMap<[u64; 2], usize> = + ScopedSecureHashMap::default(); + scoped_secure_map.insert([3, 4], 5); + assert_eq!(scoped_secure_map.get(&[3, 4]), Some(&5)); + + let mut scoped_secure_set: ScopedSecureHashSet<[u64; 2]> = ScopedSecureHashSet::default(); + scoped_secure_set.insert([3, 4]); + assert!(scoped_secure_set.contains(&[3, 4])); + + let telemetry = ConstructionTelemetry::default(); + assert!(!telemetry.has_data()); } #[test] @@ -1181,9 +1331,27 @@ fn preludes_cover_bench_apis() -> Result<(), PreludeExportTestError> { .simplices() .next() .expect("constructed tetrahedron should contain a simplex"); - assert_edge_view_exports(dt.tds(), simplex.vertices()[0], simplex.vertices()[1])?; - assert_simplex_facet_iter_exports(dt.tds(), simplex_key)?; - assert_facet_incidence_exports(dt.tds(), simplex_key)?; + let simplex_uuid = dt + .simplex_uuid_from_key(simplex_key) + .expect("simplex key should resolve to a UUID"); + assert_eq!(dt.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + assert!( + dt.simplex_uuids() + .any(|(key, uuid)| key == simplex_key && uuid == simplex_uuid) + ); + let vertex_key = simplex.vertices()[0]; + let vertex_uuid = dt + .vertex_uuid_from_key(vertex_key) + .expect("simplex vertex key should resolve to a UUID"); + assert_eq!(dt.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + assert!( + dt.vertex_uuids() + .any(|(key, uuid)| key == vertex_key && uuid == vertex_uuid) + ); + assert_edge_view_exports(&dt, simplex.vertices()[0], simplex.vertices()[1])?; + assert_simplex_facet_iter_exports(&dt, simplex_key)?; + assert_facet_incidence_exports(&dt, simplex_key)?; + assert_ridge_handle_exports(&dt, simplex_key)?; let boundary_facet_count = dt.boundary_facets()?.try_fold(0_usize, |count, facet| { facet .map(|_| count + 1) @@ -1198,58 +1366,26 @@ fn preludes_cover_bench_apis() -> Result<(), PreludeExportTestError> { .try_facets(dt.as_triangulation())? .try_fold(0_usize, |count, facet| facet.map(|_| count + 1))?; assert_eq!(hull_facet_view_count, boundary_facet_count); - dt.validate().unwrap(); - assert_tds_topology_owner(dt.tds()); + let generic_ridge_link_result: Result<(), TriangulationManifoldError> = + dt.as_triangulation().validate_ridge_links(); + generic_ridge_link_result?; + let generic_vertex_link_result: Result<(), TriangulationManifoldError> = + dt.as_triangulation().validate_vertex_links(); + generic_vertex_link_result?; + let focused_validation_link_result: Result<(), FocusedValidationManifoldError> = + dt.validate_ridge_links(); + focused_validation_link_result?; + dt.validate_ridge_links()?; + dt.validate_vertex_links()?; + dt.validate()?; + let empty_tds: Tds<(), (), 3> = Tds::empty(); + assert_tds_topology_owner(&empty_tds); assert_bistellar_flips(&dt); assert_pachner_prelude_exports(&dt, simplex_key)?; assert_send_sync_unpin::>(); assert_send_sync_unpin::(); assert_send_sync_unpin::(); - - assert_insertion_prelude_empty_tds_exports()?; - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert_send_sync_unpin::(); - assert!(NeighborSlot::Boundary.is_boundary()); - assert_eq!( - DegenerateSimplexReason::ZeroOrientation.to_string(), - "zero orientation" - ); - assert_matches!( - MatrixError::OutOfBounds { - row: 1, - column: 2, - dimension: 3 - }, - MatrixError::OutOfBounds { .. } - ); - let mut root_secure_map: SecureHashMap<[u64; 2], usize> = SecureHashMap::default(); - root_secure_map.insert([1, 2], 3); - assert_eq!(root_secure_map.get(&[1, 2]), Some(&3)); - - let mut root_secure_set: SecureHashSet<[u64; 2]> = SecureHashSet::default(); - root_secure_set.insert([1, 2]); - assert!(root_secure_set.contains(&[1, 2])); - - let mut scoped_secure_map: ScopedSecureHashMap<[u64; 2], usize> = - ScopedSecureHashMap::default(); - scoped_secure_map.insert([3, 4], 5); - assert_eq!(scoped_secure_map.get(&[3, 4]), Some(&5)); - - let mut scoped_secure_set: ScopedSecureHashSet<[u64; 2]> = ScopedSecureHashSet::default(); - scoped_secure_set.insert([3, 4]); - assert!(scoped_secure_set.contains(&[3, 4])); - - let telemetry = ConstructionTelemetry::default(); - assert!(!telemetry.has_data()); + assert_misc_bench_prelude_exports(); Ok(()) } @@ -1802,21 +1938,60 @@ fn degenerate_prelude_vertices() Ok(vertices) } +fn assert_ridge_query_type_exports() { + let _query_candidate_size = size_of::>(); + let _query_facade_candidate_size = size_of::>(); + let _root_candidate_size = size_of::>(); + let _triangulation_candidate_size = size_of::>(); + let _query_query_size = size_of::>(); + let _query_facade_query_size = size_of::>(); + let _root_query_size = size_of::>(); + let _triangulation_query_size = size_of::>(); + let _query_view_size = size_of::>(); + let _query_facade_view_size = size_of::>(); + let _root_view_size = size_of::>(); + let _triangulation_view_size = size_of::>(); + let _query_link_size = size_of::>(); + let _query_facade_link_size = size_of::>(); + let _root_link_size = size_of::>(); + let _triangulation_link_size = size_of::>(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); +} + fn assert_single_simplex_ridge_star( vertices: &[Vertex<(), D>], ) -> Result<(), PreludeExportTestError> { + assert_ridge_query_type_exports::(); let dt = DelaunayTriangulation::builder(vertices).build()?; - let ridge = RidgeCandidate::::try_from_vertices(dt.tds().vertex_keys().take(D - 1))?; - let star = ridge_star_simplices(dt.tds(), &ridge)?; - let ridge_query: RidgeQuery<'_, (), (), D> = ridge.query(dt.tds())?; + let ridge: QueryRidgeCandidate = + RidgeCandidate::::try_from_vertices(dt.vertices().map(|(key, _)| key).take(D - 1))?; + let star = dt.ridge_star_simplices(&ridge)?; + let ridge_query: RidgeQuery<'_, (), (), D> = dt.ridge_query(&ridge)?; + let query_ridge_query: QueryRidgeQuery<'_, (), (), D> = dt.ridge_query(&ridge)?; + let query_facade_ridge_query: QueryFacadeRidgeQuery<'_, (), (), D> = dt.ridge_query(&ridge)?; + let root_ridge_query: RootRidgeQuery<'_, (), (), D> = dt.ridge_query(&ridge)?; + let triangulation_ridge_query: TriangulationRidgeQuery<'_, (), (), D> = + dt.as_triangulation().ridge_query(&ridge)?; let query_star = ridge_query.incident_simplices(); - let ridge_view: RidgeView<'_, (), (), D> = ridge.view(dt.tds())?; + let ridge_view: RidgeView<'_, (), (), D> = dt.ridge_view(&ridge)?; + let query_ridge_view: QueryRidgeView<'_, (), (), D> = dt.ridge_view(&ridge)?; + let query_facade_ridge_view: QueryFacadeRidgeView<'_, (), (), D> = dt.ridge_view(&ridge)?; + let root_ridge_view: RootRidgeView<'_, (), (), D> = dt.ridge_view(&ridge)?; + let triangulation_ridge_view: TriangulationRidgeView<'_, (), (), D> = + dt.as_triangulation().ridge_view(&ridge)?; let view_star = ridge_view.incident_simplices(); let ridge_vertices = ridge_view.vertices(); let ridge_links = ridge_view.links()?; let ridge_link: &RidgeLinkView<'_, (), (), D> = ridge_links .first() .expect("simplex ridge should have a link"); + let query_ridge_link: &QueryRidgeLinkView<'_, (), (), D> = ridge_link; + let query_facade_ridge_link: &QueryFacadeRidgeLinkView<'_, (), (), D> = ridge_link; + let root_ridge_link: &RootRidgeLinkView<'_, (), (), D> = ridge_link; + let triangulation_ridge_link: &TriangulationRidgeLinkView<'_, (), (), D> = ridge_link; let link_edges = ridge_link.edges(); let link_edge: &LiftedLinkEdge = link_edges .first() @@ -1826,10 +2001,40 @@ fn assert_single_simplex_ridge_star( assert_eq!(star.len(), 1); assert_eq!(query_star.len(), star.len()); + assert_eq!(query_ridge_query.incident_simplices().len(), star.len()); + assert_eq!( + query_facade_ridge_query.incident_simplices().len(), + star.len() + ); + assert_eq!(root_ridge_query.incident_simplices().len(), star.len()); + assert_eq!( + triangulation_ridge_query.incident_simplices().len(), + star.len() + ); assert_eq!(view_star.len(), star.len()); + assert_eq!(query_ridge_view.incident_simplices().len(), star.len()); + assert_eq!( + query_facade_ridge_view.incident_simplices().len(), + star.len() + ); + assert_eq!(root_ridge_view.incident_simplices().len(), star.len()); + assert_eq!( + triangulation_ridge_view.incident_simplices().len(), + star.len() + ); assert_eq!(ridge_vertices.len(), D - 1); assert_eq!(ridge_links.len(), 1); assert_eq!(ridge_link.incident_simplices().len(), star.len()); + assert_eq!(query_ridge_link.incident_simplices().len(), star.len()); + assert_eq!( + query_facade_ridge_link.incident_simplices().len(), + star.len() + ); + assert_eq!(root_ridge_link.incident_simplices().len(), star.len()); + assert_eq!( + triangulation_ridge_link.incident_simplices().len(), + star.len() + ); assert_eq!(first_endpoint.vertex_key(), link_edge.vertex_keys().0); assert_eq!(link_edges.len(), star.len()); Ok(()) @@ -1838,8 +2043,9 @@ fn assert_single_simplex_ridge_star( fn assert_cospherical_ridge_star() -> Result<(), PreludeExportTestError> { let vertices = cospherical_prelude_vertices::()?; let dt = DelaunayTriangulation::builder(&vertices).build()?; - let ridge = RidgeCandidate::::try_from_vertices(dt.tds().vertex_keys().take(D - 1))?; - let star = ridge_star_simplices(dt.tds(), &ridge)?; + let ridge = + RidgeCandidate::::try_from_vertices(dt.vertices().map(|(key, _)| key).take(D - 1))?; + let star = dt.ridge_star_simplices(&ridge)?; assert!(!star.is_empty()); Ok(()) @@ -1871,7 +2077,7 @@ fn assert_topology_prelude_dimension() -> Result<(), PreludeExpo assert_single_simplex_ridge_star(&near_boundary_vertices)?; let dt = DelaunayTriangulation::builder(&simplex_vertices).build()?; - let keys = dt.tds().vertex_keys().collect::>(); + let keys = dt.vertices().map(|(key, _)| key).collect::>(); assert_ridge_candidate_reject_adversarial_keys::(&keys); assert_cospherical_ridge_star::()?; @@ -2177,7 +2383,7 @@ fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> }; assert!(validation_error.to_string().contains("vertex removal")); - verify_delaunay_for_triangulation(dt.as_triangulation())?; + dt.verify_via_flip_predicates()?; let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; assert!(!outcome.used_fallback_rebuild); diff --git a/tests/proptest_convex_hull.rs b/tests/proptest_convex_hull.rs index 391ae7e8..f3643e6c 100644 --- a/tests/proptest_convex_hull.rs +++ b/tests/proptest_convex_hull.rs @@ -24,6 +24,13 @@ fn finite_coordinate() -> impl Strategy { (-100.0..100.0).prop_filter("must be finite", |x: &f64| x.is_finite()) } +fn count_boundary_facets(dt: &DelaunayTriangulation) -> usize { + dt.boundary_facets() + .expect("boundary facets should be queryable for valid triangulations") + .try_fold(0_usize, |count, facet| facet.map(|_| count + 1)) + .expect("boundary facet handles should resolve") +} + // ============================================================================= // DIMENSIONAL TEST GENERATION MACROS // ============================================================================= @@ -107,7 +114,7 @@ macro_rules! test_convex_hull_properties { // Filter: Skip degenerate configurations (no boundary facets) // These are tested separately in dedicated degenerate case tests - let boundary_count = dt.tds().number_of_one_sided_facets().unwrap(); + let boundary_count = count_boundary_facets(&dt); prop_assume!(boundary_count > 0); // Should be able to construct hull from valid triangulation @@ -152,7 +159,7 @@ macro_rules! test_convex_hull_properties { let hull = hull_result.expect("assumed hull construction"); let facet_count = hull.number_of_facets(); - let vertex_count = dt.tds().vertices().count(); + let vertex_count = dt.number_of_vertices(); // Lower bound: more than D facets for a simplex in D dimensions let min_facets = $dim; @@ -200,8 +207,7 @@ macro_rules! test_convex_hull_properties { let mut dt = dt_result.expect("assumed valid random triangulation"); // Filter: Skip degenerate initial configurations - let initial_boundary_count = - dt.tds().number_of_one_sided_facets().unwrap(); + let initial_boundary_count = count_boundary_facets(&dt); prop_assume!(initial_boundary_count > 0); let hull_result = ConvexHull::try_from_triangulation(dt.as_triangulation()); @@ -221,8 +227,7 @@ macro_rules! test_convex_hull_properties { prop_assume!(dt.insert_vertex(new_vertex[0]).is_ok()); // Filter: Skip if modification resulted in degenerate configuration - let modified_boundary_count = - dt.tds().number_of_one_sided_facets().unwrap(); + let modified_boundary_count = count_boundary_facets(&dt); prop_assume!(modified_boundary_count > 0); // Hull should now be invalid (stale) @@ -264,7 +269,7 @@ macro_rules! test_convex_hull_properties { prop_assume!(hull_result.is_ok()); let hull = hull_result.expect("assumed hull construction"); - let tds_vertex_count = dt.tds().vertices().count(); + let triangulation_vertex_count = dt.number_of_vertices(); let facet_count = hull.number_of_facets(); // Each facet references D vertices (D-dimensional facets in D-space) @@ -283,10 +288,10 @@ macro_rules! test_convex_hull_properties { // so we just check facet count is reasonable) prop_assert!( facet_count > $dim, - "{}D hull should have more than {} facets for {} TDS vertices", + "{}D hull should have more than {} facets for {} triangulation vertices", $dim, $dim, - tds_vertex_count + triangulation_vertex_count ); } diff --git a/tests/proptest_delaunay_triangulation.rs b/tests/proptest_delaunay_triangulation.rs index 8ef402ba..4ea2d58d 100644 --- a/tests/proptest_delaunay_triangulation.rs +++ b/tests/proptest_delaunay_triangulation.rs @@ -27,7 +27,7 @@ //! //! ## Performance Note //! -//! Delaunay property validation now uses `verify_delaunay_via_flip_predicates()` which checks +//! Delaunay property validation now uses `DelaunayTriangulation::verify_via_flip_predicates()` which checks //! local flip configurations (O(simplices)) instead of the naive O(simplices × vertices) brute-force. //! This provides ~40-100x speedup for property-based testing while remaining equally correct. @@ -1001,7 +1001,7 @@ proptest! { // Verify the triangulation satisfies the Delaunay property (Level 5) // Use fast O(N) flip-based verification instead of O(N×V) brute-force - let delaunay_result = dt.is_delaunay_via_flips(); + let delaunay_result = dt.verify_via_flip_predicates(); prop_assert!( delaunay_result.is_ok(), "{}D triangulation should satisfy Delaunay property: {:?}", @@ -1096,7 +1096,7 @@ macro_rules! gen_high_dim_delaunay_smoke { }; prop_assert_levels_1_to_3_valid!($dim, &dt, "active smoke construction"); - let delaunay_result = dt.is_delaunay_via_flips(); + let delaunay_result = dt.verify_via_flip_predicates(); prop_assert!( delaunay_result.is_ok(), "{}D active smoke triangulation should satisfy Level 5 Delaunay validation: {:?}", diff --git a/tests/proptest_euler_characteristic.rs b/tests/proptest_euler_characteristic.rs index 2bd308a9..16d58d75 100644 --- a/tests/proptest_euler_characteristic.rs +++ b/tests/proptest_euler_characteristic.rs @@ -6,7 +6,7 @@ //! ## Test Properties //! //! 1. **Euler Formula Consistency**: Computed χ matches expected value for classification -//! 2. **Simplex Count Validity**: Vertex and simplex counts match Tds counts +//! 2. **Simplex Count Validity**: Vertex and simplex counts match owner counts //! 3. **Classification Consistency**: Expected χ for classification matches computed χ //! //! ## Notes @@ -18,7 +18,6 @@ use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee}; use delaunay::prelude::generators::try_generate_random_triangulation_with_topology_guarantee; -use delaunay::topology::characteristics::{euler, validation}; use delaunay::vertex; use proptest::prelude::*; use std::num::NonZeroUsize; @@ -78,8 +77,7 @@ macro_rules! test_euler_properties { .build() { // Validate Euler characteristic - let result = - validation::validate_triangulation_euler(dt.tds(), dt.global_topology())?; + let result = dt.euler_check()?; // Core property: χ must match expected value for the topology @@ -110,7 +108,7 @@ macro_rules! test_euler_properties { .topology_guarantee(TopologyGuarantee::PLManifold) .build() { - let counts = euler::count_simplices(dt.tds())?; + let counts = dt.simplex_counts()?; // Basic sanity checks prop_assert_eq!( @@ -150,8 +148,7 @@ macro_rules! test_euler_properties { .topology_guarantee(TopologyGuarantee::PLManifold) .build() { - let result = - validation::validate_triangulation_euler(dt.tds(), dt.global_topology())?; + let result = dt.euler_check()?; // If we have an expected χ, computed χ must match if let Some(expected_chi) = result.expected { @@ -182,8 +179,7 @@ fn test_seeded_random_generator_euler_consistent() { TopologyGuarantee::PLManifold, ) .unwrap(); - let result_2d = - validation::validate_triangulation_euler(dt_2d.tds(), dt_2d.global_topology()).unwrap(); + let result_2d = dt_2d.euler_check().unwrap(); assert!( result_2d.is_valid(), "2D seeded random triangulation Euler mismatch: χ={}, expected={:?}, classification={:?}, V={}, simplices={}", @@ -202,8 +198,7 @@ fn test_seeded_random_generator_euler_consistent() { TopologyGuarantee::PLManifold, ) .unwrap(); - let result_3d = - validation::validate_triangulation_euler(dt_3d.tds(), dt_3d.global_topology()).unwrap(); + let result_3d = dt_3d.euler_check().unwrap(); assert!( result_3d.is_valid(), "3D seeded random triangulation Euler mismatch: χ={}, expected={:?}, classification={:?}, V={}, simplices={}", diff --git a/tests/proptest_facet.rs b/tests/proptest_facet.rs index 67c00318..f9508bf6 100644 --- a/tests/proptest_facet.rs +++ b/tests/proptest_facet.rs @@ -10,7 +10,6 @@ use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee}; use delaunay::prelude::query::*; -use delaunay::prelude::tds::facet_key_from_vertices; use delaunay::try_vertices_from_points; use proptest::prelude::*; use std::collections::HashMap; @@ -43,21 +42,16 @@ macro_rules! test_facet_properties { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - let tds = dt.tds(); - for simplex_key in tds.simplex_keys() { - // Each simplex has D+1 facets (one opposite each vertex) - for facet_index in 0..=($dim as u8) { - if let Ok(facet) = FacetView::try_new(&tds, simplex_key, facet_index) { - let vertex_count = facet.vertices().count(); - prop_assert_eq!( - vertex_count, - $expected_facet_vertices, - "{}D facet should have exactly {} vertices", - $dim, - $expected_facet_vertices - ); - } - } + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + let vertex_count = facet.vertices().count(); + prop_assert_eq!( + vertex_count, + $expected_facet_vertices, + "{}D facet should have exactly {} vertices", + $dim, + $expected_facet_vertices + ); } } } @@ -72,26 +66,16 @@ macro_rules! test_facet_properties { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - let tds = dt.tds(); - for simplex_key in tds.simplex_keys() { - prop_assert!( - tds.simplex(simplex_key).is_some(), - "simplex key from iterator should exist: {simplex_key:?}" + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + let simplex_vertex_count = facet.simplex().vertices().len(); + let facet_vertex_count = facet.vertices().count(); + prop_assert_eq!( + facet_vertex_count, + simplex_vertex_count - 1, + "{}D facet should have one fewer vertex than simplex", + $dim ); - let simplex = tds.simplex(simplex_key).expect("checked above"); - let simplex_vertex_count = simplex.vertices().len(); - - for facet_index in 0..=($dim as u8) { - if let Ok(facet) = FacetView::try_new(&tds, simplex_key, facet_index) { - let facet_vertex_count = facet.vertices().count(); - prop_assert_eq!( - facet_vertex_count, - simplex_vertex_count - 1, - "{}D facet should have one fewer vertex than simplex", - $dim - ); - } - } } } } @@ -106,18 +90,12 @@ macro_rules! test_facet_properties { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - let tds = dt.tds(); - // Check that each facet is valid - for simplex_key in tds.simplex_keys() { - for facet_index in 0..=($dim as u8) { - // Each facet should be constructible - prop_assert!( - FacetView::try_new(&tds, simplex_key, facet_index).is_ok(), - "{}D facet {} of simplex should be valid", - $dim, - facet_index - ); - } + for facet in dt.facets() { + prop_assert!( + facet.is_ok(), + "{}D public facet iterator should only yield valid facets", + $dim + ); } } } @@ -132,22 +110,17 @@ macro_rules! test_facet_properties { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - let tds = dt.tds(); - for simplex_key in tds.simplex_keys() { - let mut facet_count = 0; - for facet_index in 0..=($dim as u8) { - if FacetView::try_new(&tds, simplex_key, facet_index).is_ok() { - facet_count += 1; - } - } - prop_assert_eq!( - facet_count, - $dim + 1, - "{}D simplex should have exactly {} facets", - $dim, - $dim + 1 - ); - } + let facet_count = dt + .facets() + .try_fold(0_usize, |count, facet| facet.map(|_| count + 1)) + .expect("facet iterator should resolve valid facets"); + prop_assert_eq!( + facet_count, + dt.number_of_simplices() * ($dim + 1), + "{}D triangulation should have exactly {} facets per simplex", + $dim, + $dim + 1 + ); } } } @@ -176,24 +149,14 @@ macro_rules! test_facet_multiplicity { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - let tds = dt.tds(); // Ensure we're checking a valid triangulation to avoid degenerate edge cases - prop_assume!(tds.is_valid().is_ok()); + prop_assume!(dt.is_valid_structure().is_ok()); let mut counts: HashMap = HashMap::new(); - for (_simplex_key, simplex) in tds.simplices() { - let vs = simplex.vertices(); - for i in 0..vs.len() { - let facet: Vec<_> = vs - .iter() - .copied() - .enumerate() - .filter_map(|(j, vk)| (j != i).then_some(vk)) - .collect(); - let key = facet_key_from_vertices(&facet); - *counts.entry(key).or_default() += 1; - } + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + *counts.entry(facet.key()).or_default() += 1; } for (facet, c) in counts { diff --git a/tests/proptest_flips.rs b/tests/proptest_flips.rs index 48c6d9c9..2def993c 100644 --- a/tests/proptest_flips.rs +++ b/tests/proptest_flips.rs @@ -258,7 +258,7 @@ fn check_k1_roundtrip( )) })?; - let mut triangulation = simplex.as_triangulation().clone(); + let mut triangulation = simplex.into_triangulation(); assert_valid(&triangulation, "initial")?; assert_positive_simplex_orientations(&triangulation, "before k=1 insertion")?; let before = snapshot_topology(&triangulation)?; diff --git a/tests/proptest_orientation.rs b/tests/proptest_orientation.rs index 3c07ce51..40fa4215 100644 --- a/tests/proptest_orientation.rs +++ b/tests/proptest_orientation.rs @@ -39,7 +39,7 @@ macro_rules! gen_orientation_construction_and_tamper_props { ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { prop_assert!( - dt.tds().is_coherently_oriented(), + dt.is_coherently_oriented(), "{}D: constructed triangulation must be coherently oriented", $dim ); @@ -63,10 +63,10 @@ macro_rules! gen_orientation_construction_and_tamper_props { }) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - prop_assume!(dt.tds().number_of_simplices() >= 2); - prop_assert!(dt.tds().is_coherently_oriented()); + prop_assume!(dt.number_of_simplices() >= 2); + prop_assert!(dt.is_coherently_oriented()); - let mut serialized = serde_json::to_value(dt.tds()).unwrap(); + let mut serialized = serde_json::to_value(&dt).unwrap(); let simplex_vertices_map = serialized .get_mut("simplex_vertices") .and_then(serde_json::Value::as_object_mut) @@ -140,7 +140,7 @@ macro_rules! gen_orientation_incremental_props { let result = dt.insert_best_effort_with_statistics(vertex); if let Ok((InsertionOutcome::Inserted { .. }, _stats)) = result { prop_assert!( - dt.tds().is_coherently_oriented(), + dt.is_coherently_oriented(), "{}D: orientation must remain coherent after successful insertion", $dim ); diff --git a/tests/proptest_serialization.rs b/tests/proptest_serialization.rs index e163ef4a..a08d973f 100644 --- a/tests/proptest_serialization.rs +++ b/tests/proptest_serialization.rs @@ -105,7 +105,7 @@ macro_rules! test_serialization_properties { ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - if dt.tds().validate().is_ok() { + if dt.validate_structure().is_ok() { // Serialize and deserialize via try_from_tds let json = serde_json::to_string(&dt).expect("Serialization failed"); let tds: Tds<(), (), $dim> = @@ -116,10 +116,10 @@ macro_rules! test_serialization_properties { // Deserialized triangulation should also be valid prop_assert!( - deserialized.tds().validate().is_ok(), + deserialized.validate_structure().is_ok(), "{}D deserialized triangulation should be valid: {:?}", $dim, - deserialized.tds().validate().err() + deserialized.validate_structure().err() ); } } @@ -139,7 +139,7 @@ macro_rules! test_serialization_properties { // Need more than minimal simplex (D+1) to have meaningful serialization test prop_assume!(dt.number_of_vertices() > $dim + 1); // Also skip invalid TDS (can happen with nearly-degenerate geometries) - prop_assume!(dt.tds().validate().is_ok()); + prop_assume!(dt.validate_structure().is_ok()); // Collect original vertex points let original_points: Vec<_> = dt.vertices() diff --git a/tests/proptest_simplex.rs b/tests/proptest_simplex.rs index 2242e85a..8312f26e 100644 --- a/tests/proptest_simplex.rs +++ b/tests/proptest_simplex.rs @@ -105,14 +105,14 @@ macro_rules! test_simplex_properties { /// Property: Simplices retrieved from a valid triangulation should pass validation $(#[$attr])* #[test] - fn []( + fn []( vertices in prop::collection::vec( prop::array::[](finite_coordinate()).prop_map(|coords| Point::try_new(coords).expect("finite point coordinates")), $min_vertices..=$max_vertices ).prop_map(|v| try_vertices_from_points(&v).expect("finite point coordinates")) ) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).topology_guarantee(TopologyGuarantee::PLManifold).build() { - if dt.tds().validate().is_ok() { + if dt.validate_structure().is_ok() { for (_simplex_key, simplex) in dt.simplices() { prop_assert_eq!(simplex.vertices().len(), $expected_vertices); prop_assert!(simplex.uuid().as_u128() != 0); diff --git a/tests/proptest_tds.rs b/tests/proptest_tds.rs index 1f1f5def..02193b9f 100644 --- a/tests/proptest_tds.rs +++ b/tests/proptest_tds.rs @@ -19,7 +19,8 @@ //! - **Vertex count consistency** - Vertex key count matches reported vertex count //! - **Dimension consistency** - Reported dimension matches actual structure //! -//! All tests use `dt.tds().is_valid()` (Level 2 structural validation). +//! Tests that construct through the Delaunay owner use owner-level Level 2 +//! structural validation instead of borrowing the underlying storage. use delaunay::prelude::collections::{SimplexVertexBuffer, SimplexVertexKeyBuffer}; use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; @@ -116,9 +117,9 @@ macro_rules! gen_tds_validity { #[test] fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { - prop_assert!(dt.tds().is_valid().is_ok(), + prop_assert!(dt.is_valid_structure().is_ok(), "{}D Tds should be valid: {:?}", - $dim, dt.tds().is_valid().err()); + $dim, dt.is_valid_structure().err()); } } } @@ -134,19 +135,18 @@ macro_rules! gen_neighbor_symmetry { #[test] fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { - let tds = dt.tds(); for (simplex_key, simplex) in dt.simplices() { if let Some(neighbors) = simplex.neighbors() { let simplex_neighbors: HashSet<_> = neighbors.flatten().collect(); for neighbor_key in &simplex_neighbors { - let found_reciprocal = tds + let found_reciprocal = dt .simplex(*neighbor_key) .and_then(|c| c.neighbors()) .is_some_and(|mut nn| nn.any(|n| n == Some(simplex_key))); if !found_reciprocal { // Enhanced diagnostics with Jaccard similarity - let neighbor_neighbors: HashSet<_> = tds + let neighbor_neighbors: HashSet<_> = dt .simplex(*neighbor_key) .and_then(|c| c.neighbors()) .map(|nn| nn.flatten().collect()) @@ -195,14 +195,13 @@ macro_rules! gen_neighbor_index_semantics { fn [](vertices in []()) { // Use stack-allocated buffer for D facet vertices (D ≤ 7 typical) if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { - prop_assume!(dt.tds().is_valid().is_ok()); - let tds = dt.tds(); + prop_assume!(dt.is_valid_structure().is_ok()); for (simplex_key, simplex) in dt.simplices() { if let Some(neighbors) = simplex.neighbors() { let a_vertices = simplex.vertices(); for (i, nb) in neighbors.enumerate() { if let Some(b_key) = nb { - let b_simplex = tds.simplex(b_key).unwrap(); + let b_simplex = dt.simplex(b_key).unwrap(); let b_vertices = b_simplex.vertices(); let mut a_facet: SimplexVertexBuffer<_> = a_vertices.iter().enumerate() .filter_map(|(idx, &vk)| (idx != i).then_some(vk)) @@ -240,7 +239,7 @@ macro_rules! gen_simplex_vertices_exist_in_tds { #[test] fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { - let all_vertex_keys: HashSet<_> = dt.tds().vertex_keys().collect(); + let all_vertex_keys: HashSet<_> = dt.vertices().map(|(key, _)| key).collect(); for (_simplex_key, simplex) in dt.simplices() { for vertex_key in simplex.vertices() { prop_assert!(all_vertex_keys.contains(vertex_key), @@ -302,7 +301,7 @@ macro_rules! gen_vertex_count_consistency { #[test] fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { - let keys = dt.tds().vertex_keys().count(); + let keys = dt.vertices().count(); let n = dt.number_of_vertices(); prop_assert_eq!(keys, n, "{}D vertex keys count should match number_of_vertices", $dim); } @@ -441,10 +440,10 @@ macro_rules! gen_is_connected { fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { prop_assert!( - dt.tds().is_connected(), + dt.as_triangulation().is_valid_topology().is_ok(), "{}D successfully-built triangulation must be connected ({} simplices)", $dim, - dt.tds().number_of_simplices() + dt.number_of_simplices() ); } } @@ -505,15 +504,14 @@ macro_rules! gen_high_dim_tds_smoke { } }; - let tds = dt.tds(); prop_assert!( - tds.is_valid().is_ok(), + dt.is_valid_structure().is_ok(), "{}D active TDS smoke should pass structural validation: {:?}", $dim, - tds.is_valid().err() + dt.is_valid_structure().err() ); prop_assert!( - tds.is_connected(), + dt.as_triangulation().is_valid_topology().is_ok(), "{}D active TDS smoke should be connected", $dim ); @@ -524,8 +522,8 @@ macro_rules! gen_high_dim_tds_smoke { $dim ); prop_assert_eq!( - tds.number_of_vertices(), - tds.vertex_keys().count(), + dt.number_of_vertices(), + dt.vertices().count(), "{}D active TDS smoke vertex count should match key iteration", $dim ); @@ -533,7 +531,7 @@ macro_rules! gen_high_dim_tds_smoke { for (simplex_key, simplex) in dt.simplices() { if let Some(neighbors) = simplex.neighbors() { for neighbor_key in neighbors.flatten() { - let reciprocal = tds + let reciprocal = dt .simplex(neighbor_key) .and_then(|neighbor| neighbor.neighbors()) .is_some_and(|mut neighbor_neighbors| { diff --git a/tests/proptest_triangulation.rs b/tests/proptest_triangulation.rs index 2481b98e..9a04ce01 100644 --- a/tests/proptest_triangulation.rs +++ b/tests/proptest_triangulation.rs @@ -68,8 +68,8 @@ fn finite_coordinate() -> impl Strategy { /// comparison function. /// /// # Arguments -/// * `tds_orig` - Original triangulation -/// * `tds_transformed` - Transformed triangulation (translated, scaled, rotated, etc.) +/// * `dt_orig` - Original triangulation +/// * `dt_transformed` - Transformed triangulation (translated, scaled, rotated, etc.) /// * `uuid_map` - Mapping from original vertex UUIDs to transformed vertex UUIDs /// * `_metric_name` - Name of the metric being tested (for error messages) /// * `_dimension` - Dimensionality (for error messages) @@ -103,10 +103,8 @@ fn compare_transformed_simplices( where F: FnMut(SimplexKey, SimplexKey) -> Result<(), TestCaseError>, { - let tds_orig = dt_orig.tds(); - let tds_transformed = dt_transformed.tds(); - let orig_simplex_count = tds_orig.simplex_keys().count(); - let transformed_simplex_count = tds_transformed.simplex_keys().count(); + let orig_simplex_count = dt_orig.number_of_simplices(); + let transformed_simplex_count = dt_transformed.number_of_simplices(); prop_assert_eq!( orig_simplex_count, transformed_simplex_count, @@ -116,13 +114,14 @@ where let mut matched_transformed = Vec::with_capacity(transformed_simplex_count); // Iterate through all simplices in original triangulation - for orig_key in tds_orig.simplex_keys() { - prop_assert!( - tds_orig.simplex(orig_key).is_some(), - "original simplex key from iterator should exist: {orig_key:?}" - ); - let orig_simplex = tds_orig.simplex(orig_key).expect("checked above"); - let orig_uuids = orig_simplex.vertex_uuids(tds_orig)?; + for (orig_key, orig_simplex) in dt_orig.simplices() { + let mut orig_uuids = Vec::with_capacity(orig_simplex.number_of_vertices()); + for &vertex_key in orig_simplex.vertices() { + let vertex = dt_orig.vertex(vertex_key).ok_or_else(|| { + TestCaseError::fail(format!("missing original vertex {vertex_key:?}")) + })?; + orig_uuids.push(vertex.uuid()); + } let transformed_uuids: Vec<_> = orig_uuids .iter() .filter_map(|uuid| uuid_map.get(uuid)) @@ -135,22 +134,22 @@ where ); let mut matched_key = None; - for trans_key in tds_transformed.simplex_keys() { - prop_assert!( - tds_transformed.simplex(trans_key).is_some(), - "transformed simplex key from iterator should exist: {trans_key:?}" - ); - let trans_simplex = tds_transformed.simplex(trans_key).expect("checked above"); - if let Ok(trans_simplex_uuids) = trans_simplex.vertex_uuids(tds_transformed) { - // Check if simplices have same vertices (by UUID) - if transformed_uuids.len() == trans_simplex_uuids.len() - && transformed_uuids - .iter() - .all(|u| trans_simplex_uuids.contains(u)) - { - matched_key = Some(trans_key); - break; - } + for (trans_key, trans_simplex) in dt_transformed.simplices() { + let mut trans_simplex_uuids = Vec::with_capacity(trans_simplex.number_of_vertices()); + for &vertex_key in trans_simplex.vertices() { + let vertex = dt_transformed.vertex(vertex_key).ok_or_else(|| { + TestCaseError::fail(format!("missing transformed vertex {vertex_key:?}")) + })?; + trans_simplex_uuids.push(vertex.uuid()); + } + // Check if simplices have same vertices (by UUID) + if transformed_uuids.len() == trans_simplex_uuids.len() + && transformed_uuids + .iter() + .all(|u| trans_simplex_uuids.contains(u)) + { + matched_key = Some(trans_key); + break; } } @@ -392,9 +391,8 @@ macro_rules! test_quality_properties { .topology_guarantee(TopologyGuarantee::PLManifold) .build() { - let tds = dt.tds(); let tri = dt.as_triangulation(); - for simplex_key in tds.simplex_keys() { + for (simplex_key, _) in dt.simplices() { if let Ok(ratio) = radius_ratio(tri, simplex_key) { prop_assert!( ratio > 0.0, @@ -640,9 +638,8 @@ macro_rules! test_quality_properties { .topology_guarantee(TopologyGuarantee::PLManifold) .build() { - let tds = dt.tds(); let tri = dt.as_triangulation(); - for simplex_key in tds.simplex_keys() { + for (simplex_key, _) in dt.simplices() { let rr_result = radius_ratio(tri, simplex_key); let nv_result = normalized_volume(tri, simplex_key); @@ -751,7 +748,7 @@ macro_rules! test_facet_topology_invariant { .topology_guarantee(TopologyGuarantee::PLManifold) .build() { - let mut tri = dt.as_triangulation().clone(); + let mut tri = dt.into_triangulation(); // Get all simplex keys let simplex_keys: Vec<_> = tri.simplices().map(|(k, _)| k).collect(); diff --git a/tests/public_topology_api.rs b/tests/public_topology_api.rs index 800f9674..6a181f23 100644 --- a/tests/public_topology_api.rs +++ b/tests/public_topology_api.rs @@ -2,6 +2,7 @@ //! //! These tests cover: //! - Global edge enumeration via [`DelaunayTriangulation::edges`] +//! - Topological ridge enumeration via [`DelaunayTriangulation::ridges`] //! - Vertex incident edges via [`DelaunayTriangulation::incident_edges`] //! - Simplex neighborhood traversal via [`DelaunayTriangulation::simplex_neighbors`] //! - Building and validating opt-in split topology views @@ -11,9 +12,11 @@ use delaunay::prelude::TopologyGuarantee; use delaunay::prelude::Vertex; use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::query::*; -use delaunay::prelude::tds::TdsError; +use delaunay::prelude::tds::{SimplexKey, TdsError, VertexKey}; +use delaunay::prelude::validation::ManifoldError; use delaunay::vertex; use std::collections::HashSet; +use uuid::Uuid; #[derive(Debug, thiserror::Error)] enum PublicTopologyApiTestError { @@ -23,6 +26,10 @@ enum PublicTopologyApiTestError { CoordinateConversion(#[from] CoordinateConversionError), #[error(transparent)] TopologyIndex(#[from] TopologyIndexBuildError), + #[error(transparent)] + Query(#[from] QueryError), + #[error(transparent)] + Manifold(#[from] ManifoldError), #[error("single tetrahedron triangulation has no vertices")] EmptySingleTetrahedronVertices, #[error("single tetrahedron triangulation has no simplices")] @@ -114,6 +121,36 @@ macro_rules! gen_split_topology_single_simplex_tests { gen_split_topology_single_simplex_tests!(2, 3, 4, 5); +#[test] +fn public_ridges_return_vertex_candidates_in_2d() -> Result<(), PublicTopologyApiTestError> { + let vertices = standard_simplex_vertices::<2>()?; + let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build()?; + + let ridges = dt.ridges().collect::, _>>()?; + assert_eq!(ridges.len(), 3); + + let vertex_keys = dt.vertices().map(|(key, _)| key).collect::>(); + let mut ridge_vertices = HashSet::with_capacity(ridges.len()); + for ridge in &ridges { + assert_eq!(ridge.as_slice().len(), 1); + ridge_vertices.insert(ridge.as_slice()[0]); + + let view = dt.ridge_view(ridge)?; + assert_eq!(view.vertex_keys(), ridge.as_slice()); + assert_eq!(view.incident_simplices().len(), 1); + } + assert_eq!(ridge_vertices, vertex_keys); + + assert_eq!( + dt.ridge_handles() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?, + 0, + ); + Ok(()) +} + #[test] fn edges_and_incident_edges_on_single_tetrahedron() -> Result<(), PublicTopologyApiTestError> { // Single tetrahedron: 4 vertices, 1 simplex, 6 unique edges. @@ -177,6 +214,55 @@ fn edges_and_incident_edges_on_single_tetrahedron() -> Result<(), PublicTopology Ok(()) } +#[test] +fn owner_identity_queries_reject_unknown_ids_and_keys() -> Result<(), PublicTopologyApiTestError> { + let vertices = vec![ + vertex!([0.0, 0.0, 0.0])?, + vertex!([1.0, 0.0, 0.0])?, + vertex!([0.0, 1.0, 0.0])?, + vertex!([0.0, 0.0, 1.0])?, + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build()?; + let tri = dt.as_triangulation(); + let (simplex_key, simplex) = dt + .simplices() + .next() + .ok_or(PublicTopologyApiTestError::EmptySingleTetrahedronSimplices)?; + let vertex_key = simplex.vertices()[0]; + let simplex_uuid = dt + .simplex_uuid_from_key(simplex_key) + .ok_or(PublicTopologyApiTestError::EmptySingleTetrahedronSimplices)?; + let vertex_uuid = dt + .vertex_uuid_from_key(vertex_key) + .ok_or(PublicTopologyApiTestError::EmptySingleTetrahedronVertices)?; + + assert_eq!(dt.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + assert_eq!(tri.simplex_key_from_uuid(&simplex_uuid), Some(simplex_key)); + assert_eq!(dt.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + assert_eq!(tri.vertex_key_from_uuid(&vertex_uuid), Some(vertex_key)); + + assert_eq!(dt.simplex_key_from_uuid(&Uuid::nil()), None); + assert_eq!(tri.simplex_key_from_uuid(&Uuid::nil()), None); + assert_eq!(dt.vertex_key_from_uuid(&Uuid::nil()), None); + assert_eq!(tri.vertex_key_from_uuid(&Uuid::nil()), None); + assert_eq!(dt.simplex_uuid_from_key(SimplexKey::default()), None); + assert_eq!(tri.simplex_uuid_from_key(SimplexKey::default()), None); + assert_eq!(dt.vertex_uuid_from_key(VertexKey::default()), None); + assert_eq!(tri.vertex_uuid_from_key(VertexKey::default()), None); + assert!(dt.simplex(SimplexKey::default()).is_none()); + assert!(tri.simplex(SimplexKey::default()).is_none()); + assert!(dt.vertex(VertexKey::default()).is_none()); + assert!(tri.vertex(VertexKey::default()).is_none()); + assert!(!dt.contains_simplex(SimplexKey::default())); + assert!(!tri.contains_simplex(SimplexKey::default())); + assert!(!dt.contains_vertex_key(VertexKey::default())); + assert!(!tri.contains_vertex_key(VertexKey::default())); + + Ok(()) +} + #[test] fn split_topology_indexes_on_double_tetrahedron() -> Result<(), PublicTopologyApiTestError> { // Two tetrahedra sharing a triangular facet. @@ -250,6 +336,23 @@ fn split_topology_indexes_on_double_tetrahedron() -> Result<(), PublicTopologyAp let edges_via_tri: HashSet<_> = tri.edges().collect(); assert_eq!(edges_via_tri, edges); + // In 3D, topology ridges are edges and should be deduplicated across the shared facet. + let ridge_edges: HashSet<_> = dt + .ridges() + .map(|ridge| { + let ridge = ridge?; + assert_eq!(ridge.as_slice().len(), 2); + Ok((ridge.as_slice()[0], ridge.as_slice()[1])) + }) + .collect::>()?; + let edge_endpoints: HashSet<_> = edges.iter().map(|edge| edge.endpoints()).collect(); + assert_eq!(ridge_edges, edge_endpoints); + assert_eq!( + dt.ridge_handles() + .try_fold(0_usize, |count, ridge| ridge.map(|_| count + 1))?, + simplex_keys.len() * 6, + ); + // Missing keys should yield empty iterators. assert_eq!( incidence.adjacent_simplices(VertexKey::default()).count(), diff --git a/tests/regressions.rs b/tests/regressions.rs index c69c62dd..6c60bd1a 100644 --- a/tests/regressions.rs +++ b/tests/regressions.rs @@ -9,8 +9,6 @@ use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, ExplicitConstructionError, InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, Vertex, }; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::diagnostics::debug_print_first_delaunay_violation; use delaunay::prelude::generators::generate_random_points_in_ball_seeded; use delaunay::prelude::geometry::{CoordinateRange, Point, RobustKernel}; use delaunay::prelude::insertion::{HullExtensionReason, InsertionError}; @@ -490,7 +488,7 @@ fn regression_empty_circumsphere_2d_minimal_case() { if dt.is_valid_delaunay().is_err() { #[cfg(feature = "diagnostics")] - debug_print_first_delaunay_violation(dt.tds(), None); + dt.debug_print_first_delaunay_violation(None); } dt.repair_delaunay_with_flips().unwrap(); @@ -521,7 +519,7 @@ fn regression_issue_120_minimal_failing_input_2d() { if let Err(err) = dt.validate() { #[cfg(feature = "diagnostics")] - debug_print_first_delaunay_violation(dt.tds(), None); + dt.debug_print_first_delaunay_violation(None); panic!("Issue #120 2D regression must validate Levels 1-4: {err}"); } } @@ -560,7 +558,7 @@ fn regression_periodic_neighbor_validation_uses_lifted_vertex_offsets() { "periodic image-point construction should populate lifted per-simplex offsets" ); assert!( - dt.tds().is_valid().is_ok(), + dt.is_valid_structure().is_ok(), "neighbor validation must compare lifted (offset) identities" ); } diff --git a/tests/semgrep/docs/validation_levels.md b/tests/semgrep/docs/validation_levels.md index 55239a9f..5a6d2389 100644 --- a/tests/semgrep/docs/validation_levels.md +++ b/tests/semgrep/docs/validation_levels.md @@ -20,3 +20,24 @@ The crate provides a 5-level validation hierarchy. // ok: delaunay.docs.no-stale-four-level-validation-hierarchy Level 5: Delaunay Property + +// ruleid: delaunay.rust.no-tds-accessor-in-markdown-examples +let storage = dt.tds(); + +// ok: delaunay.rust.no-tds-accessor-in-markdown-examples +let index = dt.facet_incidence_index()?; + +// ruleid: delaunay.rust.no-as-triangulation-storage-reach-through +let storage = dt.as_triangulation().tds; + +// ruleid: delaunay.rust.no-as-triangulation-storage-reach-through, delaunay.rust.no-tds-accessor-in-markdown-examples +let storage = dt.as_triangulation().tds(); + +// ruleid: delaunay.rust.no-as-triangulation-storage-reach-through +let kernel = dt.as_triangulation().kernel; + +// ruleid: delaunay.rust.no-as-triangulation-storage-reach-through +let kernel = dt.as_triangulation().kernel(); + +// ok: delaunay.rust.no-as-triangulation-storage-reach-through +let generation = dt.topology_generation(); diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index 6692ccd3..97a42321 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -2,6 +2,10 @@ use num_traits::NumCast; +// ruleid: delaunay.rust.no-module-scope-cfg-test-use +#[cfg(test)] +use crate::tests::FixtureOnlyImport; + // ruleid: delaunay.rust.prefer-prelude-imports-in-examples-benches use delaunay::core::vertex::Vertex as DeepVertex; // ok: delaunay.rust.prefer-prelude-imports-in-examples-benches @@ -51,6 +55,22 @@ pub fn function_local_use_fixture() { let _ordering = Ordering::Equal; } +// ruleid: delaunay.rust.no-public-api-cfg-test-shim +#[cfg(any(test, feature = "diagnostics"))] +pub fn public_api_test_cfg_shim_fixture() {} + +// ruleid: delaunay.rust.no-public-api-cfg-test-shim +#[cfg(any(test, feature = "diagnostics"))] +#[expect( + clippy::missing_const_for_fn, + reason = "fixture models multi-line attributes between cfg and public item" +)] +pub fn public_api_test_cfg_with_multiline_attr_fixture() {} + +// ok: delaunay.rust.no-public-api-cfg-test-shim +#[cfg(feature = "diagnostics")] +pub fn public_api_feature_cfg_fixture() {} + pub fn deep_crate_path_fixture() { // ruleid: delaunay.rust.no-deep-crate-paths-in-functions let _buffer = crate::core::collections::SimplexKeyBuffer::new(); @@ -802,6 +822,97 @@ pub struct Tds { vertices: StorageMap>, } +struct PublicTdsAccessorOwner; + +impl PublicTdsAccessorOwner { + // ruleid: delaunay.rust.no-public-tds-accessor-methods + pub const fn tds(&self) -> &Tds { + todo!() + } +} + +struct CratePrivateTdsAccessorOwner; + +impl CratePrivateTdsAccessorOwner { + // ok: delaunay.rust.no-public-tds-accessor-methods + pub(crate) const fn tds(&self) -> &Tds { + todo!() + } +} + +struct KernelFixture; + +struct TriangulationOwnerFixture { + tds: Tds, + kernel: KernelFixture, +} + +impl TriangulationOwnerFixture { + fn tds(&self) -> &Tds { + todo!() + } + + fn kernel(&self) -> &KernelFixture { + todo!() + } +} + +struct DelaunayOwnerFixture; + +impl DelaunayOwnerFixture { + fn as_triangulation(&self) -> &TriangulationOwnerFixture { + todo!() + } + + fn into_triangulation(self) -> TriangulationOwnerFixture { + todo!() + } + + fn tds(&self) -> &Tds { + todo!() + } + + fn topology_generation(&self) -> u64 { + todo!() + } +} + +fn as_triangulation_storage_bypass_fixture(dt: &DelaunayOwnerFixture) { + // ruleid: delaunay.rust.no-as-triangulation-storage-reach-through + let _storage = &dt.as_triangulation().tds; + // ruleid: delaunay.rust.no-as-triangulation-storage-reach-through + let _storage = dt.as_triangulation().tds(); + // ruleid: delaunay.rust.no-as-triangulation-storage-reach-through + let _kernel = &dt.as_triangulation().kernel; + // ruleid: delaunay.rust.no-as-triangulation-storage-reach-through + let _kernel = dt.as_triangulation().kernel(); + // ok: delaunay.rust.no-as-triangulation-storage-reach-through + let _storage = dt.tds(); + // ok: delaunay.rust.no-as-triangulation-storage-reach-through + let _generation = dt.topology_generation(); + // ok: delaunay.rust.no-as-triangulation-storage-reach-through + let _tri = dt.as_triangulation(); +} + +fn as_triangulation_clone_fixture(dt: DelaunayOwnerFixture) { + // ruleid: delaunay.rust.no-as-triangulation-clone + let _owned = dt.as_triangulation().clone(); + // ruleid: delaunay.rust.no-as-triangulation-clone + let _owned_multiline = dt.as_triangulation() + .clone(); + // ok: delaunay.rust.no-as-triangulation-clone + let _tri = dt.as_triangulation(); + // ok: delaunay.rust.no-as-triangulation-clone + let _owned = dt.into_triangulation(); +} + +fn raw_tds_flip_predicate_verifier_fixture() { + // ruleid: delaunay.rust.no-raw-tds-flip-predicate-verifier-outside-core-flips + verify_tds_via_flip_predicates(tds, kernel); + // ok: delaunay.rust.no-raw-tds-flip-predicate-verifier-outside-core-flips + dt.verify_via_flip_predicates(); +} + // ruleid: delaunay.rust.tds-serialize-must-use-snapshot impl Serialize for Tds { fn serialize(&self, serializer: S) -> Result @@ -1310,6 +1421,13 @@ fn doctest_unwrap_expect_fixture() {} /// # fn main() -> delaunay::DelaunayResult<()> { Ok(()) } fn doctest_erased_error_fixture() {} +// ruleid: delaunay.rust.no-tds-accessor-in-doctests +/// let storage = dt.tds(); +/// +// ok: delaunay.rust.no-tds-accessor-in-doctests +/// let index = dt.facet_incidence_index()?; +fn doctest_tds_accessor_fixture() {} + // ruleid: delaunay.rust.prefer-assert-matches-in-doctests /// assert!(matches!(value, Some(_))); /// @@ -1328,3 +1446,29 @@ fn doctest_assert_matches_fixture() {} /// # use delaunay::flips::BistellarFlips as HiddenDeepImport; /// ``` fn triangulation_doctest_deep_import_fixture() {} + +// ruleid: delaunay.rust.benchmark-k2-facet-selection-requires-interior-neighbor-guard +pub fn flippable_k2_facet_missing_interior_neighbor_guard() -> Result<(), Error> { + for facet in facets { + let support = facet_support_points(dt, facet)?; + if accepts(&support) { + return Ok(()); + } + } + Err(Error) +} + +pub fn flippable_k2_facet_with_interior_neighbor_guard() -> Result<(), Error> { + for facet in facets { + if facet_neighbor_key(dt, facet)?.is_none() { + continue; + } + + // ok: delaunay.rust.benchmark-k2-facet-selection-requires-interior-neighbor-guard + let support = facet_support_points(dt, facet)?; + if accepts(&support) { + return Ok(()); + } + } + Err(Error) +} diff --git a/tests/serialization_vertex_preservation.rs b/tests/serialization_vertex_preservation.rs index b7990322..0a3f225d 100644 --- a/tests/serialization_vertex_preservation.rs +++ b/tests/serialization_vertex_preservation.rs @@ -14,8 +14,6 @@ use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, TopologyGuarantee, }; use delaunay::prelude::geometry::*; -use delaunay::prelude::query::extract_vertex_coordinate_set; -use delaunay::prelude::tds::Tds; use delaunay::try_vertices_from_points; use std::collections::HashSet; @@ -31,6 +29,12 @@ macro_rules! diag_debug { ($($arg:tt)*) => {}; } +fn vertex_coordinate_set( + dt: &DelaunayTriangulation, +) -> HashSet> { + dt.vertices().map(|(_, vertex)| *vertex.point()).collect() +} + /// Test vertex preservation with duplicate coordinates #[test] fn test_vertex_preservation_with_duplicates_3d() { @@ -55,12 +59,11 @@ fn test_vertex_preservation_with_duplicates_3d() { // Construct triangulation - duplicates should be skipped let dt = DelaunayTriangulation::builder(&vertices) .topology_guarantee(TopologyGuarantee::PLManifold) - .build() + .build_with_kernel(&RobustKernel::new()) .expect("DelaunayTriangulation::builder(...).build() failed"); - let tds = dt.tds(); - let tds_vertex_count = tds.vertices().count(); - let tds_coords = extract_vertex_coordinate_set(tds); + let tds_vertex_count = dt.vertices().count(); + let tds_coords = vertex_coordinate_set(&dt); diag_debug!( tds_vertex_count, unique_tds_coordinates = tds_coords.len(), @@ -75,14 +78,15 @@ fn test_vertex_preservation_with_duplicates_3d() { ); // Serialize - let json = serde_json::to_string(&tds).expect("Serialization failed"); + let json = serde_json::to_string(&dt).expect("Serialization failed"); diag_debug!(json_bytes = json.len(), "serialized TDS size"); // Deserialize - let deserialized: Tds<(), (), 3> = serde_json::from_str(&json).expect("Deserialization failed"); + let deserialized: DelaunayTriangulation, (), (), 3> = + serde_json::from_str(&json).expect("Deserialization failed"); let deser_vertex_count = deserialized.vertices().count(); - let deser_coords = extract_vertex_coordinate_set(&deserialized); + let deser_coords = vertex_coordinate_set(&deserialized); diag_debug!( deser_vertex_count, unique_deserialized_coordinates = deser_coords.len(), @@ -114,10 +118,9 @@ fn test_vertex_preservation_without_duplicates_3d() { let dt = DelaunayTriangulation::builder(&vertices) .topology_guarantee(TopologyGuarantee::PLManifold) - .build() + .build_with_kernel(&RobustKernel::new()) .expect("Tds construction failed"); - let tds = dt.tds(); - let tds_vertex_count = tds.vertices().count(); + let tds_vertex_count = dt.vertices().count(); diag_debug!( input_vertices = vertices.len(), tds_vertex_count, @@ -125,13 +128,14 @@ fn test_vertex_preservation_without_duplicates_3d() { ); // Extract vertex coordinate sets for Jaccard comparison - let before_coords = extract_vertex_coordinate_set(tds); + let before_coords = vertex_coordinate_set(&dt); - let json = serde_json::to_string(&tds).expect("Serialization failed"); - let deserialized: Tds<(), (), 3> = serde_json::from_str(&json).expect("Deserialization failed"); + let json = serde_json::to_string(&dt).expect("Serialization failed"); + let deserialized: DelaunayTriangulation, (), (), 3> = + serde_json::from_str(&json).expect("Deserialization failed"); let deser_vertex_count = deserialized.vertices().count(); - let after_coords = extract_vertex_coordinate_set(&deserialized); + let after_coords = vertex_coordinate_set(&deserialized); diag_debug!( deser_vertex_count, unique_deserialized_coordinates = after_coords.len(), @@ -184,10 +188,9 @@ fn test_vertex_preservation_many_duplicates_3d() { let opts = ConstructionOptions::default().with_insertion_order(InsertionOrderStrategy::Input); let dt = DelaunayTriangulation::builder(&vertices) .construction_options(opts) - .build() + .build_with_kernel(&RobustKernel::new()) .expect("Tds construction succeeded"); - let tds = dt.tds(); - let tds_vertex_count = tds.vertices().count(); + let tds_vertex_count = dt.vertices().count(); diag_debug!( tds_vertex_count, "many-duplicate vertex preservation after construction" @@ -200,13 +203,14 @@ fn test_vertex_preservation_many_duplicates_3d() { ); // Extract vertex coordinate sets for Jaccard comparison - let before_coords = extract_vertex_coordinate_set(tds); + let before_coords = vertex_coordinate_set(&dt); - let json = serde_json::to_string(&tds).expect("Serialization failed"); - let deserialized: Tds<(), (), 3> = serde_json::from_str(&json).expect("Deserialization failed"); + let json = serde_json::to_string(&dt).expect("Serialization failed"); + let deserialized: DelaunayTriangulation, (), (), 3> = + serde_json::from_str(&json).expect("Deserialization failed"); let deser_vertex_count = deserialized.vertices().count(); - let after_coords = extract_vertex_coordinate_set(&deserialized); + let after_coords = vertex_coordinate_set(&deserialized); diag_debug!( deser_vertex_count, unique_deserialized_coordinates = after_coords.len(), diff --git a/tests/trait_bound_ergonomics.rs b/tests/trait_bound_ergonomics.rs index b84a722a..fd65058c 100644 --- a/tests/trait_bound_ergonomics.rs +++ b/tests/trait_bound_ergonomics.rs @@ -4,6 +4,11 @@ use std::{assert_matches, hash::Hasher}; use delaunay::DelaunayTriangulation; use delaunay::prelude::Triangulation; +use delaunay::prelude::algorithms::{ + ConflictError, LocateError, extract_cavity_boundary, find_conflict_region, locate, + locate_with_stats, +}; +use delaunay::prelude::collections::SimplexKeyBuffer; use delaunay::prelude::construction::{GlobalTopology, TopologyGuarantee, TopologyKind}; use delaunay::prelude::geometry::{Coordinate, CoordinateValidationError, FastKernel, Point}; use delaunay::prelude::query::FacetIncidenceAnalysis; @@ -163,6 +168,61 @@ fn read_only_topology_apis_accept_non_datatype_payloads() { assert!(topology.is_valid()); } +#[test] +fn locate_and_conflict_apis_accept_non_datatype_payloads() { + let point = Point::try_new([0.25, 0.25]).unwrap(); + let kernel = FastKernel::new(); + let tds: Tds = Tds::empty(); + let empty_conflict_region = SimplexKeyBuffer::default(); + let tri: Triangulation, Payload, Payload, 2> = + Triangulation::new_empty(FastKernel::new()); + let dt: DelaunayTriangulation, Payload, Payload, 2> = + DelaunayTriangulation::with_empty_kernel(FastKernel::new()); + + assert_matches!( + locate(&tds, &kernel, &point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + locate_with_stats(&tds, &kernel, &point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + find_conflict_region(&tds, &kernel, &point, SimplexKey::default()), + Err(ConflictError::InvalidStartSimplex { .. }) + ); + assert_matches!( + extract_cavity_boundary(&tds, &empty_conflict_region), + Ok(boundary) if boundary.is_empty() + ); + + assert_matches!( + tri.locate(&point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + tri.locate_with_stats(&point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + tri.find_conflict_region(&point, SimplexKey::default()), + Err(ConflictError::InvalidStartSimplex { .. }) + ); + + assert_matches!( + dt.locate(&point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + dt.locate_with_stats(&point, None), + Err(LocateError::EmptyTriangulation) + ); + assert_matches!( + dt.find_conflict_region(&point, SimplexKey::default()), + Err(ConflictError::InvalidStartSimplex { .. }) + ); +} + #[test] fn tds_equality_accepts_non_datatype_payloads() { let left: Tds = Tds::empty(); diff --git a/tests/triangulation_builder.rs b/tests/triangulation_builder.rs index 18d4df04..22d545ef 100644 --- a/tests/triangulation_builder.rs +++ b/tests/triangulation_builder.rs @@ -19,9 +19,7 @@ use delaunay::prelude::geometry::RobustKernel; use delaunay::prelude::insertion::InsertionError; use delaunay::prelude::tds::{InvariantError, TdsConstructionError, TdsError, VertexKey}; use delaunay::prelude::topology::spaces::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; -use delaunay::prelude::topology::validation::{ - TopologyClassification, count_simplices, euler_characteristic, validate_triangulation_euler, -}; +use delaunay::prelude::topology::validation::{TopologyClassification, euler_characteristic}; use delaunay::prelude::validation::{TriangulationValidationError, ValidationPolicy}; // ============================================================================= @@ -471,7 +469,7 @@ fn count_boundary_facets(dt: &DelaunayTriangulation(); assert!( - dt.tds().is_valid().is_ok(), - "TDS structural validity should pass for periodic triangulation" + dt.is_valid_structure().is_ok(), + "structural validity should pass for periodic triangulation" ); - let counts = count_simplices(dt.tds()).unwrap(); + let counts = dt.simplex_counts().unwrap(); let chi = euler_characteristic(&counts); assert_eq!( chi, 0, "Euler characteristic of periodic 2D triangulation must be 0 (torus)" ); - let semantic_result = validate_triangulation_euler(dt.tds(), dt.global_topology()).unwrap(); + let semantic_result = dt.euler_check().unwrap(); assert_eq!( semantic_result.classification, TopologyClassification::ClosedToroid(2), @@ -542,7 +540,7 @@ fn test_builder_toroidal_convenience() { .expect("periodic toroidal builder should succeed"); assert!(dt.global_topology().is_periodic()); - assert!(dt.tds().is_valid().is_ok()); + assert!(dt.is_valid_structure().is_ok()); } /// Periodic quotient construction exposes the same fluent statistics terminal. @@ -827,8 +825,8 @@ fn test_explicit_2d_two_triangle_quad() { assert_eq!(dt.number_of_vertices(), 4); assert_eq!(dt.number_of_simplices(), 2); assert!( - dt.tds().is_valid().is_ok(), - "TDS should be structurally valid" + dt.is_valid_structure().is_ok(), + "triangulation should be structurally valid" ); // Verify neighbor pointers: the two triangles share the edge (0,2) so @@ -898,8 +896,8 @@ fn test_explicit_normalizes_incoherent_simplex_order() { .expect("explicit build should normalize incoherent simplex ordering"); assert!( - dt.tds().is_valid().is_ok(), - "builder should canonicalize incoherent simplex orderings into a valid TDS" + dt.is_valid_structure().is_ok(), + "builder should canonicalize incoherent simplex orderings into a valid structure" ); } @@ -924,8 +922,8 @@ fn test_explicit_3d_two_tetrahedra() { assert_eq!(dt.number_of_vertices(), 5); assert_eq!(dt.number_of_simplices(), 2); assert!( - dt.tds().is_valid().is_ok(), - "TDS should be structurally valid" + dt.is_valid_structure().is_ok(), + "triangulation should be structurally valid" ); } @@ -946,8 +944,7 @@ fn test_explicit_round_trip_3d() { let original_vertex_count = dt_original.number_of_vertices(); let original_simplex_count = dt_original.number_of_simplices(); - let tds = dt_original.tds(); - let vertex_keys: Vec<_> = tds.vertex_keys().collect(); + let vertex_keys: Vec<_> = dt_original.vertices().map(|(key, _)| key).collect(); let key_to_index: HashMap<_, _> = vertex_keys .iter() .enumerate() @@ -956,11 +953,11 @@ fn test_explicit_round_trip_3d() { let extracted_vertices: Vec<_> = vertex_keys .iter() - .map(|&vk| *tds.vertex(vk).unwrap()) + .map(|&vk| *dt_original.vertex(vk).unwrap()) .collect(); let mut simplex_specs: Vec> = Vec::new(); - for (_, simplex) in tds.simplices() { + for (_, simplex) in dt_original.simplices() { let spec: Vec = simplex .vertices() .iter() @@ -983,8 +980,8 @@ fn test_explicit_round_trip_3d() { original_simplex_count ); assert!( - dt_reconstructed.tds().is_valid().is_ok(), - "Reconstructed 3D TDS should be structurally valid" + dt_reconstructed.is_valid_structure().is_ok(), + "reconstructed 3D triangulation should be structurally valid" ); } @@ -1006,8 +1003,7 @@ fn test_explicit_round_trip_2d() { let original_simplex_count = dt_original.number_of_simplices(); // Extract vertex keys → index mapping and simplex specifications. - let tds = dt_original.tds(); - let vertex_keys: Vec<_> = tds.vertex_keys().collect(); + let vertex_keys: Vec<_> = dt_original.vertices().map(|(key, _)| key).collect(); let key_to_index: HashMap<_, _> = vertex_keys .iter() .enumerate() @@ -1016,11 +1012,11 @@ fn test_explicit_round_trip_2d() { let extracted_vertices: Vec<_> = vertex_keys .iter() - .map(|&vk| *tds.vertex(vk).unwrap()) + .map(|&vk| *dt_original.vertex(vk).unwrap()) .collect(); let mut simplex_specs: Vec> = Vec::new(); - for (_, simplex) in tds.simplices() { + for (_, simplex) in dt_original.simplices() { let spec: Vec = simplex .vertices() .iter() @@ -1044,8 +1040,8 @@ fn test_explicit_round_trip_2d() { original_simplex_count ); assert!( - dt_reconstructed.tds().is_valid().is_ok(), - "Reconstructed TDS should be structurally valid" + dt_reconstructed.is_valid_structure().is_ok(), + "reconstructed triangulation should be structurally valid" ); } @@ -1157,7 +1153,7 @@ fn test_explicit_2d_single_triangle() { assert_eq!(dt.number_of_vertices(), 3); assert_eq!(dt.number_of_simplices(), 1); - assert!(dt.tds().is_valid().is_ok()); + assert!(dt.is_valid_structure().is_ok()); } /// Minimal case: a single tetrahedron in 3D. @@ -1178,7 +1174,7 @@ fn test_explicit_3d_single_tetrahedron() { assert_eq!(dt.number_of_vertices(), 4); assert_eq!(dt.number_of_simplices(), 1); - assert!(dt.tds().is_valid().is_ok()); + assert!(dt.is_valid_structure().is_ok()); } /// Non-Delaunay mesh: prescribed connectivity that violates the empty-circumsphere From 2f7bac802f9c1837dd5696bfe215b502783c2426 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 4 Jul 2026 01:48:09 -0700 Subject: [PATCH 2/3] test(pachner): harden flip benchmark fixtures - Split 3D flip fixture coverage by move kind so failures identify the affected Pachner workflow. - Use localized ridge-star views for k=3 support inspection and surface typed traversal failures. - Align topology proptest assertions and exact-predicate regression diagnostics with the validators they exercise. - Document owner-level structure validators and make the cfg(test) shim Semgrep rule line-local. --- benches/common/flip_workflows.rs | 33 ++++++--- docs/validation.md | 12 +++ semgrep.yaml | 2 +- tests/benchmark_flip_fixtures.rs | 123 ++++++++++++++++++++----------- tests/delaunay_edge_cases.rs | 18 +++-- tests/proptest_tds.rs | 35 +++++---- 6 files changed, 149 insertions(+), 74 deletions(-) diff --git a/benches/common/flip_workflows.rs b/benches/common/flip_workflows.rs index 0eb3f13c..41ce1573 100644 --- a/benches/common/flip_workflows.rs +++ b/benches/common/flip_workflows.rs @@ -19,7 +19,7 @@ use delaunay::prelude::construction::{ use delaunay::prelude::geometry::{CoordinateConversionError, Point, RobustKernel, simplex_volume}; use delaunay::prelude::query::{JaccardComputationError, QueryError, format_jaccard_report}; use delaunay::prelude::tds::{EdgeKeyError, FacetError, InvariantError, VertexKey}; -use delaunay::prelude::topology::validation::{RidgeCandidate, RidgeCandidateError}; +use delaunay::prelude::topology::validation::{ManifoldError, RidgeCandidate, RidgeCandidateError}; use delaunay::prelude::validation::DelaunayTriangulationValidationError; use thiserror::Error; use uuid::Uuid; @@ -92,6 +92,16 @@ pub enum FlipWorkflowError { source: RidgeCandidateError, }, + /// Ridge star traversal failed before support inspection. + #[error("failed to inspect ridge star for {ridge:?}: {source}")] + RidgeStar { + /// Ridge handle being inspected. + ridge: RidgeHandle, + /// Underlying ridge-star traversal failure. + #[source] + source: Box, + }, + /// Snapshot collection found a dangling simplex-to-vertex incidence. #[error("simplex references missing vertex key {vertex_key:?}")] DanglingSnapshotVertex { @@ -1381,15 +1391,20 @@ fn ridge_support_points( .map(|(_, vertex_key)| *vertex_key), ) .map_err(|source| FlipWorkflowError::InvalidRidgeCandidate { ridge, source })?; + let ridge_view = + dt.ridge_view(&ridge_candidate) + .map_err(|source| FlipWorkflowError::RidgeStar { + ridge, + source: Box::new(source), + })?; let mut keys = Vec::new(); - for (_, star_simplex) in dt.simplices() { - if ridge_candidate - .as_slice() - .iter() - .all(|vertex_key| star_simplex.vertices().contains(vertex_key)) - { - keys.extend(star_simplex.vertices()); - } + for &star_simplex_key in ridge_view.incident_simplices() { + let star_simplex = + dt.simplex(star_simplex_key) + .ok_or(FlipWorkflowError::MissingSimplex { + simplex_key: star_simplex_key, + })?; + keys.extend(star_simplex.vertices()); } keys.sort_unstable(); keys.dedup(); diff --git a/docs/validation.md b/docs/validation.md index f42f109e..a78fd939 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -309,6 +309,18 @@ Validates the combinatorial structure of the Triangulation Data Structure. - `Tds::structure_diagnostic()` - First actionable Level 2 diagnostic. - `Tds::structure_report()` - All checkable Level 2 structural failures. - `Tds::validate()` - Levels 1–2 (elements + structural). +- `Triangulation::is_valid_structure()` / + `DelaunayTriangulation::is_valid_structure()` - Owner-level Level 2 + fast-fail validation without exposing storage. +- `Triangulation::validate_structure()` / + `DelaunayTriangulation::validate_structure()` - Owner-level Levels 1–2 + validation. +- `Triangulation::structure_diagnostic()` / + `DelaunayTriangulation::structure_diagnostic()` - Owner-level first + actionable Level 2 diagnostic. +- `Triangulation::structure_report()` / + `DelaunayTriangulation::structure_report()` - Owner-level aggregate Level 2 + diagnostics. - `DelaunayTriangulation::validation_report()` - Cumulative diagnostic report across Levels 1–5. ### What It Checks diff --git a/semgrep.yaml b/semgrep.yaml index de639772..0ef60761 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -353,7 +353,7 @@ rules: - "/tests/**/*.rs" - "/benches/**/*.rs" - "/examples/**/*.rs" - pattern-regex: '(?ms)^[ \t]*#\[cfg\(any\([^\]\n]*\btest\b[^\]\n]*\)\)\][^\n]*\n(?:(?!^[ \t]*pub[ \t]+(?:use|(?:const[ \t]+)?fn)\b).*\n)*^[ \t]*pub[ \t]+(?:use|(?:const[ \t]+)?fn)\b' # yamllint disable-line rule:line-length + pattern-regex: '(?m)^[ \t]*#\[cfg\((?:test|any\([^\]\n]*\btest\b[^\]\n]*\))\)\][^\n]*\n(?:[ \t]*#\[[^\]\n]*\][^\n]*\n|[ \t]*#\[[^\n]*\n(?:[ \t]+[^\n]*\n){0,8}[ \t]*[)\]]+[^\n]*\n|[ \t]*///[^\n]*\n|[ \t]*\n){0,8}[ \t]*pub[ \t]+(?:use|(?:const[ \t]+)?fn)\b' # yamllint disable-line rule:line-length - id: delaunay.rust.no-deep-crate-paths-in-functions languages: diff --git a/tests/benchmark_flip_fixtures.rs b/tests/benchmark_flip_fixtures.rs index 30c4fae6..eff4db5a 100644 --- a/tests/benchmark_flip_fixtures.rs +++ b/tests/benchmark_flip_fixtures.rs @@ -67,18 +67,51 @@ fn flip_fixtures_cover_2d_workflows() { ); } -/// Verifies the stable 3D public flip fixture workflows. +/// Verifies the stable 3D public k=1 fixture workflow. #[test] -fn flip_fixtures_cover_stable_3d_workflows() { - verify_3d_fixture(STABLE_POINTS_3D, CandidateFilter::Any); +fn flip_fixtures_cover_stable_3d_k1_roundtrip() { + verify_3d_fixture_move(STABLE_POINTS_3D, CandidateFilter::Any, FlipMoveKind::K1); } -/// Verifies the adversarial 3D public flip fixture workflows. +/// Verifies the stable 3D public k=2 fixture workflow. #[test] -fn flip_fixtures_cover_adversarial_3d_workflows() { - verify_3d_fixture( +fn flip_fixtures_cover_stable_3d_k2_roundtrip() { + verify_3d_fixture_move(STABLE_POINTS_3D, CandidateFilter::Any, FlipMoveKind::K2); +} + +/// Verifies the stable 3D public k=3 fixture workflow. +#[test] +fn flip_fixtures_cover_stable_3d_k3_forward() { + verify_3d_fixture_move(STABLE_POINTS_3D, CandidateFilter::Any, FlipMoveKind::K3); +} + +/// Verifies the adversarial 3D public k=1 fixture workflow. +#[test] +fn flip_fixtures_cover_adversarial_3d_k1_roundtrip() { + verify_3d_fixture_move( ADVERSARIAL_POINTS_3D, CandidateFilter::TouchesAdversarialFeature, + FlipMoveKind::K1, + ); +} + +/// Verifies the adversarial 3D public k=2 fixture workflow. +#[test] +fn flip_fixtures_cover_adversarial_3d_k2_roundtrip() { + verify_3d_fixture_move( + ADVERSARIAL_POINTS_3D, + CandidateFilter::TouchesAdversarialFeature, + FlipMoveKind::K2, + ); +} + +/// Verifies the adversarial 3D public k=3 fixture workflow. +#[test] +fn flip_fixtures_cover_adversarial_3d_k3_forward() { + verify_3d_fixture_move( + ADVERSARIAL_POINTS_3D, + CandidateFilter::TouchesAdversarialFeature, + FlipMoveKind::K3, ); } @@ -420,46 +453,52 @@ fn verify_2d_fixture(points: &[[f64; 2]], filter: CandidateFilter) { .expect("2D benchmark k=2 forward flip should preserve topology"); } -/// Verifies all selected 3D public flip workflows for one fixture. -fn verify_3d_fixture(points: &[[f64; 3]], filter: CandidateFilter) { +/// Verifies one selected 3D public flip workflow for one fixture. +fn verify_3d_fixture_move(points: &[[f64; 3]], filter: CandidateFilter, move_kind: FlipMoveKind) { let base_dt = build_flip_dt(points).expect("3D benchmark flip fixture should build"); assert_topology_and_delaunay_valid(&base_dt, "3D benchmark flip fixture"); - let simplex_key = largest_volume_simplex(&base_dt, filter) - .expect("3D benchmark fixture should provide a selected k=1 simplex"); - if filter == CandidateFilter::TouchesAdversarialFeature { - assert!( - simplex_touches_adversarial_feature(&base_dt, simplex_key) - .expect("3D k=1 support should be inspectable"), - "3D adversarial k=1 support should touch an adversarial fixture feature" - ); - } - verify_k1_roundtrip(&base_dt, simplex_key) - .expect("3D k=1 roundtrip should recover the same triangulation"); - - let facet = flippable_k2_facet(&base_dt, true, filter) - .expect("3D benchmark fixture should provide a selected k=2 facet"); - if filter == CandidateFilter::TouchesAdversarialFeature { - assert!( - facet_support_touches_adversarial_feature(&base_dt, facet) - .expect("3D k=2 support should be inspectable"), - "3D adversarial k=2 support should touch an adversarial fixture feature" - ); - } - verify_k2_roundtrip(&base_dt, facet) - .expect("3D k=2 roundtrip should recover the same triangulation"); - - let ridge = flippable_k3_ridge(&base_dt, false, filter) - .expect("3D benchmark fixture should provide a selected k=3 ridge"); - if filter == CandidateFilter::TouchesAdversarialFeature { - assert!( - ridge_support_touches_adversarial_feature(&base_dt, ridge) - .expect("3D k=3 support should be inspectable"), - "3D adversarial k=3 support should touch an adversarial fixture feature" - ); + match move_kind { + FlipMoveKind::K1 => { + let simplex_key = largest_volume_simplex(&base_dt, filter) + .expect("3D benchmark fixture should provide a selected k=1 simplex"); + if filter == CandidateFilter::TouchesAdversarialFeature { + assert!( + simplex_touches_adversarial_feature(&base_dt, simplex_key) + .expect("3D k=1 support should be inspectable"), + "3D adversarial k=1 support should touch an adversarial fixture feature" + ); + } + verify_k1_roundtrip(&base_dt, simplex_key) + .expect("3D k=1 roundtrip should recover the same triangulation"); + } + FlipMoveKind::K2 => { + let facet = flippable_k2_facet(&base_dt, true, filter) + .expect("3D benchmark fixture should provide a selected k=2 facet"); + if filter == CandidateFilter::TouchesAdversarialFeature { + assert!( + facet_support_touches_adversarial_feature(&base_dt, facet) + .expect("3D k=2 support should be inspectable"), + "3D adversarial k=2 support should touch an adversarial fixture feature" + ); + } + verify_k2_roundtrip(&base_dt, facet) + .expect("3D k=2 roundtrip should recover the same triangulation"); + } + FlipMoveKind::K3 => { + let ridge = flippable_k3_ridge(&base_dt, false, filter) + .expect("3D benchmark fixture should provide a selected k=3 ridge"); + if filter == CandidateFilter::TouchesAdversarialFeature { + assert!( + ridge_support_touches_adversarial_feature(&base_dt, ridge) + .expect("3D k=3 support should be inspectable"), + "3D adversarial k=3 support should touch an adversarial fixture feature" + ); + } + verify_k3_forward(&base_dt, ridge) + .expect("3D benchmark k=3 forward flip should preserve topology"); + } } - verify_k3_forward(&base_dt, ridge) - .expect("3D benchmark k=3 forward flip should preserve topology"); } /// Verifies one selected roundtrip-capable public flip workflow for one dimension. diff --git a/tests/delaunay_edge_cases.rs b/tests/delaunay_edge_cases.rs index 203318d0..e862070a 100644 --- a/tests/delaunay_edge_cases.rs +++ b/tests/delaunay_edge_cases.rs @@ -14,7 +14,7 @@ use delaunay::prelude::construction::{ DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, }; use delaunay::prelude::generators::{ - generate_random_points_in_ball_seeded, + RandomPointCount, generate_random_points_in_ball_seeded, try_generate_random_triangulation_with_topology_guarantee, }; use delaunay::prelude::geometry::RobustKernel; @@ -26,6 +26,8 @@ use rand::SeedableRng; use rand::seq::SliceRandom; use std::num::NonZeroUsize; +const EXACT_PREDICATE_FAST_POINT_COUNT: NonZeroUsize = nonzero(8); + const fn nonzero(value: usize) -> NonZeroUsize { NonZeroUsize::new(value).expect("test point count must be non-zero") } @@ -918,7 +920,7 @@ fn test_collinear_points_2d() { /// Fast regression test for the exact-predicate code paths changed in #228. /// -/// Constructs a 3D triangulation from 16 random ball-distributed points using +/// Constructs a 3D triangulation from a small random ball-distributed point set using /// `AdaptiveKernel` (the default; exact+SoS predicates) and verifies the /// Delaunay property. This exercises: /// - `det_errbound()` fast filter in orientation/insphere predicates @@ -930,7 +932,9 @@ fn test_collinear_points_2d() { #[test] fn regression_issue_228_exact_predicate_paths_3d_fast() { let seed: u64 = 0x0228_FA53_0003; - let points = generate_random_points_in_ball_seeded::<3>(16, 100.0, seed) + let point_count = RandomPointCount::<3>::try_new(EXACT_PREDICATE_FAST_POINT_COUNT) + .expect("fast regression point count should build a 3D triangulation"); + let points = generate_random_points_in_ball_seeded::<3>(point_count.get(), 100.0, seed) .expect("point generation should succeed"); let vertices: Vec> = points .into_iter() @@ -940,11 +944,13 @@ fn regression_issue_228_exact_predicate_paths_3d_fast() { let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices) .topology_guarantee(TopologyGuarantee::Pseudomanifold) .build() - .expect("3D 16-point construction must not fail (#228 fast regression)"); + .expect("3D exact-predicate fast regression construction must not fail (#228)"); + let delaunay_result = dt.verify_via_flip_predicates(); assert!( - dt.verify_via_flip_predicates().is_ok(), - "Delaunay property must hold (#228 fast regression, seed=0x{seed:X})" + delaunay_result.is_ok(), + "Delaunay property must hold (#228 fast regression, seed=0x{seed:X}): {:?}", + delaunay_result.err() ); assert!(dt.number_of_vertices() > 0); assert!(dt.number_of_simplices() > 0); diff --git a/tests/proptest_tds.rs b/tests/proptest_tds.rs index 02193b9f..9ef35be4 100644 --- a/tests/proptest_tds.rs +++ b/tests/proptest_tds.rs @@ -424,26 +424,27 @@ gen_simplex_vertex_count!(4, 5, #[cfg(feature = "slow-tests")]); gen_simplex_vertex_count!(5, 6, #[cfg(feature = "slow-tests")]); // ============================================================================= -// CONNECTIVITY TESTS (2D-5D) +// TOPOLOGY VALIDITY TESTS (2D-5D) // ============================================================================= // -// Property: every successfully-constructed Delaunay triangulation is connected. -// This validates that `Tds::is_connected` (BFS over neighbor pointers) does not -// produce false negatives on well-formed triangulations. +// Property: every successfully-constructed Delaunay triangulation satisfies the +// public Level 3 topology validator. -macro_rules! gen_is_connected { +macro_rules! gen_is_valid_topology { ($dim:literal $(, #[$attr:meta])*) => { pastey::paste! { proptest! { $(#[$attr])* #[test] - fn [](vertices in []()) { + fn [](vertices in []()) { if let Ok(dt) = DelaunayTriangulation::builder(&vertices).build() { + let topology_result = dt.as_triangulation().is_valid_topology(); prop_assert!( - dt.as_triangulation().is_valid_topology().is_ok(), - "{}D successfully-built triangulation must be connected ({} simplices)", + topology_result.is_ok(), + "{}D successfully-built triangulation must satisfy Level 3 topology ({} simplices): {:?}", $dim, - dt.number_of_simplices() + dt.number_of_simplices(), + topology_result.err() ); } } @@ -452,13 +453,13 @@ macro_rules! gen_is_connected { }; } -gen_is_connected!(2); +gen_is_valid_topology!(2); -gen_is_connected!(3); +gen_is_valid_topology!(3); -gen_is_connected!(4, #[cfg(feature = "slow-tests")]); +gen_is_valid_topology!(4, #[cfg(feature = "slow-tests")]); -gen_is_connected!(5, #[cfg(feature = "slow-tests")]); +gen_is_valid_topology!(5, #[cfg(feature = "slow-tests")]); // ============================================================================= // FAST HIGH-DIMENSIONAL CI SMOKE TESTS @@ -510,10 +511,12 @@ macro_rules! gen_high_dim_tds_smoke { $dim, dt.is_valid_structure().err() ); + let topology_result = dt.as_triangulation().is_valid_topology(); prop_assert!( - dt.as_triangulation().is_valid_topology().is_ok(), - "{}D active TDS smoke should be connected", - $dim + topology_result.is_ok(), + "{}D active TDS smoke should satisfy Level 3 topology: {:?}", + $dim, + topology_result.err() ); prop_assert_eq!( dt.dim(), From 310ed68307784d8b9921447e817088528d66a39e Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 4 Jul 2026 02:32:47 -0700 Subject: [PATCH 3/3] feat(bench): add Pachner stress workflows - Add 3D, 4D, and combined Pachner Monte Carlo stress recipes with report output enabled. - Document manual stress diagnostics, release-active knobs, and Pachner stress defaults. - Cover owner-level structure validation helpers and keep benchmark flip fixture checks under CI timeouts. --- benches/README.md | 43 +++++++++++------- docs/dev/commands.md | 9 ++++ docs/dev/debug_env_vars.md | 22 +++++++++ justfile | 77 ++++++++++++++++++++++++++++++++ tests/benchmark_flip_fixtures.rs | 51 +++++++++++++++------ tests/public_topology_api.rs | 22 +++++++++ 6 files changed, 194 insertions(+), 30 deletions(-) diff --git a/benches/README.md b/benches/README.md index b987d3b9..2a59eaf6 100644 --- a/benches/README.md +++ b/benches/README.md @@ -43,7 +43,7 @@ predicates fast across 2D-5D. | Predicate cold-path work | `cargo bench --profile perf --bench cold_path_predicates -- --noplot` | | Flip-based Delaunay repair | `cargo bench --profile perf --bench delaunay_repair -- --noplot` | | Flip-repair transaction pressure | `cargo bench --profile perf --bench delaunay_repair -- repair_transaction_pressure --noplot` | -| Unified Pachner move stress | `cargo bench --profile perf --bench pachner_stress -- --noplot` | +| Unified Pachner move stress | `just pachner-stress` | | PL-manifold repair path | `cargo bench --profile perf --features bench --bench pl_manifold_repair -- --noplot` | | Large-scale scaling suite | `cargo bench --profile perf --bench profiling_suite -- --noplot` | | Vertex deletion mutation baseline | `cargo bench --profile perf --bench delete_vertex -- --noplot` | @@ -188,7 +188,9 @@ profile summary workflow and captures the construction metrics automatically. ## Pachner Stress ```bash -cargo bench --profile perf --bench pachner_stress -- --noplot +just pachner-stress +just pachner-stress-3d +just pachner-stress-4d ``` `pachner_stress.rs` contains two layers: @@ -196,25 +198,33 @@ cargo bench --profile perf --bench pachner_stress -- --noplot - accepted-move microcases for the unified 4D Pachner API facade - manual Monte Carlo stress cases for 3D and 4D long-run topology stability -The Monte Carlo cases default to 10,000 vertices in 3D and 1,000 vertices in -4D, with 100,000 attempted random Pachner moves and topology validation every -1,000 attempts. They validate topology plus the Level 4 embedding invariant -that arbitrary Pachner moves are expected to preserve; Level 5 Delaunay -validity is not a postcondition of random topology edits. The move stream runs -through the `markov-chain-monte-carlo` delayed proposal API with a flat target, -so successfully planned Pachner proposals commit with 100% acceptance while -invalid local candidates are recorded as no-proposal self-loops. Each measured -sequence emits a `pachner_stress_metric` line with accepted/rejected attempts, -proposal diagnostics, validation time, final simplex count, and RSS memory -counters. Validation failures include recent MCMC trace rows so long chains can -be diagnosed by step, outcome, and topology size. +The `just` recipes run the Monte Carlo cases at the default issue-scale target: +10,000 vertices in 3D and 1,000 vertices in 4D, with 100,000 attempted random +Pachner moves per Criterion sample and topology validation every 1,000 +attempts. Criterion requires at least 10 samples, so a default dimension-specific +recipe measures at least ten 100K-move sequences. These recipes enable +`DELAUNAY_PACHNER_STRESS_REPORT=1`, causing each measured sequence to emit a +`pachner_stress_metric` line with accepted/rejected attempts, proposal +diagnostics, validation time, final simplex count, and RSS memory counters. + +The stress cases validate topology plus the Level 4 embedding invariant that +arbitrary Pachner moves are expected to preserve; Level 5 Delaunay validity is +not a postcondition of random topology edits. The move stream runs through the +`markov-chain-monte-carlo` delayed proposal API with a flat target, so +successfully planned Pachner proposals commit with 100% acceptance while invalid +local candidates are recorded as no-proposal self-loops. Validation failures +include recent MCMC trace rows so long chains can be diagnosed by step, outcome, +and topology size. Useful overrides: ```bash +just pachner-stress-4d 10000 250 1000 10 + +DELAUNAY_PACHNER_STRESS_REPORT=1 \ DELAUNAY_PACHNER_STRESS_ATTEMPTS=10000 \ DELAUNAY_PACHNER_STRESS_VERTICES_4D=250 \ -cargo bench --profile perf --bench pachner_stress -- "monte_carlo/4d" +cargo bench --profile perf --bench pachner_stress -- "monte_carlo/4d" --noplot ``` Supported override families are `DELAUNAY_PACHNER_STRESS_VERTICES`, @@ -222,7 +232,8 @@ Supported override families are `DELAUNAY_PACHNER_STRESS_VERTICES`, `DELAUNAY_PACHNER_STRESS_VALIDATE_EVERY`, `DELAUNAY_PACHNER_STRESS_KEY_REFRESH_EVERY`, and `DELAUNAY_PACHNER_STRESS_SEED`. Append `_3D` or `_4D` for a -dimension-specific value. +dimension-specific value. Use `MONTE_CARLO_SAMPLE_SIZE` to change Criterion's +sample count; the benchmark enforces Criterion's minimum of 10 samples. ## Circumsphere Containment diff --git a/docs/dev/commands.md b/docs/dev/commands.md index 0896b27b..11d2c835 100644 --- a/docs/dev/commands.md +++ b/docs/dev/commands.md @@ -271,6 +271,9 @@ just perf-compare just perf-vs-ref just perf-no-regressions just bench-perf-summary +just pachner-stress +just pachner-stress-3d +just pachner-stress-4d cargo bench --profile perf --bench ci_performance_suite ``` @@ -284,6 +287,12 @@ Workspace-wide benchmark recipes (`just bench`, `just bench-smoke`, `just bench-compile`, and the benchmark compile step inside `just ci`) enable `--features bench` so feature-gated benchmark fixtures are compiled. +Use `just pachner-stress [attempts] [validate_every] [samples]` for the full +manual 3D+4D Pachner Monte Carlo diagnostic run. The dimension-specific +`just pachner-stress-3d` and `just pachner-stress-4d` recipes default to +100,000 attempted moves per Criterion sample and enable report lines so long +chains can be diagnosed without making the workflow part of routine CI. + Some repair benchmarks need feature-gated fixtures that deliberately construct invalid-but-structurally-coherent topology. Run those harnesses with `--features bench`; the `bench` feature exists only for benchmark fixtures and diff --git a/docs/dev/debug_env_vars.md b/docs/dev/debug_env_vars.md index a4254513..45c80739 100644 --- a/docs/dev/debug_env_vars.md +++ b/docs/dev/debug_env_vars.md @@ -162,6 +162,8 @@ These variables configure property tests in `tests/proptest_*.rs`. ## Benchmarks +### CI Performance Suite + These variables configure `benches/ci_performance_suite.rs` and run in release builds only. @@ -172,6 +174,26 @@ release builds only. | `DELAUNAY_BENCH_DISCOVER_SEEDS_LIMIT` | **value** (integer) | Maximum seeds to try per (dim, count) pair during discovery or fallback (default: 2000) | | `DELAUNAY_BENCH_EXPORT_METRICS` | presence | Metric-only mode: print `api_benchmark_metric` vertex/simplex counts; optional `tds_new` filter | +### Pachner Stress Benchmarks + +These variables configure `benches/pachner_stress.rs` and run in release builds. +The `just pachner-stress*` recipes set the report flag and dimension-specific +defaults for the 3D and 4D Monte Carlo cases. + +| Variable | Activation | Description | +|---|---|---| +| `DELAUNAY_PACHNER_STRESS_REPORT` | presence | `[release]` Print source and `pachner_stress_metric` lines for Monte Carlo runs | +| `DELAUNAY_PACHNER_STRESS_VERTICES` | **value** (integer) | `[release]` Shared initial vertex count override | +| `DELAUNAY_PACHNER_STRESS_VERTICES_{D}D` | **value** (integer) | `[release]` Per-dimension vertex count override, e.g. `_3D` or `_4D` | +| `DELAUNAY_PACHNER_STRESS_ATTEMPTS` | **value** (integer) | `[release]` Shared attempted-move count per Criterion sample | +| `DELAUNAY_PACHNER_STRESS_ATTEMPTS_{D}D` | **value** (integer) | `[release]` Per-dimension attempted-move count per Criterion sample | +| `DELAUNAY_PACHNER_STRESS_VALIDATE_EVERY` | **value** (integer) | `[release]` Shared periodic topology/embedding validation cadence | +| `DELAUNAY_PACHNER_STRESS_VALIDATE_EVERY_{D}D` | **value** (integer) | `[release]` Per-dimension validation cadence | +| `DELAUNAY_PACHNER_STRESS_KEY_REFRESH_EVERY` | **value** (integer) | `[release]` Shared cached-key refresh cadence | +| `DELAUNAY_PACHNER_STRESS_KEY_REFRESH_EVERY_{D}D` | **value** (integer) | `[release]` Per-dimension cached-key refresh cadence | +| `DELAUNAY_PACHNER_STRESS_SEED` | **value** (integer) | `[release]` Shared Monte Carlo RNG seed | +| `DELAUNAY_PACHNER_STRESS_SEED_{D}D` | **value** (integer) | `[release]` Per-dimension Monte Carlo RNG seed | + ## Miscellaneous | Variable | Activation | Module | Description | diff --git a/justfile b/justfile index 36fe375c..18586cfc 100644 --- a/justfile +++ b/justfile @@ -214,6 +214,73 @@ bench-perf-summary: _ensure-uv bench-smoke: CRIT_SAMPLE_SIZE=10 CRIT_MEASUREMENT_MS=500 CRIT_WARMUP_MS=200 cargo bench --workspace --profile perf --features bench +# Run the 3D and 4D Pachner Monte Carlo stress cases with reports enabled. +pachner-stress attempts="100000" validate_every="1000" samples="10": + just _pachner-stress-dim 3d 10000 "{{ attempts }}" "{{ validate_every }}" "{{ samples }}" + just _pachner-stress-dim 4d 1000 "{{ attempts }}" "{{ validate_every }}" "{{ samples }}" + +# Run the 3D Pachner Monte Carlo stress case with reports enabled. +pachner-stress-3d attempts="100000" vertices="10000" validate_every="1000" samples="10": + just _pachner-stress-dim 3d "{{ vertices }}" "{{ attempts }}" "{{ validate_every }}" "{{ samples }}" + +# Run the 4D Pachner Monte Carlo stress case with reports enabled. +pachner-stress-4d attempts="100000" vertices="1000" validate_every="1000" samples="10": + just _pachner-stress-dim 4d "{{ vertices }}" "{{ attempts }}" "{{ validate_every }}" "{{ samples }}" + +[private] +_pachner-stress-dim label vertices attempts validate_every samples: + #!/usr/bin/env bash + set -euo pipefail + + label="{{ label }}" + vertices="{{ vertices }}" + attempts="{{ attempts }}" + validate_every="{{ validate_every }}" + samples="{{ samples }}" + + require_positive_integer() { + local name="$1" + local value="$2" + if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then + echo "ERROR: $name must be a positive integer, got: $value" >&2 + exit 2 + fi + } + + require_positive_integer "vertices" "$vertices" + require_positive_integer "attempts" "$attempts" + require_positive_integer "validate_every" "$validate_every" + require_positive_integer "samples" "$samples" + + if (( samples < 10 )); then + echo "ERROR: samples must be at least 10 because Criterion requires sample_size >= 10." >&2 + exit 2 + fi + + case "$label" in + 3d) + suffix="3D" + filter="monte_carlo/3d" + ;; + 4d) + suffix="4D" + filter="monte_carlo/4d" + ;; + *) + echo "ERROR: unsupported Pachner stress dimension: $label" >&2 + exit 2 + ;; + esac + + echo "Pachner stress ${suffix}: ${vertices} vertices, ${attempts} attempted moves per Criterion sample." + env \ + DELAUNAY_PACHNER_STRESS_REPORT=1 \ + "DELAUNAY_PACHNER_STRESS_VERTICES_${suffix}=$vertices" \ + "DELAUNAY_PACHNER_STRESS_ATTEMPTS_${suffix}=$attempts" \ + "DELAUNAY_PACHNER_STRESS_VALIDATE_EVERY_${suffix}=$validate_every" \ + "MONTE_CARLO_SAMPLE_SIZE=$samples" \ + cargo bench --profile perf --bench pachner_stress -- "$filter" --noplot + # Compile benchmarks and release integration tests without running. bench-test-compile: bench-compile test-integration-compile @@ -382,6 +449,11 @@ help-workflows: @echo " just bench-smoke # Smoke-test benchmark harnesses (minimal samples)" @echo " just bench # Run all benchmarks with perf profile (ThinLTO)" @echo " just bench-ci # CI regression benchmarks with perf profile (~5-10 min)" + @echo " just pachner-stress # 3D+4D Pachner MCMC stress with report lines" + @echo " just pachner-stress-3d [attempts] [vertices] [validate_every] [samples]" + @echo " # 3D Pachner MCMC stress (defaults: 100K, 10K vertices)" + @echo " just pachner-stress-4d [attempts] [vertices] [validate_every] [samples]" + @echo " # 4D Pachner MCMC stress (defaults: 100K, 1K vertices)" @echo " just perf-large-scale-smoke [max_secs] # Quick pre-push 2D-5D wall-clock guard (default 60s)" @echo " just perf-no-regressions [threshold] # Fast pre-PR 2D-5D regression guard (default 7.5%)" @echo " just perf-baseline [ref] # Persist/update default local baseline (default: main)" @@ -602,6 +674,9 @@ perf-help: @echo " just bench # Full benchmark suite with perf profile" @echo " just bench-ci # CI benchmark suite with perf profile" @echo " just bench-allocations # Allocation-contract microbenchmarks" + @echo " just pachner-stress # 3D+4D Pachner MCMC stress with report lines" + @echo " just pachner-stress-3d # 3D Pachner MCMC stress (100K moves, 10K vertices)" + @echo " just pachner-stress-4d # 4D Pachner MCMC stress (100K moves, 1K vertices)" @echo " just perf-no-regressions # Fast pre-PR 2D-5D regression guard" @echo " just bench-smoke # Smoke-test benchmark harnesses" @echo "" @@ -620,6 +695,8 @@ perf-help: @echo " just perf-baseline-to /tmp/delaunay-main-baseline" @echo " # Generate scratch main baseline without overwriting baseline-artifact" @echo " CRIT_SAMPLE_SIZE=100 just bench # Custom sample size" + @echo " just pachner-stress-4d 100000 1000 1000 10" + @echo " # 4D long-run Pachner diagnostics with minimum Criterion samples" @echo " just bench-ci # Final optimized CI-suite benchmark run" @echo " just profile v0.7.5 # v0.7.5 code on its declared Rust toolchain" @echo " just profile 1.96.0 # Current tree on Rust 1.96.0" diff --git a/tests/benchmark_flip_fixtures.rs b/tests/benchmark_flip_fixtures.rs index eff4db5a..e589fb22 100644 --- a/tests/benchmark_flip_fixtures.rs +++ b/tests/benchmark_flip_fixtures.rs @@ -3,15 +3,18 @@ //! Tests for benchmark-owned public bistellar flip fixtures. //! //! The roundtrip assertions are n=1 ergodicity checks for the public -//! Pachner/bistellar move API. For each selected fixture move, one admissible -//! flip followed immediately by its inverse must recover the same valid -//! triangulation: the same vertex UUID set and the same simplex-to-vertex UUID -//! incidence. Exact equality remains the pass/fail condition; failed roundtrips -//! report Jaccard similarity for the vertex and simplex-incidence sets to make -//! near misses debuggable. Pachner's connectedness theorem motivates the broader -//! ergodicity story; see `REFERENCES.md` under "Bistellar (Pachner) Moves and -//! Delaunay Repair". - +//! Pachner/bistellar move API. For each selected roundtrip fixture move, one +//! admissible flip followed immediately by its inverse must recover the same +//! valid triangulation: the same vertex UUID set and the same simplex-to-vertex +//! UUID incidence. Exact equality remains the pass/fail condition; failed +//! roundtrips report Jaccard similarity for the vertex and simplex-incidence +//! sets to make near misses debuggable. Forward-only checks keep the default +//! fixture suite focused on one selected admissible move. Pachner's connectedness +//! theorem motivates the broader ergodicity story; see `REFERENCES.md` under +//! "Bistellar (Pachner) Moves and Delaunay Repair". + +// Reuse the benchmark-owned fixture catalog so this integration test certifies +// the same public flip workflows that the Criterion harnesses measure. #[path = "../benches/common/flip_fixtures.rs"] mod flip_fixtures; #[path = "../benches/common/flip_workflows.rs"] @@ -57,6 +60,14 @@ enum RoundtripMove { K3, } +const MINIMAL_K2_ROUNDTRIP_POINTS_3D: &[[f64; 3]] = &[ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.20, 0.20, 0.85], + [0.20, 0.20, -0.85], +]; + /// Verifies the stable and adversarial 2D public flip fixture workflows. #[test] fn flip_fixtures_cover_2d_workflows() { @@ -75,10 +86,22 @@ fn flip_fixtures_cover_stable_3d_k1_roundtrip() { /// Verifies the stable 3D public k=2 fixture workflow. #[test] -fn flip_fixtures_cover_stable_3d_k2_roundtrip() { +fn flip_fixtures_cover_stable_3d_k2_forward() { verify_3d_fixture_move(STABLE_POINTS_3D, CandidateFilter::Any, FlipMoveKind::K2); } +/// Verifies the public 3D k=2 inverse workflow on a minimal local support. +#[test] +fn flip_fixtures_cover_minimal_3d_k2_roundtrip() { + let base_dt = + build_flip_dt(MINIMAL_K2_ROUNDTRIP_POINTS_3D).expect("minimal 3D fixture should build"); + assert_topology_and_delaunay_valid(&base_dt, "minimal 3D k=2 fixture"); + let facet = flippable_k2_facet(&base_dt, true, CandidateFilter::Any) + .expect("minimal 3D fixture should provide a roundtrip-capable k=2 facet"); + verify_k2_roundtrip(&base_dt, facet) + .expect("minimal 3D k=2 roundtrip should recover the same triangulation"); +} + /// Verifies the stable 3D public k=3 fixture workflow. #[test] fn flip_fixtures_cover_stable_3d_k3_forward() { @@ -97,7 +120,7 @@ fn flip_fixtures_cover_adversarial_3d_k1_roundtrip() { /// Verifies the adversarial 3D public k=2 fixture workflow. #[test] -fn flip_fixtures_cover_adversarial_3d_k2_roundtrip() { +fn flip_fixtures_cover_adversarial_3d_k2_forward() { verify_3d_fixture_move( ADVERSARIAL_POINTS_3D, CandidateFilter::TouchesAdversarialFeature, @@ -473,7 +496,7 @@ fn verify_3d_fixture_move(points: &[[f64; 3]], filter: CandidateFilter, move_kin .expect("3D k=1 roundtrip should recover the same triangulation"); } FlipMoveKind::K2 => { - let facet = flippable_k2_facet(&base_dt, true, filter) + let facet = flippable_k2_facet(&base_dt, false, filter) .expect("3D benchmark fixture should provide a selected k=2 facet"); if filter == CandidateFilter::TouchesAdversarialFeature { assert!( @@ -482,8 +505,8 @@ fn verify_3d_fixture_move(points: &[[f64; 3]], filter: CandidateFilter, move_kin "3D adversarial k=2 support should touch an adversarial fixture feature" ); } - verify_k2_roundtrip(&base_dt, facet) - .expect("3D k=2 roundtrip should recover the same triangulation"); + verify_k2_forward(&base_dt, facet) + .expect("3D benchmark k=2 forward flip should preserve topology"); } FlipMoveKind::K3 => { let ridge = flippable_k3_ridge(&base_dt, false, filter) diff --git a/tests/public_topology_api.rs b/tests/public_topology_api.rs index 6a181f23..03ef7178 100644 --- a/tests/public_topology_api.rs +++ b/tests/public_topology_api.rs @@ -121,6 +121,28 @@ macro_rules! gen_split_topology_single_simplex_tests { gen_split_topology_single_simplex_tests!(2, 3, 4, 5); +#[test] +fn owner_level_structure_validation_helpers_succeed_on_valid_topology() +-> Result<(), PublicTopologyApiTestError> { + let vertices = standard_simplex_vertices::<3>()?; + let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices) + .topology_guarantee(TopologyGuarantee::PLManifold) + .build()?; + let tri = dt.as_triangulation(); + + assert!(dt.is_valid_structure().is_ok()); + assert!(dt.validate_structure().is_ok()); + assert!(dt.structure_diagnostic().is_none()); + assert!(dt.structure_report().is_ok()); + + assert!(tri.is_valid_structure().is_ok()); + assert!(tri.validate_structure().is_ok()); + assert!(tri.structure_diagnostic().is_none()); + assert!(tri.structure_report().is_ok()); + + Ok(()) +} + #[test] fn public_ridges_return_vertex_candidates_in_2d() -> Result<(), PublicTopologyApiTestError> { let vertices = standard_simplex_vertices::<2>()?;